[DX11] Render fbx, pmx, obj

ChangJin·2025년 10월 1일

DirectX11

목록 보기
6/13

글에 사용된 모든 그림과 내용은 직접 작성한 것입니다.

[유튜브 영상]


[깃허브 보러가기]
https://github.com/Chang-Jin-Lee/D3D11-AliceTutorial/tree/main/17_fbx_pmx_obj_WithPhong

[풀리퀘 보러가기]
https://github.com/Chang-Jin-Lee/D3D11-AliceTutorial/pull/27


글의 목적

D3D11에서 assimp로 FBX를 로드해 각 버퍼를 생성해 값을 할당하는 흐름을 정리하기 위함


  • 아래의 아티클에서 모델 준비를 살펴볼 수 있습니다

https://velog.io/@whoamicj/DX11-Ready-for-Rendering-fbx-pmx

  • 이 글에서는 3D 모델이 완벽하게 구해졌다는 것을 가정하고 설명합니다
  • 또한 Relative Path로 텍스쳐 경로가 fbx 파일에 임베드 되어 있다고 가정합니다

Load

  • assimp의 가장 중요한 부분입니다. Assimp::Importer를 사용해 파일을 읽고, 이 파일에 존재하는 텍스쳐(만약 없다면 경로로 순회), 버텍스 정보, 노말 정보, uv 정보, tangent 정보, bitangent 정보 등을 모두 얻어낼 수 있습니다.
bool FbxManager::Load(ID3D11Device* device, const std::wstring& pathW)
{
    Release();
    Assimp::Importer importer;
    std::string pathA(pathW.begin(), pathW.end());
    const aiScene* scene = importer.ReadFile(pathA,
        aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_ImproveCacheLocality |
        aiProcess_GenSmoothNormals | aiProcess_CalcTangentSpace | aiProcess_ConvertToLeftHanded);
    if (!scene || !scene->HasMeshes()) return false;

    std::wstring baseDir = pathW;
    size_t slash = baseDir.find_last_of(L"/\\");
    baseDir = (slash == std::wstring::npos) ? L"" : baseDir.substr(0, slash + 1);

    if (!LoadMaterials(device, scene, baseDir)) return false;
    if (!BuildMeshBuffers(device, scene)) return false;
    return true;
}
  • pathA에는 다음처럼 fbx 파일의 경로가 들어가게 됩니다

  • 그리고 텍스쳐 경로를 순회하기 위해서 파일을 제외한 폴더명까지의 경로를 준비합니다

LoadMaterials

  • 이제 각 경우를 나누어서 구현합니다
  1. fbx 자체에 텍스처가 임베드 되어 있는가?
  2. 없다면 인덱스로 순회하면서 찾을 수 있는가?
  3. 그것도 안된다면 Relative path로 순회하여 찾을 수 있는가?
  4. 그것도 안된다면 이 모델은 텍스처가 없다.
