Directx11教程四十六之FBX SDK

在之前的DX11入门系列文章中,有篇有关 Directx11教程四十之加载OBJ模型 读取obj模型数据的博客。不过在obj读取的那篇博客我有些坑并没有说,就是我写的那个obj解析器只能解析特定的obj文件格式,因为后面我用我写的obj模型解析器发现根本无法解析大多数的obj文件格式,真是让人崩溃,因为那时的我是分析特定的一两个obj文件写出来的解析器,实际上obj文件数据的多样性超越我的想象。所以我不推荐大家在使用obj了,请远离obj。 obj能直接查看文本数据,但并不好用。我们紧跟商业引擎的步伐,用FBX格式。


惯例,放出本篇博客有关程序对应的结构:



关于商业引擎与FBX SDK

我们知道UE4,U3D导入模型是FBX文件,也就是说FBX文件是UE4,U3D的中间模型文件,为什么特意加个“中间”呢?那是因为FBX模型文件仅仅是UE4,U3D的导入模型文件,并不是UE4,U3D游戏运行时加载的模型文件。像UE4,U3D这样的引擎导入FBX文件会生成自己的自定义模型文件。这是为什么呢?用过FBX SDK读取模型数据你就会发现使用 FBX SDK 读取FBX模型文件的直接得到的几何数据(例如顶点数据)存在一定(有时候甚至可以说是严重的)的冗余,并且读取的速度也不够快,因此FBX文件只是商业引擎的中间模型文件。当然我们是DX11教程做案例,没必要搞那么复杂,直接用FBX SDK读取FBX文件的数据作为渲染数据来使用。


FBX SDK的环境配置。

我用的是VS2017和 FBX  SDK2017.1,

FBX SDK2017.1下载地址:http://usa.autodesk.com/adsk/servlet/pc/item?siteID=123112&id=26012646

相应的开发文档:http://help.autodesk.com/view/FBX/2017/ENU/,可以跟着文档来配置环境

因为用的是VS2017,相应的FBX SDK2017也只给出了VS2015,VS2013的编译版本,环境配置方案有三种,我三种都试过了,发现VS2017可以使用DLL配置的那种。

FBX 文件的读取

关于FBX数据的读取 这里我就不献丑了,直接引用前人写的博客就行了。下面三篇有关FBX读取数据的博客强烈推荐:

1.基于FBX SDK的FBX模型解析与加载 -(一)

2.基于FBX SDK的FBX模型解析与加载 -(二)

3.Working with FBX SDK (2)

4.解析 FBX 模型文件作为 Direct3D 的渲染模型

跟着这三篇博客基本上能理解基本的FBX SDK读取数据的概念。

这里唯一要纠结的是,材质的读取接口已经发生改变了,我们因爲需要在DX11读取文件,就得获取纹理文件的相对路径(不推荐用绝对路径),采用的接口是FBXFileTexture而并非FBXTexture,如下所示:

void ImportFBX::ReadReletiveTextureFileName(FbxProperty* mproperty,
	int materialIndex, map<int, Material>& materialMap)
{
	if (!mproperty || !mproperty->IsValid())
	{
		return;
	}
	string name = mproperty->GetName();
	bool isNeedTexture = false;
	isNeedTexture = (name == FbxSurfaceMaterial::sDiffuse) || (name == FbxSurfaceMaterial::sSpecular)
		|| (name == FbxSurfaceMaterial::sTransparentColor) || (name == FbxSurfaceMaterial::sBump);
	if (!isNeedTexture)
	{
		return;
	}

	int textureNum = mproperty->GetSrcObjectCount<FbxFileTexture>();
	//现在每种纹理仅仅读取一个
	if (textureNum > 0)
	{
		FbxFileTexture* fbxFileTexture = mproperty->GetSrcObject<FbxFileTexture>(0);
		string  relativeFileName = fbxFileTexture->GetRelativeFileName();
		size_t tgaTagPos = relativeFileName.find(".tga");
		if (tgaTagPos != string::npos)
		{
			relativeFileName = relativeFileName.substr(0, tgaTagPos);
			relativeFileName += string(".jpg");
		}
		string fileName = fbxFileNamePre + relativeFileName;
		if (name == FbxSurfaceMaterial::sDiffuse)
		{
			materialMap[materialIndex].diffuseMapFileName = fileName;
		}
		else if (name == FbxSurfaceMaterial::sSpecularFactor)
		{
			materialMap[materialIndex].specularMapFileName = fileName;
		}
		else if (name == FbxSurfaceMaterial::sTransparentColor)
		{
			materialMap[materialIndex].alphaMapFileName = fileName;
		}
		else if (name == FbxSurfaceMaterial::sBump)
		{
			materialMap[materialIndex].bumpMapFileName = fileName;
		}

	}

}

