04
11

https://dlemrcnd.tistory.com/85?category=525778 

 

DirectX11 3D - FBX SDK, FBX Importer, FBX 파일 불러오기, 3ds Max Exporter Plugin, Assimp (1)

1. 3ds Max Exporter Plugin 게임에서는 FBX, OBJ 같은 확장자를 가진 파일을 직접 탑재해서 구동하지 않는다. FBX, OBJ, ASE 같은 확장자의 모델링 된 파일을 가지고 해석하는 과정, 컨버팅 하는 작업은 미리

dlemrcnd.tistory.com

이전 포스팅과 이어짐.

 

이전 포스팅에서 NodeProcess() 함수로 N트리 구조의 FBX 트리를 재귀 함수로 돌아서

매쉬 타입 오브젝트를 저장해, ParseMesh() 함수로 그 매쉬의 정보를 가져오는 작업을 했다.

 

NodeProcess()

void KFbxLoader::NodeProcess(KFBXObj* pParentObj, FbxNode* pNode)
{
	KFBXObj* fbx = nullptr;
	if (pNode!=nullptr)
	{
		fbx = new KFBXObj;
		fbx->m_pFbx_ThisNode = pNode;
		fbx->m_pFbx_ParentNode = pNode->GetParent();
		fbx->m_pFbx_ParentObj = pParentObj;
		fbx->m_iIndex = m_FBXTreeList.size();
		fbx->m_ObjName = to_mw(pNode->GetName());
		m_FBXTreeList.push_back(fbx); // obj 검색 데이터를 넣을때,
		m_pFbxNodeMap.insert(std::make_pair(pNode, fbx->m_iIndex));
		m_pFbxObjMap.insert(std::make_pair(fbx->m_ObjName, fbx));
		//이름으로 바로 인덱스 접근할 수 있으면 끝.
	}
	// 카메라나 라이트, 헬퍼 오브젝트는 메시타입이 아님
	FbxMesh* pMesh = pNode->GetMesh();
	if (pMesh)
	{
		//매쉬 타입만 라이트, 카메라 제외
		m_MeshList.push_back(fbx);
	}
	int iNumChild = pNode->GetChildCount();
	for (int iNode = 0; iNode < iNumChild; iNode++)
	{
		FbxNode* child = pNode->GetChild(iNode);
		NodeProcess(fbx, child);
	}
}

 

위 m_MeshList에 오브젝트 KFBXObj 객체를 저장한다.

이제 m_MeshList에서 하나 씩 빼내서 해석하면 된다. 

 

ParseMesh()