bool FbxManager::LoadMaterials(ID3D11Device* device, const aiScene* scene, const std::wstring& baseDir)
{
    if (!m_pWhite)
    {
        UINT white = 0xFFFFFFFF;
        D3D11_TEXTURE2D_DESC td{}; td.Width = 1; td.Height = 1; td.MipLevels = 1; td.ArraySize = 1;
        td.Format = DXGI_FORMAT_R8G8B8A8_UNORM; td.SampleDesc.Count = 1; td.Usage = D3D11_USAGE_IMMUTABLE; td.BindFlags = D3D11_BIND_SHADER_RESOURCE;
        D3D11_SUBRESOURCE_DATA sd{}; sd.pSysMem = &white; sd.SysMemPitch = sizeof(UINT);
        ComPtr<ID3D11Texture2D> tex; HR_T(device->CreateTexture2D(&td, &sd, tex.GetAddressOf()));
        D3D11_SHADER_RESOURCE_VIEW_DESC srvd{}; srvd.Format = td.Format; srvd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; srvd.Texture2D.MipLevels = 1; srvd.Texture2D.MostDetailedMip = 0;
        HR_T(device->CreateShaderResourceView(tex.Get(), &srvd, &m_pWhite));
    }

    m_MaterialSRVs.assign(scene->mNumMaterials, nullptr);

    auto findCached = [&](const std::wstring& key){ auto it = m_TexCache.find(key); return it==m_TexCache.end()? (ID3D11ShaderResourceView*)nullptr : it->second; };
    auto addCache = [&](const std::wstring& key, ID3D11ShaderResourceView* v){ if (v) { m_TexCache[key] = v; v->AddRef(); } };

    for (unsigned m = 0; m < scene->mNumMaterials; ++m)
    {
        aiMaterial* mat = scene->mMaterials[m];
        aiString texPath;
        if (mat->GetTexture(aiTextureType_DIFFUSE, 0, &texPath) == AI_SUCCESS)
        {
            std::string t = texPath.C_Str();

            // fbx 파일에 임베디드된 텍스처 로딩을 시도함 이름 또는 *인덱스 표기를 모두 처리함 (Assimp 헬퍼를 사용해서)
            if (!t.empty())
            {
                const aiTexture* at = scene->GetEmbeddedTexture(t.c_str());
                if (at)
                {
                    ComPtr<ID3D11Resource> res; ID3D11ShaderResourceView* srv = nullptr;
                    if (at->mHeight == 0)
                    {
                        // 이미지를 읽어옴. 압축 버퍼를 사용함 (PNG/JPG 등)
                        if (SUCCEEDED(CreateWICTextureFromMemory(device, reinterpret_cast<const uint8_t*>(at->pcData), at->mWidth, res.GetAddressOf(), &srv)))
                            m_MaterialSRVs[m] = srv;
                    }
                    else
                    {
                        // RAW BGRA8 픽셀 데이터
                        D3D11_TEXTURE2D_DESC td{}; td.Width = at->mWidth; td.Height = at->mHeight; td.MipLevels = 1; td.ArraySize = 1;
                        td.Format = DXGI_FORMAT_B8G8R8A8_UNORM; td.SampleDesc.Count = 1; td.Usage = D3D11_USAGE_IMMUTABLE; td.BindFlags = D3D11_BIND_SHADER_RESOURCE;
                        D3D11_SUBRESOURCE_DATA sd{}; sd.pSysMem = at->pcData; sd.SysMemPitch = at->mWidth * sizeof(aiTexel);
                        ComPtr<ID3D11Texture2D> tex;
                        if (SUCCEEDED(device->CreateTexture2D(&td, &sd, tex.GetAddressOf())))
                        {
                            D3D11_SHADER_RESOURCE_VIEW_DESC srvd{}; srvd.Format = td.Format; srvd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; srvd.Texture2D.MipLevels = 1; srvd.Texture2D.MostDetailedMip = 0;
                            if (SUCCEEDED(device->CreateShaderResourceView(tex.Get(), &srvd, &srv))) m_MaterialSRVs[m] = srv;
                        }
                    }
                }
            }

            // 임베디드된 텍스쳐가 없거나 실패 시, 구형 *인덱스 방식 처리
            if (!m_MaterialSRVs[m] && !t.empty() && t[0] == '*')
            {
                int idx = atoi(t.c_str() + 1);
                if (idx >= 0 && (unsigned)idx < scene->mNumTextures)
                {
                    const aiTexture* at = scene->mTextures[idx];
                    if (at)
                    {
                        ComPtr<ID3D11Resource> res; ID3D11ShaderResourceView* srv = nullptr;
                        if (at->mHeight == 0)
                        {
                            if (SUCCEEDED(CreateWICTextureFromMemory(device, reinterpret_cast<const uint8_t*>(at->pcData), at->mWidth, res.GetAddressOf(), &srv)))
                                m_MaterialSRVs[m] = srv;
                        }
                        else
                        {
                            D3D11_TEXTURE2D_DESC td{}; td.Width = at->mWidth; td.Height = at->mHeight; td.MipLevels = 1; td.ArraySize = 1;
                            td.Format = DXGI_FORMAT_B8G8R8A8_UNORM; td.SampleDesc.Count = 1; td.Usage = D3D11_USAGE_IMMUTABLE; td.BindFlags = D3D11_BIND_SHADER_RESOURCE;
                            D3D11_SUBRESOURCE_DATA sd{}; sd.pSysMem = at->pcData; sd.SysMemPitch = at->mWidth * sizeof(aiTexel);
                            ComPtr<ID3D11Texture2D> tex;
                            if (SUCCEEDED(device->CreateTexture2D(&td, &sd, tex.GetAddressOf())))
                            {
                                D3D11_SHADER_RESOURCE_VIEW_DESC srvd{}; srvd.Format = td.Format; srvd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; srvd.Texture2D.MipLevels = 1; srvd.Texture2D.MostDetailedMip = 0;
                                if (SUCCEEDED(device->CreateShaderResourceView(tex.Get(), &srvd, &srv))) m_MaterialSRVs[m] = srv;
                            }
                        }
                    }
                }
            }

            // 위 방법 둘다 안될경우에 외부 파일 경로 시도
            if (!m_MaterialSRVs[m])
            {
                std::wstring wtex = WStringFromUtf8(t);
                bool isAbs = (!wtex.empty() && (wtex.find(L":") != std::wstring::npos || wtex[0] == L'/' || wtex[0] == L'\\'));
                std::wstring full = isAbs ? wtex : (baseDir + wtex);
                if (auto* cached = findCached(full)) { m_MaterialSRVs[m] = cached; cached->AddRef(); }
                else
                {
                    ComPtr<ID3D11Resource> res; ID3D11ShaderResourceView* srv = nullptr;
                    if (SUCCEEDED(CreateWICTextureFromFile(device, full.c_str(), res.GetAddressOf(), &srv))) { m_MaterialSRVs[m] = srv; addCache(full, srv); }
                }
            }
        }
        if (!m_MaterialSRVs[m]) { m_MaterialSRVs[m] = m_pWhite; if (m_pWhite) m_pWhite->AddRef(); }
    }
    return true;
}

  • 텍스처가 없는 경우에는 이 texPath가 빈 스트링으로 나옵니다

  • 텍스처가 있는 경우는 이 texPath가 블렌더에서 설정한 그 경로로 나오게 됩니다

  • 따라서 맨 아래부분 외부 파일 경로로 시도하는 부분에서 텍스처를 로딩하게 됩니다

