用FBX_SDK读取网格数据
1.前言
前一篇讲了如何配置FBX_SDK,这一篇来看如何读取模型的网格数据,效果如下:
2.导出
首先随便创建一个场景,然后导出:
在导出的时候勾选上三角算法,将网格全部以三角形面的形式存储,这样方便DirectX的API使用:
有些模型在导出三角面时会失败,我在3ds max里转化为可编辑多边形后就导出成功了。
当然也可以在代码中执行转换的操作,不过编辑器里都会转换失败,代码就也很可能转换失败,所以我还是在编辑器里导出时就转换好。
3.具体代码
DX需要的是顶点缓存和索引缓存,所以先定义一个类来缓存数据,如果以后有需要也可以方便实现自己的3D模型中转文件,这样不仅加快读取速度,并且可以避免版权问题。
所以如下,先用两个vector保存顶点和索引:
class MeshObject
{
public:
UINT64 GetVertexDataSize()
{
return _vecVertex.size() * sizeof(DirectX::Vertex);
}
UINT8* GetVertexData()
{
return (UINT8*)(_vecVertex.data());
}
UINT64 GetIndexDataSize()
{
return _vecIndex.size() * sizeof(UINT32);
}
UINT8* GetIndexData()
{
return (UINT8*)(_vecIndex.data());
}
vector<Vertex> _vecVertex;
vector<UINT32> _vecIndex;
};
先初始化FBX_SDK,设置iosetting,创建一个importer:
// Initialize the SDK manager. This object handles all our memory management.
FbxManager* _lSdkManager = FbxManager::Create();
// Create the IO settings object.
FbxIOSettings* ios = FbxIOSettings::Create(_lSdkManager, IOSROOT);
_lSdkManager->SetIOSettings(ios);
// Create an importer using the SDK manager.
FbxImporter* lImporter = FbxImporter::Create(_lSdkManager, "");
然后用importer加载文件:
if (!lImporter->Initialize(lFilename, -1, _lSdkManager->GetIOSettings()))
{
//error
}
接着用importer创建一个scene,然后释放importer,遍历scene的根节点,完成后再释放内存:
// Create a new scene so that it can be populated by the imported file.
FbxScene* lScene = FbxScene::Create(_lSdkManager, "myScene");
// Import the contents of the file into the scene.
lImporter->Import(lScene);
// The file is imported; so get rid of the importer.
lImporter->Destroy();
// Print the nodes of the scene and their attributes recursively.
// Note that we are not printing the root node because it should
// not contain any attributes.
MeshObject* ret = nullptr;
FbxNode* lRootNode = lScene->GetRootNode();
if (lRootNode) {
ret = _init_mesh_object(lRootNode);
/*for (int i = 0; i < lRootNode->GetChildCount(); i++)
PrintNode(lRootNode->GetChild(i));*/
}
// Destroy the SDK manager and all the other objects it was handling.
_lSdkManager->Destroy();
在_init_mesh_object函数,我们传入根节点,然后读取了网格数据。从root_node遍历有可能得到多个Mesh,下面的代码处理了一下,将多个Mesh放到了一起,当然也可以分开放:
DND::MeshObject* DNDFBX::_init_mesh_object(FbxNode* root_node)
{
if (!root_node)
return nullptr;
MeshObject* ret = new MeshObject;
int last_index = 0;//上一个模型的最大索引
for (int i = 0; i < root_node->GetChildCount(); i++)
{
FbxNode* p_node = root_node->GetChild(i);
for (int j = 0; j < p_node->GetNodeAttributeCount(); j++)
{
FbxNodeAttribute* p_attribute = p_node->GetNodeAttributeByIndex(j);
if (p_attribute->GetAttributeType() == FbxNodeAttribute::eMesh)
{
FbxMesh* mesh = p_attribute->GetNode()->GetMesh();
if (mesh == NULL)
{
return nullptr;
}
//数据是以形状存储的,会有重复的顶点,所以需要计算索引缓存
int count_polygon = mesh->GetPolygonCount();
//g_debug.Line(to_wstring(count_polygon));
ret->_vecVertex.resize(last_index + count_polygon * 3);
//已初始化标记
vector<bool> vec_inited;
vec_inited.resize(count_polygon * 3, false);
int max_index = 0;
for (int k = 0; k != count_polygon; ++k)
{
if (mesh->GetPolygonSize(k) != 3)
{
g_debug.Line(L"模型数据未三角化!");
continue;
}
FbxVector4* ctrl_point = mesh->GetControlPoints();
for (int l = 0; l != 3; ++l)
{
int index = mesh->GetPolygonVertex(k, l);
if (index == -1)
{
g_debug.Line(L"获取顶点失败!");
continue;
}
max_index = max(index, max_index);
//依次记录顶点的索引,而顶点数据只存放一次
//g_debug.Line(to_wstring(index));
//g_debug.Line(String::Format(L"(%.0lf)(%.0lf)(%.0lf)", ctrl_point[index][0], ctrl_point[index][1], ctrl_point[index][2]));
ret->_vecIndex.push_back(last_index + index);
if (!vec_inited[index])
{
vec_inited[index] = true;
ret->_vecVertex[last_index + index].position.x = (float)(ctrl_point[index][0]) * 0.01f;
ret->_vecVertex[last_index + index].position.y = (float)(ctrl_point[index][1]) * 0.01f;
ret->_vecVertex[last_index + index].position.z = -(float)(ctrl_point[index][2]) * 0.01f;
ret->_vecVertex[last_index + index].color = { 1.0f, 1.0f, 1.0f, 1.0f };
}
}
}
last_index += max_index + 1;
}
}
}
ret->_vecVertex.resize(last_index + 1);
return ret;
}
4.数据传递到DX
顶点缓存和索引缓存都是需要通过上载堆传递数据,使用com_ptr的局部变量需要在完成GPU的数据上传后才能被释放。这一部分比较麻烦,后面再详细捋一捋。
HRESULT hr = S_OK;
//-------------------------------------------------顶点缓存------------------------------------------------------------
// Create the vertex buffer.
//默认堆需要上载堆传递CPU数据
CD3DX12_HEAP_PROPERTIES heapProps0(D3D12_HEAP_TYPE_DEFAULT);
auto desc0 = CD3DX12_RESOURCE_DESC::Buffer(mesh_object->GetVertexDataSize());
hr = g_dx._device->CreateCommittedResource(
&heapProps0,
D3D12_HEAP_FLAG_NONE,
&desc0,
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&_vertexBuffer));
if (FAILED(hr))
{
g_debug.Line(L"创建顶点缓存失败(DEFAULT)!");
return;
}
//上载堆
com_ptr<ID3D12Resource> vertexBufferUploadHeap;
CD3DX12_HEAP_PROPERTIES heapProps1(D3D12_HEAP_TYPE_UPLOAD);
auto desc1 = CD3DX12_RESOURCE_DESC::Buffer(mesh_object->GetVertexDataSize());
hr = g_dx._device->CreateCommittedResource(
&heapProps1,
D3D12_HEAP_FLAG_NONE,
&desc1,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&vertexBufferUploadHeap));
if (FAILED(hr))
{
g_debug.Line(L"创建顶点缓存失败(UPLOAD)!");
return;
}
_vertexBuffer->SetName(L"vb0");
// Copy data to the intermediate upload heap and then schedule a copy
// from the upload heap to the vertex buffer.
//从内存复制到上载堆,再复制到GPU
D3D12_SUBRESOURCE_DATA vertexData = {};
vertexData.pData = mesh_object->GetVertexData();
vertexData.RowPitch = mesh_object->GetVertexDataSize();
vertexData.SlicePitch = vertexData.RowPitch;
UpdateSubresources<1>(g_dx._commandList.get(), _vertexBuffer.get(), vertexBufferUploadHeap.get(), 0, 0, 1, &vertexData);
auto num_barrier = CD3DX12_RESOURCE_BARRIER::Transition(_vertexBuffer.get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER);
g_dx._commandList->ResourceBarrier(1, &num_barrier);
// Initialize the vertex buffer view.
//创建顶点缓存视图
_vertexBufferView.BufferLocation = _vertexBuffer->GetGPUVirtualAddress();
_vertexBufferView.StrideInBytes = sizeof(DirectX::Vertex);
_vertexBufferView.SizeInBytes = mesh_object->GetVertexDataSize();
//-------------------------------------------------索引缓存------------------------------------------------------------
CD3DX12_HEAP_PROPERTIES heapProps2(D3D12_HEAP_TYPE_DEFAULT);
auto desc2 = CD3DX12_RESOURCE_DESC::Buffer(mesh_object->GetIndexDataSize());
hr = g_dx._device->CreateCommittedResource(
&heapProps2,
D3D12_HEAP_FLAG_NONE,
&desc2,
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&_indexBuffer));
if (FAILED(hr))
{
g_debug.Line(L"创建索引缓存失败(DEFAULT)!");
return;
}
com_ptr<ID3D12Resource> indexBufferUploadHeap;
CD3DX12_HEAP_PROPERTIES heapProps3(D3D12_HEAP_TYPE_UPLOAD);
auto desc3 = CD3DX12_RESOURCE_DESC::Buffer(mesh_object->GetIndexDataSize());
hr = g_dx._device->CreateCommittedResource(
&heapProps3,
D3D12_HEAP_FLAG_NONE,
&desc3,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&indexBufferUploadHeap));
_indexBuffer->SetName(L"ib0");
// Copy data to the intermediate upload heap and then schedule a copy
// from the upload heap to the index buffer.
D3D12_SUBRESOURCE_DATA indexData = {};
indexData.pData = mesh_object->GetIndexData();
indexData.RowPitch = mesh_object->GetIndexDataSize();
indexData.SlicePitch = indexData.RowPitch;
UpdateSubresources<1>(g_dx._commandList.get(), _indexBuffer.get(), indexBufferUploadHeap.get(), 0, 0, 1, &indexData);
auto num_barrier1 = CD3DX12_RESOURCE_BARRIER::Transition(_indexBuffer.get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_INDEX_BUFFER);
g_dx._commandList->ResourceBarrier(1, &num_barrier1);
// Describe the index buffer view.
_indexBufferView.BufferLocation = _indexBuffer->GetGPUVirtualAddress();
_indexBufferView.Format = DXGI_FORMAT_R32_UINT;
_indexBufferView.SizeInBytes = mesh_object->GetIndexDataSize();
_numIndices = mesh_object->GetIndexDataSize() / 4; // R32_UINT (SampleAssets::StandardIndexFormat) = 4 bytes each.
5.模型位置
之所以最终绘制出的模型都在原点,是因为FBX的单个模型的顶点都是以自身坐标系的,而自身的位置属性(位移、旋转、缩放)通过以下面方式得到:
FbxDouble3 translation = pNode->LclTranslation.Get();
FbxDouble3 rotation = pNode->LclRotation.Get();
FbxDouble3 scaling = pNode->LclScaling.Get();
其后可以通过构造矩阵,在读取时就变换到整体的坐标系,比如一个大的游戏场景,有很多固定不动的物体,我们就不必分开处理,直接当成一个整体模型来处理。
如果是动态的、反复出现的模型,就需要单独保存,通过上传矩阵到着色器来计算位置。
6.结语
目前我也只是一步一步的尝试,有很多不完善的地方,此文章只是简单说一下,等以后理解得比较透彻后再详细一步一步的说明。
代码上传到了这儿:
https://gitee.com/lveyou/dnd3d
接下来做一下按键检测,来控制镜头,方便详细的观察模型,好弄清楚它们的坐标系到底是怎样的。