더보기
void KFbxLoader::ParseMesh(KFBXObj* pObject)
{
	//현재 노드의 매쉬를 만듬, 버텍스 PNCT를 채워줘야함
	FbxMesh* pFbxMesh = pObject->m_pFbx_ThisNode->GetMesh();

	pObject->m_bSkinned = ParseMeshSkinning(pFbxMesh, pObject);
	if (pFbxMesh)
	{
		//기하 행렬(FBX 위치 버텍스에서 -> 초기 정점 로컬 위치로 변환)
		FbxAMatrix  mat_Geo;
		FbxVector4	t = pObject->m_pFbx_ThisNode->GetGeometricTranslation(FbxNode::eSourcePivot);
		FbxVector4	r = pObject->m_pFbx_ThisNode->GetGeometricRotation(FbxNode::eSourcePivot);
		FbxVector4	s = pObject->m_pFbx_ThisNode->GetGeometricScaling(FbxNode::eSourcePivot);

		mat_Geo.SetT(t);
		mat_Geo.SetR(r);
		mat_Geo.SetS(s);

		//노말 행렬, 기하행렬의 역행렬의 전치
		//노말 매트릭스
		FbxAMatrix normalMatrix = mat_Geo;
		normalMatrix = normalMatrix.Inverse();
		normalMatrix = normalMatrix.Transpose();


		std::vector<FbxLayerElementMaterial*>	 MaterialSet; // 매터리얼
		std::vector<FbxLayerElementUV*>			 UVSet; // UV
		std::vector<FbxLayerElementVertexColor*> VertexColorSet;//

		//// 노말맵을 위한 노말, 바이노말, 탄젠트
		std::vector<FbxLayerElementNormal*>		 NormalSet;  
		std::vector<FbxLayerElementBinormal*>    BinormalSet;
		std::vector<FbxLayerElementTangent*>     TangentSet; 

		int iLayerCount = pFbxMesh->GetLayerCount(); // 레이어 ( 1번에 랜더링, 여러번에 걸쳐서 랜더링 개념)

		for (int iLayer = 0; iLayer < iLayerCount; iLayer++)
		{
			FbxLayer* pFbxLayer = pFbxMesh->GetLayer(iLayer);// 레이어에 UV 정보가 있음 필수적임

			//매터리얼, UV, 버텍스 컬러
			if (pFbxLayer->GetMaterials() != nullptr)
			{
				MaterialSet.push_back(pFbxLayer->GetMaterials());
			}
			if (pFbxLayer->GetUVs() != nullptr)
			{
				UVSet.push_back(pFbxLayer->GetUVs());
			}
			if (pFbxLayer->GetVertexColors() != nullptr)
			{
				VertexColorSet.push_back(pFbxLayer->GetVertexColors());
			}
			//노말값
			if (pFbxLayer->GetNormals() != nullptr)
			{
				NormalSet.push_back(pFbxLayer->GetNormals());
			}

			if (pFbxLayer->GetBinormals() != nullptr) 
			{
				BinormalSet.push_back(pFbxLayer->GetBinormals());
			}
			if (pFbxLayer->GetTangents() != nullptr)
			{
				TangentSet.push_back(pFbxLayer->GetTangents());
			}
		}

		//매터리얼 개수 만큼 돌면서 읽어옴 
		//현재는 저장된 텍스쳐의 이름만 가져옴
		int iNumMtrl = pObject->m_pFbx_ThisNode->GetMaterialCount();
		for (int iMtrl = 0; iMtrl < iNumMtrl; iMtrl++)
		{
			FbxSurfaceMaterial* pSurface = pObject->m_pFbx_ThisNode->GetMaterial(iMtrl);
			if (pSurface)
			{
				//메터리얼의 텍스쳐 이름을 가져와서 리스트 추가 및 SRV 생성
				std::wstring strFbxPath = L"../../data/model/";
				std::wstring strFBXTexName = to_mw(ParseMaterial(pSurface));
				std::wstring strTexDefault1 = L"../../data/model/Default_Diffuse.jpg";
				std::wstring strTexDefault2 = L"../../data/model/Default_Specular.jpg";
				std::wstring strTexDefault3 = L"../../data/model/T_Pack_01_N.jpg";
				if (!strFBXTexName.empty())
				{
					strFbxPath += strFBXTexName;
					//자동으로 텍스쳐를 만드는데, 실패할 경우
					KTexture* pTex1 = g_TextureMananger.Load(strFbxPath);
					KTexture* pTex2 = g_TextureMananger.Load(strTexDefault2);
					KTexture* pTex3 = g_TextureMananger.Load(strTexDefault3);
					if (pTex1 != nullptr)
					{
						pObject->m_pTexture_Diffuse = pTex1;
					}
					else
					{
						KTexture* pNoDir = g_TextureMananger.Load(strTexDefault1);
						pObject->m_pTexture_Diffuse = pNoDir;
					}
					
					pObject->m_pTexture_Specular = pTex2;
					pObject->m_pTexture_Normal = pTex3;
				}
			}
		}
		//개수가 1보다 많다면 매터리얼 수만큼 배열 할당해주고 
		// 그외에는 무조건 하나로 할당해준다.
		if (iNumMtrl > 0)
		{
			pObject->m_pSubBTList.resize(iNumMtrl);
			pObject->m_pSubVertexList.resize(iNumMtrl);
			pObject->m_pSubIWVertexList.resize(iNumMtrl);
		}
		else
		{
			pObject->m_pSubBTList.resize(1);
			pObject->m_pSubVertexList.resize(1);
			pObject->m_pSubIWVertexList.resize(1);
		}
		//----------------------------------------------------------
		// 폴리곤, 면 개수 만큼 돌면서 위치를 저장
		// 삼각형, 사각형
		int iCurpolyIndex = 0; // 증가되는 폴리곤 인덱스
		int iNumPolyCount = pFbxMesh->GetPolygonCount(); //폴리곤 수
		FbxVector4* pVertexPositions = pFbxMesh->GetControlPoints(); //정점 위치 
		int iNumFace = 0;
		for (int iPoly = 0; iPoly < iNumPolyCount; iPoly++)
		{
			int iPolySize = pFbxMesh->GetPolygonSize(iPoly); //4또는 3 삼각형이나 사각형이냐
			iNumFace = iPolySize - 2; // 한면 구하는 계산

			int iSubMtrl = 0;
			//서브 매터리얼 
			if (iNumMtrl >= 1 && MaterialSet[0] != nullptr)
			{
				iSubMtrl = GetSubMaterialIndex(iPoly, MaterialSet[0]);
			}
			//면 4 - 2는 2개의 트라이앵글
			for (int iFace = 0; iFace < iNumFace; iFace++)
			{
				int VertexIndex[3] = { 0, iFace + 2, iFace + 1 };
				int CornerIndex[3];
				CornerIndex[0] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[0]);
				CornerIndex[1] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[1]);
				CornerIndex[2] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[2]);
				for (int iIndex = 0; iIndex < 3; iIndex++)
				{
					PNCT_VERTEX pnct_vertex;
					BT_VERTEX bt_vertex;
					// Max(x,z,y) ->(dx)x,y,z    
					FbxVector4 v = pVertexPositions[CornerIndex[iIndex]];
					v = mat_Geo.MultT(v); // 로컬 좌표로 행렬 곱 
					pnct_vertex.pos.x = v.mData[0];
					pnct_vertex.pos.y = v.mData[2];
					pnct_vertex.pos.z = v.mData[1];

					// UV
					int u[3];
					u[0] = pFbxMesh->GetTextureUVIndex(iPoly, VertexIndex[0]);
					u[1] = pFbxMesh->GetTextureUVIndex(iPoly, VertexIndex[1]);
					u[2] = pFbxMesh->GetTextureUVIndex(iPoly, VertexIndex[2]);
					//UV 리스트에 값이 있다면
					if (UVSet.size() > 0)
					{
						FbxLayerElementUV* pUVSet = UVSet[0];
						FbxVector2 uv;
						ReadTextureCoord(
							pFbxMesh,
							pUVSet,
							CornerIndex[iIndex],
							u[iIndex],
							uv);
						pnct_vertex.tex.x = uv.mData[0];
						pnct_vertex.tex.y = 1.0f - uv.mData[1];
					}
					//----------------------------------------------------------
					//버텍스 컬러 값이 있다면
					FbxColor color = FbxColor(1, 1, 1, 1);
					if (VertexColorSet.size() > 0)
					{
						color = ReadColor(pFbxMesh,
							VertexColorSet.size(),
							VertexColorSet[0],
							CornerIndex[iIndex],
							iCurpolyIndex + VertexIndex[iIndex]);
					}
					pnct_vertex.color.x = color.mRed;
					pnct_vertex.color.y = color.mGreen;
					pnct_vertex.color.z = color.mBlue;
					pnct_vertex.color.w = pObject->m_iIndex; //버텍스 컬러 값에 인덱스 저장
					//----------------------------------------------------------
					//노말값이 있다면
					if (NormalSet.size() > 0)
					{
						FbxVector4 normal = ReadNormal(pFbxMesh,
							CornerIndex[iIndex],
							iCurpolyIndex + VertexIndex[iIndex]);
						normal = normalMatrix.MultT(normal);
						pnct_vertex.normal.x = normal.mData[0]; // x
						pnct_vertex.normal.y = normal.mData[2]; // z
						pnct_vertex.normal.z = normal.mData[1]; // y
					}

					if (BinormalSet.size() > 0)
					{
						FbxVector4 binormal = ReadBinormal(pFbxMesh,
							CornerIndex[iIndex],
							iCurpolyIndex + VertexIndex[iIndex]);
						binormal = normalMatrix.MultT(binormal);
						bt_vertex.binormal.x = binormal.mData[0]; // x
						bt_vertex.binormal.y = binormal.mData[2]; // z
						bt_vertex.binormal.z = binormal.mData[1]; // y
					}

					if (TangentSet.size() > 0)
					{
						FbxVector4 tangent = ReadTangent(pFbxMesh,
							CornerIndex[iIndex],
							iCurpolyIndex + VertexIndex[iIndex]);
						tangent = normalMatrix.MultT(tangent);
						bt_vertex.tangent.x = tangent.mData[0]; // x
						bt_vertex.tangent.y = tangent.mData[2]; // z
						bt_vertex.tangent.z = tangent.mData[1]; // y
					}
					//애니메이션을 위한 가중치
					//캐릭터 애니메이션이 아닌 오브젝트도 스키닝화 시킨다.
					IW_VERTEX iwVertex;
					if (pObject->m_bSkinned) //캐릭터일 경우
					{
						KWeight* weight = &pObject->m_WeightList[CornerIndex[iIndex]];
						for (int i = 0; i < 4; i++)
						{
							iwVertex.i[i] = weight->Index[i];
							iwVertex.w[i] = weight->Weight[i];
						}
					}
					else//오브젝트 애니메이션인 경우
					{
						// 일반오브젝트 에니메이션을 스키닝 케릭터 화 작업.
						iwVertex.i[0] = pObject->m_iIndex;
						iwVertex.w[0] = 1.0f;
					}
					pObject->m_pSubVertexList[iSubMtrl].push_back(pnct_vertex);
					pObject->m_pSubIWVertexList[iSubMtrl].push_back(iwVertex);
					pObject->m_pSubBTList[iSubMtrl].push_back(bt_vertex);
				}
			}
			iCurpolyIndex += iPolySize;
		}

	}
}

 