好吧,这里我只读取diffuseTexture,NormalTexture,SpecularTexture,AlphaTexture,并且DXUT由于无法加载.tga格式的纹理,我转为.tga文件后缀名为.jpg,并且将相应的FBX文件的所有.tga图片在PS里转为了.jpg文件。


读取FBX的数据结构:

我们知道FBX的节点是以树的形状来组织的,因此用树来组织是再好不过的,当然我为了教程的简易,只是用了数组来组织。

1.顶点结构如下,没多少好说的

struct VertexPCNTT
{
	XMFLOAT3 pos;
	XMFLOAT3 color;
	XMFLOAT3 normal;
	XMFLOAT3 tangent;
	XMFLOAT2 uv;
};


2. 三角形结构,因为在fbx的fbxNode节点中,读取的每个三角形都有一个材质id,索引到相应的材质,相应的材质蕴含相应的相应各种属性(Diffuse,Specular,以及各种纹理)  。 这里得注意一点,材质id是绑定于相应的节点的,比如说fbxNode1和fbxNode2的同一个材质id(例如都为0或者1之类的)是完全不存在关系的,材质id仅仅针对于绑定的节点。

struct Triangle
{
	VertexPCNTT vertexs[3];
	int MaterialId;
};


3. 材质结构,这里我们的材质直接用纹理文件名来表示,当读取到相应的纹理文件名为空时,就代表不存在相应纹理,反之则存在相应纹理。

struct Material
{
	string diffuseMapFileName;
	string specularMapFileName;
	string alphaMapFileName;
	string bumpMapFileName;
};

4. mesh结构,我们从前面的四篇博客可以知道,一个mesh的节点读取的所有三角形存在可能不只一个材质id,而每个材质id是绑定于相应的fbxNode的,因为一个mesh类型的fbxNode或者说fbxMesh存在多少个材质id,则就存在多少个mesh.(这里不可能一个三角形就是一个mesh,drawCall得吓死人,所以得根据在读取fbxMesh数据的时候,把材质id相同的三角形归为同一个mesh)。总体来说 一个fbxMesh存在多少个材质id,就生成多少个mesh,也就是一个mesh存在一种材质id

struct Mesh
{
	vector<VertexPCNTT> mVertexData;
	vector<WORD> mIndexData;
	int materialId;
	ID3D11Buffer* mVertexBuffer;
	ID3D11Buffer* mIndexBuffer;
};

