在之前的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/?guid=__files_GUID_6E7B1A2E_584C_49C6_999E_CD8367841B7C_htm 可以跟着文档来配置环境
因为用的是VS2017,相应的FBX SDK2017也只给出了VS2015,VS2013的编译版本,环境配置方案有三种,我三种都试过了,发现VS2017可以使用DLL配置的那种。
FBX 文件的读取
关于FBX数据的读取 这里我就不献丑了,直接引用前人写的博客就行了。下面三篇有关FBX读取数据的博客强烈推荐:
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;
}
}
}
最终渲染结果:
参考资料:
源码链接:
集成了FBX SDK的3D渲染引擎源码:
https://github.com/2047241149/SDEngine
未来待提高
【1】命名有问题,不过懒得改了,Mesh应该改为SubMesh, Model改为Mesh,FBXModel改为Model
【2】FBXModel管理Model更应该用树结构,而非vector数组。
【3】纹理哈希表不应该与FBXModel绑定在一起,因为加载的多个FBX模型很有可能存在相同的加载纹理或者说是相同的纹理相对路径名。更应该建立一个全局的纹理管理类,避免纹理加载资源的重复性。
【4】顶点数据存在大量的冗余,可以自定义文件来存储模型数据,FBX还是作为导入模型比较合适,不适合作为游戏运行时的加载模型文件。