BuildMeshBuffers

  • 이제 버텍스 정보, 노말 정보, uv 정보, tangent 정보, bitangent 정보를 로드하면 됩니다
bool FbxManager::BuildMeshBuffers(ID3D11Device* device, const aiScene* scene)
{

	......
    
    std::vector<VertexTBN> vertices; vertices.reserve(8192);
    std::vector<uint32_t> indices; indices.reserve(16384);
    m_Subsets.clear();

    std::function<void(const aiNode*, const aiMatrix4x4&)> traverse;
    traverse = [&](const aiNode* node, const aiMatrix4x4& parent){
        aiMatrix4x4 global = parent * node->mTransformation;
        for (unsigned mi = 0; mi < node->mNumMeshes; ++mi)
        {
            const aiMesh* mesh = scene->mMeshes[node->mMeshes[mi]];
            size_t base = vertices.size();
            for (unsigned i = 0; i < mesh->mNumVertices; ++i)
            {
                aiVector3D p = mesh->mVertices[i];
                aiVector3D n = mesh->HasNormals() ? mesh->mNormals[i] : aiVector3D(0,1,0);
                aiVector3D uv = mesh->HasTextureCoords(0) ? mesh->mTextureCoords[0][i] : aiVector3D(0,0,0);
                aiVector3D tg = mesh->HasTangentsAndBitangents() ? mesh->mTangents[i]   : aiVector3D(1,0,0);
                aiVector3D bt = mesh->HasTangentsAndBitangents() ? mesh->mBitangents[i] : aiVector3D(0,1,0);

                vertices.push_back({ {p.x,p.y,p.z}, {n.x,n.y,n.z}, {tg.x,tg.y,tg.z}, {bt.x,bt.y,bt.z}, {1,1,1,1}, {uv.x,uv.y} });
            }
            uint32_t start = (uint32_t)indices.size();
            for (unsigned f = 0; f < mesh->mNumFaces; ++f)
            {
                const aiFace& face = mesh->mFaces[f];
                if (face.mNumIndices == 3)
                {
                    // Reverse winding to correct back/front after Y-flip
                    indices.push_back((uint32_t)(base + face.mIndices[0]));
                    indices.push_back((uint32_t)(base + face.mIndices[1]));
                    indices.push_back((uint32_t)(base + face.mIndices[2]));
                }
            }
            uint32_t count = (uint32_t)indices.size() - start;
            m_Subsets.push_back({ start, count, mesh->mMaterialIndex });
        }
        for (unsigned ci = 0; ci < node->mNumChildren; ++ci) traverse(node->mChildren[ci], global);
    };
    traverse(scene->mRootNode, aiMatrix4x4());
	
    ......
}

  • 각 정보가 들어가는 것을 확인할 수 있습니다

Render

  • 이제 Constant Buffer를 통해서 GPU에게 값을 전달해 그려내기만 하면 됩니다. 다음의 쉐이더 코드로 만들면 됩니다
#include "17_Shared.fxh"