5.model结构, 刚才我们说过一个mesh的fbxNode或者说fbxMesh可以解析出多个mesh,则一个fbxNode的所有mesh解析成了model.不过得注意我在model结构添加了材质哈希表,mesh可以通过自身的材质id,在model结构找到相应的材质(相应的纹理相对路径。

struct Model
{
	vector<Mesh> mMeshList;
	map<int, Material> mMaterialMap;
};


6.FBXModel结构,我们上面说model为一个节点解析出来的结构,而一个fbx文件如果含有n多个分fbxNode(fbxMesh),也就是生成了n多个Model结构体,我解析其为FBXModel.注意我用了一个哈希表来查询相应的 ID3D11ShaderResourceView* 资源,这个mSRVMap的键为纹理的相对路径,可能看到这你可豁然开朗。

//保证了一个FBX加载的所有纹理文件都仅仅加载一次
struct FBXModel
{
	vector<Model> mModelList;
	map<string, ID3D11ShaderResourceView*> mSRVMap;
};

这里思路很明确,在渲染的时候,我们是以mesh为个体,一个一个进行渲染的,思路过程:

(1) 设置mesh的顶点缓存和索引缓存

  (2) 根据 mesh 的材质MaterialId在相应的 model 材质名哈希表 找到相应的Material,最后用Material对应的四种纹理相应路径名在FBXModel 材质哈希表找到相应的ID3D11ShaderResourceView* 资源,然后用相应的Shader进行渲染,好吧,一切已然豁然开朗。


FBX数据读取的坐标系空间纠正:

因为我们是在3DS MAX建模的,而我们3DS MAX的坐标系空间如下:


3DS MAX的坐标系空间是Z轴向上的右手坐标系,而D3D11的是Y轴向上的左手坐标系。因此如果你直接拿FBX读取的数据渲染,会发现模型 往往是躺着的。我采用了绕X轴渲染 -90度 的办法,对读取的顶点数据做如下变换:

1.位置(XMMatrixRotationX(-XM_PI/2.0)),

//由于3DS MAX里坐标轴为Z轴向上的,Y轴向里的右手坐标系, D3D11为左手坐标系
//参考https://www.cnblogs.com/wantnon/p/4372764.html
void ImportFBX::ReadVertexPos(FbxMesh* mesh, int ctrlPointIndex, XMFLOAT3* pos)
{
	FbxNode* meshNode = mesh->GetNode();
	FbxAnimEvaluator* lEvaluator = mScene->GetAnimationEvaluator();
	FbxMatrix lGlobal;

	lGlobal.SetIdentity();
	lGlobal = lEvaluator->GetNodeGlobalTransform(meshNode);
	FbxDouble3 scaling = meshNode->LclScaling.Get();
	FbxVector4 * ctrPoints = mesh->GetControlPoints();
	pos->x = ctrPoints[ctrlPointIndex][0] * scaling[0];
	pos->y = ctrPoints[ctrlPointIndex][2] * scaling[2];
	pos->z = -ctrPoints[ctrlPointIndex][1] * scaling[1];


}

2.顶点法线(顶点位置变换的逆反矩阵变换)

void ImportFBX::ReadVertexNormal(FbxMesh* mesh, int ctrlPointIndex, int vertexCount, XMFLOAT3* normal)
{
	if (mesh->GetElementNormalCount() < 1)
	{
		return;
	}

	FbxGeometryElementNormal* vertexNormal = mesh->GetElementNormal(0);

	switch (vertexNormal->GetMappingMode())
	{
	case FbxGeometryElement::eByControlPoint:
	{
		switch (vertexNormal->GetReferenceMode())
		{
		case FbxGeometryElement::eDirect:
		{
			normal->x = vertexNormal->GetDirectArray().GetAt(ctrlPointIndex).mData[0];
			normal->y = vertexNormal->GetDirectArray().GetAt(ctrlPointIndex).mData[2];
			normal->z = -vertexNormal->GetDirectArray().GetAt(ctrlPointIndex).mData[1];
		}
		break;
		case FbxGeometryElement::eIndexToDirect:
		{
			int id = vertexNormal->GetIndexArray().GetAt(ctrlPointIndex);
			normal->x = vertexNormal->GetDirectArray().GetAt(id).mData[0];
			normal->y = vertexNormal->GetDirectArray().GetAt(id).mData[2];
			normal->z = -vertexNormal->GetDirectArray().GetAt(id).mData[1];
		}
		break;

		default:
			break;
		}
	}
	break;

	case FbxGeometryElement::eByPolygonVertex:
	{
		switch (vertexNormal->GetReferenceMode())
		{
		case FbxGeometryElement::eDirect:
		{
			normal->x = vertexNormal->GetDirectArray().GetAt(vertexCount).mData[0];
			normal->y = vertexNormal->GetDirectArray().GetAt(vertexCount).mData[2];
			normal->z = -vertexNormal->GetDirectArray().GetAt(vertexCount).mData[1];
		}
		break;

		case FbxGeometryElement::eIndexToDirect:
		{
			int id = vertexNormal->GetIndexArray().GetAt(vertexCount);
			normal->x = vertexNormal->GetDirectArray().GetAt(id).mData[0];
			normal->y = vertexNormal->GetDirectArray().GetAt(id).mData[2];
			normal->z = -vertexNormal->GetDirectArray().GetAt(id).mData[1];
		}
		break;

		default:
			break;
		}
		break;
	}
	}
}

3.顶点切线(跟顶点位置变换一致)

void ImportFBX::ReadVertexTangent(FbxMesh* mesh, int ctrlPointIndex, int vertexCount, XMFLOAT3* tangent)
{
	if (mesh->GetElementTangentCount() < 1)
	{
		return;
	}

	FbxGeometryElementTangent* vertexTangent = mesh->GetElementTangent(0);

	switch (vertexTangent->GetMappingMode())
	{
	case FbxGeometryElement::eByControlPoint:
	{
		switch (vertexTangent->GetReferenceMode())
		{
		case FbxGeometryElement::eDirect:
		{
			tangent->x = vertexTangent->GetDirectArray().GetAt(ctrlPointIndex).mData[0];
			tangent->y = vertexTangent->GetDirectArray().GetAt(ctrlPointIndex).mData[2];
			tangent->z = -vertexTangent->GetDirectArray().GetAt(ctrlPointIndex).mData[1];
		}
		break;
		case FbxGeometryElement::eIndexToDirect:
		{
			int id = vertexTangent->GetIndexArray().GetAt(ctrlPointIndex);
			tangent->x = vertexTangent->GetDirectArray().GetAt(id).mData[0];
			tangent->y = vertexTangent->GetDirectArray().GetAt(id).mData[2];
			tangent->z = -vertexTangent->GetDirectArray().GetAt(id).mData[1];
		}
		break;

		default:
			break;
		}
	}
	break;

	case FbxGeometryElement::eByPolygonVertex:
	{
		switch (vertexTangent->GetReferenceMode())
		{
		case FbxGeometryElement::eDirect:
		{
			tangent->x = vertexTangent->GetDirectArray().GetAt(vertexCount).mData[0];
			tangent->y = vertexTangent->GetDirectArray().GetAt(vertexCount).mData[2];
			tangent->z = -vertexTangent->GetDirectArray().GetAt(vertexCount).mData[1];
		}
		break;

		case FbxGeometryElement::eIndexToDirect:
		{
			int id = vertexTangent->GetIndexArray().GetAt(vertexCount);
			tangent->x = vertexTangent->GetDirectArray().GetAt(id).mData[0];
			tangent->y = vertexTangent->GetDirectArray().GetAt(id).mData[2];
			tangent->z = -vertexTangent->GetDirectArray().GetAt(id).mData[1];
		}
		break;

		default:
			break;
		}
		break;
	}
	}
}

4.顶点UV,u2 = u1, v2 = 1.0 - v1,则就是U不变,而v反过来(DX11与OPenGL相反)。

void ImportFBX::ReadVertexUV(FbxMesh* mesh, int ctrlPointIndex, int uvIndex, XMFLOAT2* uv)
{
	if (mesh->GetElementUVCount() < 1)
	{
		return;
	}

	FbxGeometryElementUV* vertexUV = mesh->GetElementUV(0);

	switch (vertexUV->GetMappingMode())
	{
	case FbxGeometryElement::eByControlPoint:
	{
		switch (vertexUV->GetReferenceMode())
		{
		case FbxGeometryElement::eDirect:
		{
			//因为这些这些数据个Opengl坐标一样,而我们是需要在D3D11里渲染的数据,所以部分数据得改变
			//v反转
			uv->x = vertexUV->GetDirectArray().GetAt(ctrlPointIndex).mData[0];
			uv->y = 1.0f - vertexUV->GetDirectArray().GetAt(ctrlPointIndex).mData[1];

		}
		break;
		case FbxGeometryElement::eIndexToDirect:
		{
			int id = vertexUV->GetIndexArray().GetAt(ctrlPointIndex);
			uv->x = vertexUV->GetDirectArray().GetAt(id).mData[0];
			uv->y = 1.0f - vertexUV->GetDirectArray().GetAt(id).mData[1];
		}
		break;

		default:
			break;
		}
	}
	break;

	case FbxGeometryElement::eByPolygonVertex:
	{
		switch (vertexUV->GetReferenceMode())
		{
		case FbxGeometryElement::eDirect:
		case FbxGeometryElement::eIndexToDirect:
		{
			uv->x = vertexUV->GetDirectArray().GetAt(uvIndex).mData[0];
			uv->y = 1.0f - vertexUV->GetDirectArray().GetAt(uvIndex).mData[1];
		}
		break;

		default:
			break;
		}
		break;
	}
	}
}

最终渲染结果:







参考资料:

【1】Working with FBX SDK (2)

【2】基于FBX SDK的FBX模型解析与加载 -(一)

【3】基于FBX SDK的FBX模型解析与加载 -(二)

【4】FBX 2017.1开发者文档


源码链接:

https://download.csdn.net/download/qq_29523119/10351468

未来待提高

【1】命名有问题,不过懒得改了,Mesh应该改为SubMesh, Model改为Mesh,FBXModel改为Model

【2】FBXModel管理Model更应该用树结构,而非vector数组。

【3】纹理哈希表不应该与FBXModel绑定在一起,因为加载的多个FBX模型很有可能存在相同的加载纹理或者说是相同的纹理相对路径名。更应该建立一个全局的纹理管理类,避免纹理加载资源的重复性。

【4】顶点数据存在大量的冗余,可以自定义文件来存储模型数据,FBX还是作为导入模型比较合适,不适合作为游戏运行时的加载模型文件。

猜你喜欢

转载自blog.csdn.net/qq_29523119/article/details/79955690