오브젝트의 버텍스 위치, 버텍스 컬리, 매터리얼, UV값, 법선값, 접선 값, 종법선 값 등

이 한 함수에서 정보를 빼내 온다. 

 

여기서 한 가지 문제점이 있다. FBX 파일에 따라서, 내 경우에는 대부분의 파일이

탄젠트 공간의 정보가 빠져있었다. 

 

FBX를 Export 할 때, Tangent 공간을 같이 익스포트 하지 않는 이상,

GetBinormal, GetTangents

Binormal(종법선), Tangent(접선) 정보가 저장돼있지 않기 때문에 FBX 매쉬에

정보를 꺼내올 수 없었던 것이다.

Blender Export 창

 

위는 Blender에서 Tangent Space를 같이 익스포트 하는 방법이다.

하지만, 모든 파일을 내가 직접 제작할 거면 굳이 FBX 파일을 사용하는

이유가 없기 때문에, 

 

트라이앵글 3점을 이용해서 탄젠트 공간을 구하는 함수를 구현했었다.

아래는 그 함수의 내용이 있는 포스팅이다.

 

https://dlemrcnd.tistory.com/80?category=525778 

 

DirectX11 - 법선매핑 (Normal Mapping) 접선 공간, 접선 (Tangent), 종법선 (Binormal)