// 픽셀 셰이더(쉐이더/셰이더)
float4 main(VertexOut pIn) : SV_Target
{
	// 디버그 단축 경로들 (g_Pad)
	if (abs(g_Pad - 1.0f) < 1e-3) { return float4(1,0,1,1); }
	if (abs(g_Pad - 2.0f) < 1e-3) { return float4(1,1,1,1); }
	if (abs(g_Pad - 3.0f) < 1e-3) { return pIn.color; }


	// 알파 컷아웃 조명 모드에서만 적용
	float4 textureColor = g_DiffuseMap.Sample(g_Sam, pIn.tex);
    float alphaTex = textureColor.a * g_Material.diffuse.a;
    clip(alphaTex - 0.1f);

	// 월드 노말 계산(Nw) - 노말맵 토글에 따라 분기
	float3 N = normalize(pIn.normalW);
	if (g_EnableNormalMap != 0)
	{
		float3 T = normalize(pIn.tangentW);
		float3 N = normalize(pIn.normalW);
		float3 B = normalize(pIn.bitanW);
		float handed = dot(cross(T, B), N);
		if (handed < 0.0f) B = -B;
		float3x3 TBN = float3x3(T, B, N);
		float3 N_ts = g_NormalMap.Sample(g_Sam, pIn.tex).xyz * 2.0f - 1.0f;
		N_ts.y = -N_ts.y; // 그린 채널 반전 보정
		N_ts = normalize(N_ts);
		N = normalize(mul(N_ts, TBN));
	}

	// 공통: 라이팅 벡터들
	float3 L = normalize(-g_DirLight.direction);
	float3 V = normalize(g_EyePosW - pIn.posW);
	float NdotL = dot(N, L);
	float theta = saturate(NdotL);

	float4 ambientTerm  = g_Material.ambient * g_DirLight.ambient;
    float4 diffuseTerm  = theta * g_DirLight.diffuse;
    float4 specularTerm = 0;

	// 분기별 조명 계산
	if (g_ShadingMode == 0)
	{
		// Phong
		float3 R = reflect(-L, N);
		float NdotV = saturate(dot(N, V));
		float specGate = step(0.0f, NdotL) * step(0.0f, NdotV);
		float s = pow(max(dot(R, V), 0.0f), max(g_Material.specular.w, 1.0f)) * specGate;
		specularTerm = s * g_Material.specular * g_DirLight.specular;
	}
	else if (g_ShadingMode == 1)
	{
		// Blinn-Phong
		float3 H = normalize(L + V);
        float NdotH = saturate(dot(N, H));
        float NdotV = saturate(dot(N, V));
        float specGate = step(0.0f, NdotL) * step(0.0f, NdotV);
        float s = pow(NdotH, g_Material.specular.w) * specGate;
        specularTerm = s * g_Material.specular * g_DirLight.specular;
	}
	else if (g_ShadingMode == 2)
	{
		// Lambert
		specularTerm = 0;
	}
	// Unlit (3): 조명 없이 텍스처*diffuse만
	else if (g_ShadingMode == 3)
	{
		ambientTerm = 0;
        diffuseTerm = 0;
        specularTerm = 0;
	}
	// TextureOnly (4): 텍스처만 출력
	else if (g_ShadingMode == 4)
	{
		float4 only = textureColor * g_Material.diffuse;
        only.a = alphaTex;
        return only;
	}
	
	// kd = texture * material.diffuse
    float4 kd = textureColor * g_Material.diffuse;
    float4 litColor = kd * (ambientTerm + diffuseTerm) + specularTerm;

    // 환경 반사 (Unlit/Lambert에는 적용하지 않음)
    if (g_ShadingMode == 0 || g_ShadingMode == 1)
    {
        float3 Renv = reflect(-V, N);
        float roughness = saturate(g_Material.reflect.a);
        const float kMaxMip = 8.0f;
        float mipBias = roughness * roughness * kMaxMip;
        float4 reflectionColor = g_TexCube.SampleBias(g_Sam, Renv, mipBias);
        float reflectGate = theta;
        litColor += (g_Material.reflect * reflectGate) * reflectionColor;
    }

    litColor.a = alphaTex;
    return litColor;
}

  • 이제 다음처럼 pmx, fbx, obj 모두 로드하여 렌더할 수 있습니다

PMX

pmx - Phongpmx - Blinn Phong


pmx - Lambertpmx - TextureOnly


FBX

fbx - Phongfbx - Blinn Phong


fbx - Lambertfbx - TextureOnly


OBJ

obj - Phongobj - Blinn Phong


obj - Lambertobj - TextureOnly




profile
게임 프로그래머

4개의 댓글

comment-user-thumbnail
2025년 10월 15일

도움이 많이 되었어요

1개의 답글
comment-user-thumbnail
2026년 1월 17일

늘 신세 지고 있습니다.

1개의 답글