이번에는 법선매핑, 노말 맵을 다이렉트x에 적용한다. 노말 맵은 픽셀에 사용할 법선 정보를 담고 있다. 노말 매핑을 사용하면 버텍스의 노말(법선) 정보가 아닌 텍스쳐의 노말(법선)정보를 사용

dlemrcnd.tistory.com

 

하지만 이 방법은 비효율적이라고 할 수 있다.

왜냐하면 FBX SDK에는 이미 구현되어 있다..!

 

int iLayerCount = pFbxMesh->GetLayerCount();
// 레이어 ( 1번에 랜더링, 여러번에 걸쳐서 랜더링 개념)

if (iLayerCount == 0 || pFbxMesh->GetLayer(0)->GetNormals() == nullptr)
{
	pFbxMesh->InitNormals();
	pFbxMesh->GenerateNormals();
}
//노말 탄젠트 바이노말 없을때 생성해준다.
if (iLayerCount == 0 || pFbxMesh->GetLayer(0)->GetTangents() == nullptr || 
	pFbxMesh->GetLayer(0)->GetBinormals() == nullptr)
{
	pFbxMesh->InitTangents();
	pFbxMesh->InitBinormals();
	pFbxMesh->CreateElementBinormal();
	pFbxMesh->GenerateTangentsData(0, true, false);
}

 

레이어 단위로 정보를 얻기 전에

GetNormal(), GetTangent(), GetBinomal을 호출한다.

nullptr, 즉, 결과 없을 경우에 정보가 없는 경우이기 때문에,

새로이 노말, 탄젠트, 바이 노말을 생성한다. 

InitNormals(), InitTangents(), InitBinormals()은 정점 개수만큼

배열의 메모리를 할당하는 함수이다. 

 

GenerateNormals() 함수로 혹시 없을 노말 값을 생성하고, 

GenerateTagentsData()로 탄젠트, 바이 노말을 값을 생성한다.

bool GenerateTangentsData(int pUVSetLayerIndex,
		bool pOverwrite=false,
		bool pIgnoreTangentFlip = false);

pOverwrite에 true값을 준 것은, init함수로 할당될때, 0000값이 들어가서

덮어쓰겠다는 의미이다. 

 

결과

노말맵 적용

정점에 탄젠트 공간을 만들어 노말 맵이 잘 적용된 모습이다.

이제 어떤 파일이든 노말값, 탄젠트 공간 정보가 없어도

생성해서 쉐이더에 잘 적용할 수 있다.  

 

COMMENT