天天看點

基于FBX SDK的FBX模型解析與加載

1. 簡介

FBX是Autodesk的一個用于跨平台的免費三維數 據交換的格式(最早不是由Autodesk開發,但後來被其收購),目前被衆多的标準模組化軟體所支援,在遊戲開發領域也常用來作為各種模組化工具的标準導出 格式。Autodesk提供了基于C++(還有Python)的SDK來實作對FBX格式的各種讀寫、修改以及轉換等操作,之是以如此是因為FBX的格式 不是公開的,這也是FBX的诟病之一。與FBX相對的則是格式開源的Collada,它的應用也很廣泛。總體來說這兩種格式還是各有優劣不相上下,關于兩 種格式在遊戲開發中使用的對比與讨論也比較多,可見GameDev中的文章:http://www.gamedev.net/topic/467753-collada-vs-autodesk-fbx , 這裡就不再論述了。大多數情況下我們是需要解析模型在程式中渲染使用,是以這裡主要讨論使用FBX SDK來對FBX模型進行解析與加載(主要包括幾何網格、材質、Light與Camera、Skeleton動畫等),而對于在模組化工具中可能涉及到的FBX寫出等則沒有涉及。

2. FBX SDK的配置

首先,在使用前需要下載下傳安裝FBX的SDK,可以從Autodesk的網站上進行獲得最新的版本http://usa.autodesk.com/adsk/servlet/index?siteID=123112&id=7478532(需要填些基本資訊注冊下)。安 裝之後在VS裡邊的配置就跟D3D類似。其中的Samples基本上涵蓋了FBX相關的應用,可以在使用之前好好研究一下。最新的SDK版本(2012 版)與之前的版本會在細節上有所不同(有些較大的改動是實作某些功能的API接口的修改,具體細節可以用2012的Programmer's guide中找到),而且支援最新的FBX格式,是以最好就直接用最新的版本。

3. FBX模型的組織結構

FBX是以scene graph的結構來存儲模型的所有資訊(也可以認為是一個多叉樹),類似于OSG中的組織方式,這一點可以從SDK所帶的Sample裡邊很清楚的看出來。一個較為典型的模型的組織結構與下圖所示:

基于FBX SDK的FBX模型解析與加載

整個Scene是從一個空屬性的根結點開始,其中每個結 點均是一個KFbxNode的對象,所有對象之間的關聯均是雙向的,比如從子結點可以索引到父結點,從父結點也可以索引到子結點;從單個結點可以索引到整 個Scene,從Scene也可以索引到該結點。每個結點都會有一個标記屬性的Enum值,比如eMesh、eLight、eCamera或 eSkeleton等,分别用來标記目前結點是Mesh、Light、Camera或Skeleton。在整個結構的周遊過程中可以通過判斷不同的結點屬 性而進行不同的處理操作。

在進行使用SDK進行FBX的處理操作之前需要先初始化 兩個必須的FBX對象:KFbxSdkManager和KFbxScene。前者用來對所有的FBX對象進行内在管理,所有使用SDK加載的資源均在此對 象的管控之下,最終的資源釋放也由其來完成。有了記憶體管理器之後再在其上建立一個相關的KFbxScene對象之後即可以進行模型的加截與處理了。 KFbxScene其實相當于Manager提供的整個場景對象的一個入口。兩個對象的初始化與配置代碼如下所述:

初始化SDKManager

  1. bool FBXImporter::Initialize()
  2. {
  3. // Create the FBX SDK Manager, destroy the old manager at first
  4. if(mpFBXSDKManager)
  5. {
  6. mpFBXSDKManager->Destroy();
  7. }
  8. mpFBXSDKManager = KFbxSdkManager::Create();
  9. if(mpFBXSDKManager == NULL)
  10. {
  11. return false;
  12. }
  13. // Create an IOSettings object
  14. KFbxIOSettings* ios = KFbxIOSettings::Create(mpFBXSDKManager , IOSROOT);
  15. mpFBXSDKManager->SetIOSettings(ios);
  16. // Load plug-ins from the executable directory
  17. KString lExtension = "dll";
  18. KString lPath = KFbxGetApplicationDirectory();
  19. mpFBXSDKManager->LoadPluginsDirectory(lPath.Buffer() , lExtension.Buffer());
  20. // Create the entity that hold the whole Scene
  21. mpFBXSDKScene = KFbxScene::Create(mpFBXSDKManager , "");
  22. return true;
  23. }

FbxScene的初始化

  1. bool FBXImporter::LoadScene(const char* pSeneName)
  2. {
  3. if(mpFBXSDKManager == NULL)
  4. {
  5. return false;
  6. }
  7. // Get the file version number generate by the FBX SDK.
  8. KFbxSdkManager::GetFileFormatVersion(mSDKVersion.mMajor , mSDKVersion.mMinor , mSDKVersion.mRevision);
  9. // Create an importer.
  10. KFbxImporter* pKFBXImporter = KFbxImporter::Create(mpFBXSDKManager , "");
  11. // Initialize the importer by providing a filename
  12. FBXFileVersion fileVersion;
  13. bool importStatus = pKFBXImporter->Initialize(fileName , -1 , mpFBXSDKManager->GetIOSettings());
  14. lImporter->GetFileVersion(fileVersion.mMajor , fileVersion.mMinor , fileVersion.mRevision);
  15. if(!importStatus)
  16. {
  17. return false;
  18. }
  19. // Import the scene
  20. mpFBXScene->Clear();
  21. importStatus = pKFBXImporter->Import(m_mpFBXScene);
  22. // Destroy the importer.
  23. pKFBXImporter->Destroy();
  24. return importStatus;
  25. }

在完成了對KFbxScene的初始化操作之後即可以從 其中得到整個模型的所有資訊。由于FBX的是采用了類似于樹的結構來進行存儲,因而就很容易使用類似于樹的遞歸方法來周遊其中的每個結點,并根據結點的屬 性選擇合适的處理操作,下述代碼就完成了從根結點開始的全局周遊:

  1. void ProcessNode(KFbxNode* pNode)
  2. {
  3. KFbxNodeAttribute::EAttributeType attributeType;
  4. if(pNode->GetNodeAttribute())
  5. {
  6. switch(pNode->GetNodeAttribute()->GetAttributeType())
  7. {
  8. case KFbxNodeAttribute::eMESH:
  9. ProcessMesh(pNode);
  10. break;
  11. case KFbxNodeAttribute::eSKELETON:
  12. ProcessSkeleton(pNode);
  13. break;
  14. case KFbxNodeAttribute::eLIGHT:
  15. ProcessLight(pNode);
  16. break;
  17. case KFbxNodeAttribute::eCAMERA:
  18. ProcessCamera();
  19. break;
  20. }
  21. }
  22. for(int i = 0 ; i < pNode->GetChildCount() ; ++i)
  23. {
  24. ProcessNode(pNode->GetChild(i));
  25. }
  26. }

上述代碼比較簡單,直接傳入由KFbxScene中 獲得的根結點之後即可周遊到每一個子結點。在FBX的存儲中,每個父結點可以包含多個子結點,但每個子結點隻有一個根結點,而且這其中的聯系是雙向的,這 樣很友善,比如在處理Skeleton時就常常需要從子結點中得到父結點的matrix等資訊,而這種雙向關系使得這些操作很容易實作。注意,上述代碼中 有pNode->GetNodeAttribute()檢查操作是必須的,因為并不是所有的結點都有相應的屬性(Attribute也是以子結點的 方式關聯到目前的結點上的,因而可能為空)。

4. 加載幾何網格

FBX對幾何網格支援得還是很好的,Nurbes、 Polygon、Triangle等均可以存儲。不過為了簡化加載和處理時的操作,最好直接在FBX導出插件中選擇一種統一的模式。比如可以在導出生成 FBX時選中Triangluation的屬性,那麼FBX導出插件會自動把所有的Nurbes、Polygon三角化為三角形進行存儲。當然,這個過程 也可以在模型進行加載時來進行。這樣在得到的FBX中就隻有三角形這樣一種網格模型,友善了加載的操作。模型的幾何資料主要包括以下部分:

  • Vertex 組成網格的頂點資訊,這一部分是必須的。
  • Color 每個頂點的顔色,一般不需要。
  • Normal 每個頂點所對應的法向,是由FBX導出插件計算生成,可以是逐面片或逐頂點。
  • UV 每個頂點所對應的法向,是由FBX導出插件計算生成,可以是逐面片或逐頂點。
  • Tangent 每個頂點所對應的貼圖UV值,一般來說,每個UV對應一個Layer,一個頂點可以有多個UV通道,這在讀入的時間需要進行判斷

幾何網格的加載比較簡單,直接遞歸地從根結點來周遊整個graph,檢測目前的結點是否為eMESH的屬性,若是即處理其中的幾何資料,主要代碼如下所示:

  1. void ProcessMesh(KFbxNode* pNode)
  2. {
  3. KFbxMesh* pMesh = pNode->GetMesh();
  4. if(pMesh == NULL)
  5. {
  6. return;
  7. }
  8. D3DXVECTOR3 vertex[3];
  9. D3DXVECTOR4 color[3];
  10. D3DXVECTOR3 normal[3];
  11. D3DXVECTOR3 tangent[3];
  12. D3DXVECTOR2 uv[3][2];
  13. int triangleCount = pMesh->GetPolygonCount();
  14. int vertexCounter = 0;
  15. for(int i = 0 ; i < triangleCount ; ++i)
  16. {
  17. for(int j = 0 ; j < 3 ; j++)
  18. {
  19. int ctrlPointIndex = pMesh->GetPolygonVertex(i , j);
  20. // Read the vertex
  21. ReadVertex(pMesh , ctrlPointIndex , &vertex[j]);
  22. // Read the color of each vertex
  23. ReadColor(pMesh , ctrlPointIndex , vertexCounter , &color[j]);
  24. // Read the UV of each vertex
  25. for(int k = 0 ; k < 2 ; ++k)
  26. {
  27. ReadUV(pMesh , ctrlPointIndex , pMesh->GetTextureUVIndex(i, j) , k , &(uv[j][k]));
  28. }
  29. // Read the normal of each vertex
  30. ReadNormal(pMesh , ctrlPointIndex , vertexCounter , &normal[j]);
  31. // Read the tangent of each vertex
  32. ReadTangent(pMesh , ctrlPointIndex , vertexCounter , &tangent[j]);
  33. vertexCounter++;
  34. }
  35. // 根據讀入的資訊組裝三角形,并以某種方式使用即可,比如存入到清單中、儲存到檔案等...
  36. }
  37. }

上述代碼完成了從一個Node裡邊讀出相應的網格資訊。 首先,從Node裡邊得到相應KFbxMesh指針,可知,如果該Node不是eMESH屬性的話那麼該指針就為空,後繼操作不能再進行。注意其中用 triangleCount變量來存儲pMesh->GetPolygonCount()的值,這主要是在前面也提到過了,假定對于所有的FBX模 型在存儲時均標明了Triangulation的操作,因而其中存儲的Polygon是三角形,如此一來每個裡邊一定隻包含3個頂點,依次讀入這3個頂點 所對應的各屬性資訊即可。在FBX中對于每個頂點所對應的各種額外屬性,比如Normal、Tangent、UV等均可對應多個通道,這可以通過在每個 Mesh裡邊增加相應屬性的一個Layer即可實作,在使用FBX SDK寫出FBX檔案時很容易做到。比如上述代碼中就從FBX中讀出4個UV通道中的值(第一個是正常的貼圖通道,第二層是LightMap的通道)。 vertexCounter是記錄已經處理過的頂點的數目,這主要是頂點資訊讀取在某些映射模式下(比如下述使用到vertexCounter的 eBY_POLYGON_VERTEX等)需要知道其在全局頂ControlPoints中的資訊,因而增加這樣的一個變量來進行記錄。

讀入頂點:

  1. void ReadVertex(KFbxMesh* pMesh , int ctrlPointIndex , D3DXVECTOR3* pVertex)
  2. {
  3. KFbxVector4* pCtrlPoint = pMesh->GetControlPoints();
  4. pVertex->x = pCtrlPoint[ctrlPointIndex].GetAt(0);
  5. pVertex->y = pCtrlPoint[ctrlPointIndex].GetAt(1);
  6. pVertex->z = pCtrlPoint[ctrlPointIndex].GetAt(2);
  7. }

讀入Color:

  1. void ReadColor(KFbxMesh* pMesh , int ctrlPointIndex , int vertexCounter , D3DXVECTOR4* pColor)
  2. {
  3. if(pMesh->GetElementVertexColorCount < 1)
  4. {
  5. return;
  6. }
  7. KFbxGeometryElementVertexColor* pVertexColor = pMesh->GetElementVertexColor(0);
  8. switch(pVertexColor->GetMappingMode())
  9. {
  10. case KFbxGeometryElement::eBY_CONTROL_POINT:
  11. {
  12. switch(pVertexColor->GetReferenceMode())
  13. {
  14. case KFbxGeometryElement::eDIRECT:
  15. {
  16. pColor->x = pVertexColor->GetDirectArray().GetAt(ctrlPointIndex).mRed;
  17. pColor->y = pVertexColor->GetDirectArray().GetAt(ctrlPointIndex).mGreen;
  18. pColor->z = pVertexColor->GetDirectArray().GetAt(ctrlPointIndex).mBlue;
  19. pColor->w = pVertexColor->GetDirectArray().GetAt(ctrlPointIndex).mAlpha;
  20. }
  21. break;
  22. case KFbxGeometryElement::eINDEX_TO_DIRECT:
  23. {
  24. int id = pVertexColor->GetIndexArray().GetAt(ctrlPointIndex);
  25. pColor->x = pVertexColor->GetDirectArray().GetAt(id).mRed;
  26. pColor->y = pVertexColor->GetDirectArray().GetAt(id).mGreen;
  27. pColor->z = pVertexColor->GetDirectArray().GetAt(id).mBlue;
  28. pColor->w = pVertexColor->GetDirectArray().GetAt(id).mAlpha;
  29. }
  30. break;
  31. default:
  32. break;
  33. }
  34. }
  35. break;
  36. case KFbxGeometryElement::eBY_POLYGON_VERTEX:
  37. {
  38. switch (pVertexColor->GetReferenceMode())
  39. {
  40. case KFbxGeometryElement::eDIRECT:
  41. {
  42. pColor->x = pVertexColor->GetDirectArray().GetAt(vertexCounter).mRed;
  43. pColor->y = pVertexColor->GetDirectArray().GetAt(vertexCounter).mGreen;
  44. pColor->z = pVertexColor->GetDirectArray().GetAt(vertexCounter).mBlue;
  45. pColor->w = pVertexColor->GetDirectArray().GetAt(vertexCounter).mAlpha;
  46. }
  47. break;
  48. case KFbxGeometryElement::eINDEX_TO_DIRECT:
  49. {
  50. int id = pVertexColor->GetIndexArray().GetAt(vertexCounter);
  51. pColor->x = pVertexColor->GetDirectArray().GetAt(id).mRed;
  52. pColor->y = pVertexColor->GetDirectArray().GetAt(id).mGreen;
  53. pColor->z = pVertexColor->GetDirectArray().GetAt(id).mBlue;
  54. pColor->w = pVertexColor->GetDirectArray().GetAt(id).mAlpha;
  55. }
  56. break;
  57. default:
  58. break;
  59. }
  60. }
  61. break;
  62. }
  63. }

讀入UV:

  1. void ReadUV(KFbxMesh* pMesh , int ctrlPointIndex , int textureUVIndex , int uvLayer , D3DXVECTOR2* pUV)
  2. {
  3. if(uvLayer >= 2 || pMesh->GetElementUVCount() <= uvLayer)
  4. {
  5. return false;
  6. }
  7. KFbxGeometryElementUV* pVertexUV = pMesh->GetElementUV(uvLayer);
  8. switch(pVertexUV->GetMappingMode())
  9. {
  10. case KFbxGeometryElement::eBY_CONTROL_POINT:
  11. {
  12. switch(pVertexUV->GetReferenceMode())
  13. {
  14. case KFbxGeometryElement::eDIRECT:
  15. {
  16. pUV->x = pVertexUV->GetDirectArray().GetAt(ctrlPointIndex).GetAt(0);
  17. pUV->y = pVertexUV->GetDirectArray().GetAt(ctrlPointIndex).GetAt(1);
  18. }
  19. break;
  20. case KFbxGeometryElement::eINDEX_TO_DIRECT:
  21. {
  22. int id = pVertexUV->GetIndexArray().GetAt(ctrlPointIndex);
  23. pUV->x = pVertexUV->GetDirectArray().GetAt(id).GetAt(0);
  24. pUV->y = pVertexUV->GetDirectArray().GetAt(id).GetAt(1);
  25. }
  26. break;
  27. default:
  28. break;
  29. }
  30. }
  31. break;
  32. case KFbxGeometryElement::eBY_POLYGON_VERTEX:
  33. {
  34. switch (pVertexUV->GetReferenceMode())
  35. {
  36. case KFbxGeometryElement::eDIRECT:
  37. case KFbxGeometryElement::eINDEX_TO_DIRECT:
  38. {
  39. pUV->x = pVertexUV->GetDirectArray().GetAt(textureUVIndex).GetAt(0);
  40. pUV->y = pVertexUV->GetDirectArray().GetAt(textureUVIndex).GetAt(1);
  41. }
  42. break;
  43. default:
  44. break;
  45. }
  46. }
  47. break;
  48. }
  49. }

讀入Normal:

  1. void ReadNormal(KFbxMesh* pMesh , int ctrlPointIndex , int vertexCounter , D3DXVECTOR3* pNormal)
  2. {
  3. if(pMesh->GetElementNormalCount() < 1)
  4. {
  5. return;
  6. }
  7. KFbxGeometryElementNormal* leNormal = pMesh->GetElementNormal(0);
  8. switch(leNormal->GetMappingMode())
  9. {
  10. case KFbxGeometryElement::eBY_CONTROL_POINT:
  11. {
  12. switch(leNormal->GetReferenceMode())
  13. {
  14. case KFbxGeometryElement::eDIRECT:
  15. {
  16. pNormal->x = leNormal->GetDirectArray().GetAt(ctrlPointIndex).GetAt(0);
  17. pNormal->y = leNormal->GetDirectArray().GetAt(ctrlPointIndex).GetAt(1);
  18. pNormal->z = leNormal->GetDirectArray().GetAt(ctrlPointIndex).GetAt(2);
  19. }
  20. break;
  21. case KFbxGeometryElement::eINDEX_TO_DIRECT:
  22. {
  23. int id = leNormal->GetIndexArray().GetAt(ctrlPointIndex);
  24. pNormal->x = leNormal->GetDirectArray().GetAt(id).GetAt(0);
  25. pNormal->y = leNormal->GetDirectArray().GetAt(id).GetAt(1);
  26. pNormal->z = leNormal->GetDirectArray().GetAt(id).GetAt(2);
  27. }
  28. break;
  29. default:
  30. break;
  31. }
  32. }
  33. break;
  34. case KFbxGeometryElement::eBY_POLYGON_VERTEX:
  35. {
  36. switch(leNormal->GetReferenceMode())
  37. {
  38. case KFbxGeometryElement::eDIRECT:
  39. {
  40. pNormal->x = leNormal->GetDirectArray().GetAt(vertexCounter).GetAt(0);
  41. pNormal->y = leNormal->GetDirectArray().GetAt(vertexCounter).GetAt(1);
  42. pNormal->z = leNormal->GetDirectArray().GetAt(vertexCounter).GetAt(2);
  43. }
  44. break;
  45. case KFbxGeometryElement::eINDEX_TO_DIRECT:
  46. {
  47. int id = leNormal->GetIndexArray().GetAt(vertexCounter);
  48. pNormal->x = leNormal->GetDirectArray().GetAt(id).GetAt(0);
  49. pNormal->y = leNormal->GetDirectArray().GetAt(id).GetAt(1);
  50. pNormal->z = leNormal->GetDirectArray().GetAt(id).GetAt(2);
  51. }
  52. break;
  53. default:
  54. break;
  55. }
  56. }
  57. break;
  58. }
  59. }

讀入Tangent:

  1. void ReadTangent(KFbxMesh* pMesh , int ctrlPointIndex , int vertecCounter , D3DXVECTOR3* pTangent)
  2. {
  3. if(pMesh->GetElementTangentCount() < 1)
  4. {
  5. return;
  6. }
  7. KFbxGeometryElementTangent* leTangent = pMesh->GetElementTangent(0);
  8. switch(leTangent->GetMappingMode())
  9. {
  10. case KFbxGeometryElement::eBY_CONTROL_POINT:
  11. {
  12. switch(leTangent->GetReferenceMode())
  13. {
  14. case KFbxGeometryElement::eDIRECT:
  15. {
  16. pTangent->x = leTangent->GetDirectArray().GetAt(ctrlPointIndex).GetAt(0);
  17. pTangent->y = leTangent->GetDirectArray().GetAt(ctrlPointIndex).GetAt(1);
  18. pTangent->z = leTangent->GetDirectArray().GetAt(ctrlPointIndex).GetAt(2);
  19. }
  20. break;
  21. case KFbxGeometryElement::eINDEX_TO_DIRECT:
  22. {
  23. int id = leTangent->GetIndexArray().GetAt(ctrlPointIndex);
  24. pTangent->x = leTangent->GetDirectArray().GetAt(id).GetAt(0);
  25. pTangent->y = leTangent->GetDirectArray().GetAt(id).GetAt(1);
  26. pTangent->z = leTangent->GetDirectArray().GetAt(id).GetAt(2);
  27. }
  28. break;
  29. default:
  30. break;
  31. }
  32. }
  33. break;
  34. case KFbxGeometryElement::eBY_POLYGON_VERTEX:
  35. {
  36. switch(leTangent->GetReferenceMode())
  37. {
  38. case KFbxGeometryElement::eDIRECT:
  39. {
  40. pTangent->x = leTangent->GetDirectArray().GetAt(vertecCounter).GetAt(0);
  41. pTangent->y = leTangent->GetDirectArray().GetAt(vertecCounter).GetAt(1);
  42. pTangent->z = leTangent->GetDirectArray().GetAt(vertecCounter).GetAt(2);
  43. }
  44. break;
  45. case KFbxGeometryElement::eINDEX_TO_DIRECT:
  46. {
  47. int id = leTangent->GetIndexArray().GetAt(vertecCounter);
  48. pTangent->x = leTangent->GetDirectArray().GetAt(id).GetAt(0);
  49. pTangent->y = leTangent->GetDirectArray().GetAt(id).GetAt(1);
  50. pTangent->z = leTangent->GetDirectArray().GetAt(id).GetAt(2);
  51. }
  52. break;
  53. default:
  54. break;
  55. }
  56. }
  57. break;
  58. }
  59. }

上述幾個Normal、Tangent、UV等資訊讀取的函數的實作其實都差不多,首先需要判斷有沒有相應的Layer關聯在目前的Mesh中,若有則擷取其位址,然後根據不同的映射方式使用不同的方法從記憶體中讀取相應的值即可。

完成了這些基本幾何資訊的讀取之後即可以使用其進行渲染了:

基于FBX SDK的FBX模型解析與加載

5. 加載材質

Material是一個模型渲染時必不可少的部分,當 然,這些資訊也被存到了FBX之中(甚至各種貼圖等也可以直接内嵌到FBX内部),就需要從FBX中加載這些資訊以完成帶有材質的渲染。材質的加載可以與 Mesh的加載相結合來完成,但更好的方法是獨立進行,這樣各子產品間的關系更清晰,但這就需要一個額外的操作,那就是關聯Mesh與Material。 FBX中的材質對象包含了豐富的資訊,比如最正常的從Max中可以看到那些材質屬性,如ambient、diffuse、specular的color和 texture;shininess、opacity值等,更進階一點的屬性諸如Effect的參數、源檔案等都可以儲存。它是盡可能保證從模組化工具中導 出時不丢失地儲存材質資訊,但我們在使用時卻可以有選擇地讀取。

5.1 關聯Mesh與材質

對于Material與Mesh獨立加載的系統而言,首先需要讀取相關的資訊将兩者關聯起來,這些資訊其實對也都存儲在KFbxMesh之内(屬于幾何資訊的一部分吧)。每個帶有材質的Mesh結點上都會包含有一個類型為KFbxGeometryElementMaterial的結點(若不含有材質則該結點為空),該結點中記錄了Mesh中的多邊形(這裡全部為三角形)與每個材質的對應關系,讀取該結點中的資訊建立Mesh與Material之間的連接配接關系,代碼如下:

  1. void ConnectMaterialToMesh(KFbxMesh* pMesh , int triangleCount , int* pTriangleMtlIndex)
  2. {
  3. // Get the material index list of current mesh
  4. KFbxLayerElementArrayTemplate<int>* pMaterialIndices;
  5. KFbxGeometryElement::EMappingMode materialMappingMode = KFbxGeometryElement::eNONE;
  6. if(pMesh->GetElementMaterial())
  7. {
  8. pMaterialIndices = &pMesh->GetElementMaterial()->GetIndexArray();
  9. materialMappingMode = pMesh->GetElementMaterial()->GetMappingMode();
  10. if(pMaterialIndices)
  11. {
  12. switch(materialMappingMode)
  13. {
  14. case KFbxGeometryElement::eBY_POLYGON:
  15. {
  16. if(pMaterialIndices->GetCount() == triangleCount)
  17. {
  18. for(int triangleIndex = 0 ; triangleIndex < triangleCount ; ++triangleIndex)
  19. {
  20. int materialIndex = pMaterialIndices->GetAt(triangleIndex);
  21. pTriangleMtlIndex[triangleIndex] = materialIndex;
  22. }
  23. }
  24. }
  25. break;
  26. case KFbxGeometryElement::eALL_SAME:
  27. {
  28. int lMaterialIndex = pMaterialIndices->GetAt(0);
  29. for(int triangleIndex = 0 ; triangleIndex < triangleCount ; ++triangleIndex)
  30. {
  31. int materialIndex = pMaterialIndices->GetAt(triangleIndex);
  32. pTriangleMtlIndex[triangleIndex] = materialIndex;
  33. }
  34. }
  35. }
  36. }
  37. }
  38. }

其中上triangleCount即為從pMesh中讀 取得到的三角形的數量,pTriangleMtlIndex是一個長度為triangleCount的數組,主要用來存儲讀取到的三角形對應的材質索引。 注意:這裡考慮的情況是對于一個三角形隻對應一個材質,而一般情況下也是這樣(如果是對應多個材質的話需要些許修改此處的代碼)。完成Mesh的索引讀取 之後即可以将pTriangleMtlIndex中的值以合适的方式轉儲到對應的三角形清單中(或以其它的方式對應)以便在渲染時使用。

5.2 普通材質

FBX中實際存儲材質資訊的位置是每個Mesh中對應的一個類型為KFbxSurfaceMaterial的結點,其裡邊存儲了普通材質的典型資訊,主要包括以下屬性(有一些沒有列出):

  • ShadingModel 材質的光照模型,一般為兩種典型的局部光照模型:Phong、Lambert
  • Emissive Emissive屬性
  • EmissiveFactor
  • Ambient Ambient屬性
  • AmbientFactor
  • Diffuse Diffuse屬性
  • DiffuseFactor
  • Specular Specular屬性
  • SpecularFactor
  • Shininess Sepcular的Shininess屬性
  • Bump Normal Map相關的屬性
  • NormalMap
  • BumpFactor
  • TransparentColor Transparent屬性
  • TransparencyFactor
  • Reflection Reflection屬性
  • ReflectionFactor

當然,在實際應用中這些屬性并不一定需要全部讀取,可以根據情況選擇讀取即可。材質的讀取代碼如下所述(簡略版):

  1. void LoadMaterial(KFbxMesh* pMesh)
  2. {
  3. int materialCount;
  4. KFbxNode* pNode;
  5. if(pMesh && pMesh->GetNode())
  6. {
  7. pNode = pMesh->GetNode();
  8. materialCount = pNode->GetMaterialCount();
  9. }
  10. if(materialCount > 0)
  11. {
  12. for(int materialIndex = 0 ; materialIndex < materialCount ; materialIndex++)
  13. {
  14. KFbxSurfaceMaterial* pSurfaceMaterial = pNode->GetMaterial(materialIndex);
  15. LoadMaterialAttribute(pSurfaceMaterial);
  16. }
  17. }
  18. }
  1. void LoadMaterialAttribute(KFbxSurfaceMaterial* pSurfaceMaterial)
  2. {
  3. // Get the name of material
  4. pSurfaceMaterial->GetName();
  5. // Phong material
  6. if(pSurfaceMaterial->GetClassId().Is(KFbxSurfacePhong::ClassId))
  7. {
  8. // Ambient Color
  9. fbxDouble3 = ((KFbxSurfacePhong*)pSurfaceMaterial)->Ambient;
  10. // ...
  11. // Diffuse Color
  12. fbxDouble3 =((KFbxSurfacePhong*)pSurfaceMaterial)->Diffuse;
  13. // ...
  14. // Specular Color
  15. fbxDouble3 =((KFbxSurfacePhong*)pSurfaceMaterial)->Specular;
  16. // ...
  17. // Emissive Color
  18. fbxDouble3 =((KFbxSurfacePhong*)pSurfaceMaterial)->Emissive;
  19. // ...
  20. // Opacity
  21. fbxDouble1 =((KFbxSurfacePhong*)pSurfaceMaterial)->TransparencyFactor;
  22. // ...
  23. // Shininess
  24. fbxDouble1 =((KFbxSurfacePhong*)pSurfaceMaterial)->Shininess;
  25. // ...
  26. // Reflectivity
  27. fbxDouble1 =((KFbxSurfacePhong*)pSurfaceMaterial)->ReflectionFactor;
  28. // ...
  29. return;
  30. }
  31. // Lambert material
  32. if(pSurfaceMaterial->GetClassId().Is(KFbxSurfaceLambert::ClassId))
  33. {
  34. // Ambient Color
  35. fbxDouble3=((KFbxSurfaceLambert*)pSurfaceMaterial)->Ambient;
  36. // ...
  37. // Diffuse Color
  38. fbxDouble3 =((KFbxSurfaceLambert*)pSurfaceMaterial)->Diffuse;
  39. // ...
  40. // Emissive Color
  41. fbxDouble3 =((KFbxSurfaceLambert*)pSurfaceMaterial)->Emissive;
  42. // ...
  43. // Opacity
  44. fbxDouble1 =((KFbxSurfaceLambert*)pSurfaceMaterial)->TransparencyFactor;
  45. // ...
  46. return;
  47. }
  48. }

上述代碼就可以完成對普通屬性加載。另外,材質中關聯的Texture也需要進行加載,這個操作一般與一個紋理管理器結合起來進行,以便對所有的Texture與Material之間形成合理的關聯,這一步的操作一般如下代碼所述:

  1. void LoadMaterialTexture(KFbxSurfaceMaterial* pSurfaceMaterial)
  2. {
  3. int textureLayerIndex;
  4. KFbxProperty pProperty;
  5. int texID;
  6. MaterialTextureDesc::MtlTexTypeEnum texType;
  7. for(textureLayerIndex = 0 ; textureLayerIndex < KFbxLayerElement::LAYERELEMENT_TYPE_TEXTURE_COUNT ; ++textureLayerIndex)
  8. {
  9. pProperty = pSurfaceMaterial->FindProperty(KFbxLayerElement::TEXTURE_CHANNEL_NAMES[textureLayerIndex]);
  10. if(pProperty.IsValid())
  11. {
  12. int textureCount = pProperty.GetSrcObjectCount(KFbxTexture::ClassId);
  13. for(int j = 0 ; j < textureCount ; ++j)
  14. {
  15. KFbxTexture* pTexture = KFbxCast<KFbxTexture>(pProperty.GetSrcObject(KFbxTexture::ClassId,j));
  16. if(pTexture)
  17. {
  18. // Use pTexture to load the attribute of current texture...
  19. }
  20. }
  21. }
  22. }
  23. }

5.3 硬體相關的材質與Effect

有過模組化經驗的童鞋都知道,在3D Max或Maya中可以為某些材質指定特定的Shader來完成特定的效果,這些模型在儲存時也會儲存相應的硬體相關的Shader到FBX模型中,因而 針對這樣屬性的材質也需要特别的代碼來進行加載。FBX裡邊支援嵌入CG、HLSL、GLSL等主流着色語言,而着色語言的類型在解析時也很容易得到。

  1. void LoadMaterialAttribute(KFbxSurfaceMaterial* pSurfaceMaterial)
  2. {
  3. KFbxImplementation* pImplementation;
  4. KString implemenationType;
  5. pImplementation = GetImplementation(pSurfaceMaterial , ImplementationHLSL);
  6. KString implemenationType = "HLSL";
  7. if(pImplementation)
  8. {
  9. LoadMaterialEffect(pSurfaceMaterial , pImplementation , &implemenationType);
  10. }
  11. }

上述代碼可以與前面的Material屬性讀取的代碼合 并。FBX一般通過一個類型為KFbxImplementation的對象将硬體相關的Shader與Material進行關聯,可以使用如上的代碼實作 兩者之間關聯的情況的擷取,其中ImplementationHLSL為一個辨別HLSL類型Shader的宏,若是CG則用 ImplementationCGFX。如果目前Material中包含了HLSL類型Shader之後,那麼就可以得到一個不為空的 KFbxImplementation類型的指針,在其中就可以解析該Shader的屬性,否則,則該指針為空,說明些材質關聯了其它類似的Shader 或是不包含Shader。通過KFbxImplementation來擷取Effect對應的屬性的代碼如下所示:

  1. void LoadMaterialEffect(KFbxSurfaceMaterial* pSurfaceMaterial , const KFbxImplementation* pImplementation , KString* pImplemenationType)
  2. {
  3. KFbxBindingTable const* lRootTable = pImplementation->GetRootTable();
  4. fbxString lFileName = lRootTable->DescAbsoluteURL.Get();
  5. fbxString lTechniqueName = lRootTable->DescTAG.Get();
  6. // Name of the effect file
  7. lFileName.Buffer();
  8. KFbxBindingTable const* pBTable = pImplementation->GetRootTable();
  9. size_t entryCount = pBTable->GetEntryCount();
  10. for(size_t i = 0 ; i < entryCount ; ++i)
  11. {
  12. const KFbxBindingTableEntry& btEntry = pBTable->GetEntry(i);
  13. const char* pEntrySrcType = btEntry.GetEntryType(true);
  14. KFbxProperty fbxProperty;
  15. // Name of Parameter
  16. btEntry.GetDestination();
  17. // Semantic of Parameter
  18. btEntry.GetDestination();
  19. if(strcmp(KFbxPropertyEntryView::sEntryType , pEntrySrcType) == 0)
  20. {
  21. fbxProperty = pSurfaceMaterial->FindPropertyHierarchical(btEntry.GetSource());
  22. if(!fbxProperty.IsValid())
  23. {
  24. fbxProperty = pSurfaceMaterial->RootProperty.FindHierarchical(btEntry.GetSource());
  25. }
  26. }
  27. else
  28. {
  29. if(strcmp(KFbxConstantEntryView::sEntryType , pEntrySrcType) == 0)
  30. {
  31. fbxProperty = pImplementation->GetConstants().FindHierarchical(btEntry.GetSource());
  32. }
  33. }
  34. if(fbxProperty.IsValid())
  35. {
  36. if(fbxProperty.GetSrcObjectCount(FBX_TYPE(KFbxTexture)) > 0)
  37. {
  38. // Texture Parameter
  39. for(int j = 0 ; j < fbxProperty.GetSrcObjectCount(FBX_TYPE(KFbxFileTexture)) ; ++j)
  40. {
  41. KFbxFileTexture* pFileTexture = fbxProperty.GetSrcObject(FBX_TYPE(KFbxFileTexture) , j);
  42. }
  43. for(int j = 0 ; j < fbxProperty.GetSrcObjectCount(FBX_TYPE(KFbxLayeredTexture)) ; ++j)
  44. {
  45. KFbxLayeredTexture* pLayeredTexture = fbxProperty.GetSrcObject(FBX_TYPE(KFbxLayeredTexture) , j);
  46. }
  47. for(int j = 0 ; j < fbxProperty.GetSrcObjectCount(FBX_TYPE(KFbxProceduralTexture)) ; ++j)
  48. {
  49. KFbxProceduralTexture* pProceduralTexture = fbxProperty.GetSrcObject(FBX_TYPE(KFbxProceduralTexture) , j);
  50. }
  51. }
  52. else
  53. {
  54. // Common Parameter
  55. KFbxDataType dataType = fbxProperty.GetPropertyDataType();
  56. // Bool value
  57. if(DTBool == dataType)
  58. {
  59. bool boolValue = KFbxGet<bool>(fbxProperty);
  60. }
  61. // Integer value
  62. if(DTInteger == dataType || DTEnum == dataType)
  63. {
  64. int intValue = KFbxGet<int>(fbxProperty);
  65. }
  66. // Float
  67. if(DTFloat == dataType)
  68. {
  69. float floatValue = KFbxGet<float>(fbxProperty);
  70. }
  71. // Double
  72. if(DTDouble == dataType)
  73. {
  74. double doubleValue = (float)KFbxGet<double>(fbxProperty);
  75. }
  76. // Double2
  77. if(DTDouble2 == dataType)
  78. {
  79. fbxDouble2 lDouble2 = KFbxGet<fbxDouble2>(fbxProperty);
  80. D3DXVECTOR2 double2Value = D3DXVECTOR2((float)lDouble2[0] , (float)lDouble2[1]);
  81. }
  82. // Double3
  83. if(DTDouble3 == dataType || DTVector3D == dataType || DTColor3 == dataType)
  84. {
  85. fbxDouble3 lDouble3 = KFbxGet<fbxDouble3>(fbxProperty);
  86. D3DXVECTOR3 double3Value = D3DXVECTOR3((float)lDouble3[0] , (float)lDouble3[1] , (float)lDouble3[2]);
  87. }
  88. // Double4
  89. if(DTDouble4 == dataType || DTVector4D == dataType || DTColor4 == dataType)
  90. {
  91. fbxDouble4 lDouble4 = KFbxGet<fbxDouble4>(fbxProperty);
  92. D3DXVECTOR4 double4Value = D3DXVECTOR4((float)lDouble4[0] , (float)lDouble4[1] , (float)lDouble4[2] , (float)lDouble4[3]);
  93. }
  94. // Double4x4
  95. if(DTDouble44 == dataType)
  96. {
  97. fbxDouble44 lDouble44 = KFbxGet<fbxDouble44>(fbxProperty);
  98. D3DXMATRIX double4x4Value;
  99. for(int i = 0 ; i < 4 ; ++i)
  100. {
  101. for(int j = 0 ; j < 4 ; ++j)
  102. {
  103. double4x4Value.m[i][j] = (float)lDouble44[i][j];
  104. }
  105. }
  106. }
  107. // String
  108. if(DTString == dataType || DTUrl == dataType || DTXRefUrl == dataType)
  109. {
  110. char* pStringBuffer =(KFbxGet<fbxString>(fbxProperty)).Buffer();
  111. }
  112. }
  113. }
  114. }
  115. }

可以解析到的Effect的主要屬性包括Shader所對應的源檔案、Shader中提供的各種外部參數的初始設定等(比如在3D Max中通過UI控件所調節的參數的數值)。具體的方法代碼裡邊已經比較明确了,這裡就不在贅述了。後續的一些操作就要看整個材質與Effect部分的資料結構如何組織以及如何與你自己的代碼整合。

5.4 根據材質優化Mesh

通過FBX導出之後得到的FBX模型在存儲時一般會以幾何屬性為首要考量因素來生成整個檔案的Scene graph,是以上述解析得到的幾何網格與Material之間的映射關系可能并不适合于直接進行繪制,一般需要重新再組織。比如其間的映射關系可能是

  1. Triangle0 -> Material1
  2. Triangle1 -> Material0
  3. Triangle2 -> Material1
  4. ...

如果一個應用的渲染流程使用了Material之間的最少切換次數來作為渲染的首要考慮的話,那麼就不能直接使用Triangle的順序來生成渲染Buffer,而需要根據Material對其進行再排序并重新組織幾何資料間的次序。

完成上述加載之後即可實作帶有材質的渲染效果:

基于FBX SDK的FBX模型解析與加載

6. 加載Camera和Light

在FBX模型中除了幾何資料外較為常用的資訊可能就是Camera和Light,雖然在遊戲中一般不直接從模型中得到這兩部分資訊,而是由引擎來提供,但是FBX中提供了對這些資訊儲存的支援。其實單純加載這兩部分的資訊很簡單,就像之前介紹的在整個Scene Graph中對每個Node周遊過程中,判斷得到目前結點是Camera或Light時調用相應的ProcessCamera或ProcessLight來完成相關的處理操作即可。

如果對于目前結點判斷得到其是一個Camera結點,那麼可以直接調用GetCamera來得到一個KFbxCamera類型的指針,之後就可以通過該指針來完成Camera屬性的擷取。

  1. void ProcessCamera(KFbxNode* pNode)
  2. {
  3. KFbxCamera* pCamera = pNode->GetCamera();
  4. // 調用相應的接口擷取Camera的屬性即可
  5. }

對于Light結點的處理與Camera類似。至于Camera與Light結點所具有的屬性可以直接在SDK中看kfbxcamera與kfbxlight的類型定義即可。

  1. void ProcessLight(KFbxNode* pNode)
  2. {
  3. KFbxLight* pLight = pNode->GetLight();
  4. // 調用相應的接口擷取Light的屬性即可
  5. }

7. 加載動畫

動畫資訊是模型資料中非常重要 的一部分,也是一個渲染或遊戲引擎最基本的需求之一。FBX對Animation的良好支援也成為其與.obj等靜态模型最主要差別之一,而且最新的 SDK中也提供了對Animation很豐富與簡便的操作接口,包括自定義寫入與讀出等。接下來介紹一下如何使用FBX SDK來加載FBX中存儲的動畫資訊。

7.1 動畫資料讀取

在FBX中實作對于動畫資料的存儲主要通過以下三個對象層來實作:Animaiton Stack、 Animation Layer、Animation Node,其層次關系為

Animation Stack -> Animation Layer -> Animation Node,圖示化結構為(圖檔來自于FBX SDKRef):

基于FBX SDK的FBX模型解析與加載

其中的Animation Stack為FBX動畫管理的最高層,其中包含着與之相關聯的Animation Layer等;每個Animation Stack對應着一套動作過程。每個Stack中包含一個或多個Animation Layer(當用來做blend時就需要多個Layer,但一般是一個)。在每個Layer中又通過一個KFbxAnimCurveNode的結點使 Layer與具體的動畫資料發生關系。一般情況下可以根據自己的需要情況或引擎的動畫實作方式來讀取FBX中的動畫資料,例如本人在實作時從FBX中讀取 資料的方法就可以抽像化為如下圖所示的結構:

基于FBX SDK的FBX模型解析與加載

其中對每個Node判斷其是否有對應的動畫資料,若有則讀取其Curve中的資料并存儲以供渲染更新使用,代碼如下所述:

  1. void LoadNodeCurve(KFbxAnimLayer* pAnimationLayer , KFbxNode* pNode , StackTimeSpan& timeSpan)
  2. {
  3. KTime keyTimer;
  4. unsigned long millseconds;
  5. for(UINT i = 0 ; i < timeSpan.mKeyNums ; ++i)
  6. {
  7. millseconds = timeSpan.mStart + (float)i * timeSpan.mStep;
  8. keyTimer.SetMilliSeconds(millseconds);
  9. // 計算得到目前結點在目前時刻下所對應的空間局部和全局矩陣
  10. // 局部矩陣對于Skeleton是必需的,因需要使用它來計算父子Skeleton之間的空間關系
  11. KFbxXMatrix curveKeyLocalMatrix = pNode->EvaluateLocalTransform(keyTimer);
  12. KFbxXMatrix curveKeyGlobalMatrix = pNode->EvaluateGlobalTransform(keyTimer);
  13. }
  14. }

代碼中的timeSpan是一 個自定義的結構,其中包含了整個FBX對象動畫資訊的相關資料,比如幀數、起始時間、幀間時差等;在讀取時需将其中的資訊轉換為一個KTime類型的對象 (keyTimer)以供FBX SDK的API使用。上述操作加載了動畫資料中直接相關的空間Matrix資訊,這是普通模型對象的基本動畫資訊。但是對于Camera或Light等對 象而言,動畫不僅包含着位置或空間資訊的變化而且還包含着一些其它的屬性變化如Camera的FOV,Light的Direction,Color等,這 些資訊也導出FBX時被存儲到了FBX中。而這些資訊的擷取就是通過KFbxCurveNode來實作,其關聯具體的Curve到相應的Property 上,進而從中獲得對應的動畫資訊。比如我們熟悉的Camera實作中有一個常用的屬性PixelAspectRatio,用來描述視口Width與Height之間的比值,對于某些動畫效果這個Ratio可能是時變的,因而在模組化時就會将該資訊同樣以動畫的資訊進行存儲,現在我們想要得到這一部分動畫資料。通過檢視kfbxcamera.h可以發現在KFbxCamera的定義中含有

KFbxTypedProperty<fbxDouble1> PixelAspectRatio

的一個成員變量,這即是PixelAspcetRatio動畫資料所存儲的位置;而在ProcessCamera時已經由目前Node的指針得到了Camera對應的指針,之後該部分讀取代碼基本上如下所述:

  1. void LoadCameraCurve(KFbxAnimLayer* pAnimationLayer , KFbxCamera* pCamera , StackTimeSpan& timeSpan)
  2. {
  3. if(pCamera == NULL)
  4. {
  5. return;
  6. }
  7. // 通過FBX的屬性對象而擷取其所對應的Animation Curve
  8. KFbxAnimCurve* pCameraAttriAnimCurve = pCamera->PixelAspectRatio.GetCurve<KFbxAnimCurve>(pAnimationLayer);
  9. // 判斷目前的屬性是否含有可變的Animation值
  10. if(pCameraAttriAnimCurve)
  11. {
  12. KTime keyTimer;
  13. unsigned long millseconds;
  14. for(UINT i = 0 ; i < timeSpan.mKeyNums ; ++i)
  15. {
  16. millseconds = timeSpan.mStart + (float)i * timeSpan.mStep;
  17. keyTimer.SetMilliSeconds(millseconds);
  18. // 計算Camera的某屬性在目前時刻所對應的值
  19. pCameraAttriAnimCurve->Evaluate(keyTimer);
  20. }
  21. }
  22. }

上述代碼通過PixelAspectRatio的屬性對象加載了其不同時刻下的動畫值,其它的屬性的動畫讀取也可以用類似的操作實作。

7.2 動畫驅動

加載了上述的動畫資料以後,即 可以使用其來驅動模型中的直接動畫相關部分,如Camera、Light、Skeleton等。由之前的代碼可知,在加載動畫資料時我們使用了目前 Node的指針,因而就可以用它在加載動畫時存儲其它的一些額外資訊使這些動畫資料與對應的Camera、Light、Skeleton等部件進行關聯 (比如Node的指針,或是Node的Name等),進而可以從動畫管理器中随時查得到某結點在指定時刻位置上的動畫資料。該部分可以根據具體的實作采取 适宜的操作即可完成。

最後,帶有動畫驅動的Skeleton渲染效果如下列圖所示(Camera,Light的動畫效果木有繪出):

基于FBX SDK的FBX模型解析與加載
基于FBX SDK的FBX模型解析與加載
基于FBX SDK的FBX模型解析與加載
基于FBX SDK的FBX模型解析與加載

8. 骨骼蒙皮動畫

骨骼蒙皮動畫是目前遊戲引擎中最常用的一種動畫方式,關 于其基本原理網絡上的資料較多,關于到涉及的其它較複雜操作,如插值、融合等在這裡也就先不再讨論了,而且其實作方式也與具體引擎的動作管理系統相關;在 這裡就主要簡單介紹一下如何從FBX裡加載骨骼以及蒙皮資訊并完成最基本的蒙皮動畫效果。骨骼動畫的實作主要包括骨骼的驅動和蒙皮兩部分操作,骨骼的驅動 在前一篇中介紹動畫資料的加載時已經完成了,接下來就是對于Mesh與Skeleton之間的Skinning操作。

我們知道,骨骼動畫其實就是通過更新較少量的 Skeleton,進而實作對關聯到這些骨骼上的Mesh的更新,在每幀間都進行這樣的更新并做合适的插值與融合就可以得到平滑流暢的動作效果了。通過前 面基本幾何和動畫資料(Skeleton和Mesh)的加載已經有了這兩部分必要資訊,接下來就需要對兩者進行關聯進而實作Skinning時的正确映 射。這一部分資料的讀取其實還是以Mesh為機關進行的,其層次關系結構圖如下所示:

基于FBX SDK的FBX模型解析與加載

其中的Mesh可從目前屬性為eMESH的Node結點 中獲得(與讀取幾何網格資料相同),其可能是構成整個模型的網格的一小部分(Sub-Mesh)。若目前的Mesh中含有相應的蒙皮動畫資料,則可以從其 中讀取出全部的Vertex到Skeleton的映射資訊。Mesh中的蒙皮資料由一個或多個KFbxDeformer來管理,KFbxDeformer 是類型為KFbxTakeNodeContainer的一個對象。每個Deformer管理目前Mesh中的部分頂點到Skeleton的映射關系,而這 種映射關系的組織方式又分為兩種不同的形式,因而就有了派生自Deformer的KFbxSkin和KFbxVertexCacheDeformer(一 般情況下隻需考慮KFbxSkin的方式)。每個Skin(Deformer)中可能對應到多個頂點,這些頂點又可能映射到多個Skeleton,在 Skin(Deformer)中的每個Skeleton對應着一個Cluster。如此一來,通過在每個Cluster(->Skeleton)中 尋找其所影響到的Vertex,得到相應的聯接資訊如映射Matrix、骨骼Weight等并做相應的存儲即可完成Skeleton到Mesh之間的映射 蒙皮。另外注意:Vertex和Skeleton之間的關系是多對多,即一個Vertex可能受多個Skeleton影響,而一個Skeleton又可能 影響到多個Vertex;這些關系在設計資料結構時就應該有所注意。該部分的代碼大體如下所述:

  1. void AssociateSkeletonWithCtrlPoint(KFbxMesh* pMesh , CSkeletonMgr* pSkeletonMgr , List<VertexSkeletonList>& ctrlPointSkeletonList)
  2. {
  3. if(!pMesh || !pSkeletonMgr)
  4. {
  5. return;
  6. }
  7. int ctrlPointCount = pMesh->GetControlPointsCount();
  8. int deformerCount = pMesh->GetDeformerCount();
  9. // 初始化相應的清單
  10. ctrlPointSkeletonList.SetCapacity(ctrlPointCount);
  11. ctrlPointSkeletonList.setListSize(ctrlPointCount);
  12. KFbxDeformer* pFBXDeformer;
  13. KFbxSkin* pFBXSkin;
  14. for(int i = 0 ; i < deformerCount ; ++i)
  15. {
  16. pFBXDeformer = pMesh->GetDeformer(i);
  17. if(pFBXDeformer == NULL)
  18. {
  19. continue;
  20. }
  21. // 隻考慮eSKIN的管理方式
  22. if(pFBXDeformer->GetDeformerType() != KFbxDeformer::eSKIN)
  23. {
  24. continue;
  25. }
  26. pFBXSkin = (KFbxSkin*)(pFBXDeformer);
  27. if(pFBXSkin == NULL)
  28. {
  29. continue;
  30. }
  31. AssociateSkeletonWithCtrlPoint(pFBXSkin , pSkeletonMgr , ctrlPointSkeletonList);
  32. }
  33. }
  1. void AssociateSkeletonWithCtrlPoint(KFbxSkin* pSkin , CSkeletonMgr* pSkeletonMgr , List<VertexSkeletonList>& ctrlPointSkeletonList)
  2. {
  3. if(!pSkin || !pSkeletonMgr)
  4. {
  5. return;
  6. }
  7. KFbxCluster::ELinkMode linkMode = KFbxCluster::eNORMALIZE;
  8. KFbxCluster* pCluster;
  9. KFbxNode* pLinkNode;
  10. int skeletonIndex;
  11. CSkeleton* pSkeleton;
  12. KFbxXMatrix transformMatrix , transformLinkMatrix;
  13. int clusterCount = pSkin->GetClusterCount();
  14. // 處理目前Skin中的每個Cluster(對應到Skeleton)
  15. for(int i = 0 ; i < clusterCount ; ++i)
  16. {
  17. pCluster = pSkin->GetCluster(i);
  18. if(!pCluster)
  19. {
  20. continue;
  21. }
  22. pLinkNode = pCluster->GetLink();
  23. if(!pLinkNode)
  24. {
  25. continue;
  26. }
  27. // 通過Skeleton管理器搜尋到目前Cluster所對應的Skeleton,并與Cluster進行關聯
  28. skeletonIndex = pSkeletonMgr->FindSkeleton(pLinkNode->GetName());
  29. // ... //關聯Skeleton與Cluster
  30. if(skeletonIndex < 0)
  31. {
  32. continue;
  33. }
  34. pSkeleton = pSkeletonMgr->GetSkeleton(skeletonIndex);
  35. // 得到每個Cluster(Skeleton)所對應的Transform和TransformLink矩陣,後面具體說明
  36. pCluster->GetTransformMatrix(transformMatrix);
  37. pCluster->GetTransformLinkMatrix(transformLinkMatrix);
  38. // 其它适宜的操作,将Transform、TransformLink轉換為映射矩陣并存儲到相應的Skeleton中
  39. // ...
  40. int associatedCtrlPointCount = pCluster->GetControlPointIndicesCount();
  41. int* pCtrlPointIndices = pCluster->GetControlPointIndices();
  42. double* pCtrlPointWeights = pCluster->GetControlPointWeights();
  43. int ctrlPointIndex;
  44. // 周遊目前Cluster所影響到的每個Vertex,并将對相應的資訊做記錄以便Skinning時使用
  45. for(int j = 0 ; j < associatedCtrlPointCount ; ++j)
  46. {
  47. ctrlPointIndex = pCtrlPointIndices[j];
  48. ctrlPointSkeletonList[ctrlPointIndex].AddSkeleton(skeletonIndex , pCtrlPointWeights[j]);
  49. }
  50. }
  51. }

上述代碼隻是完整代碼的一部分,因其中涉及的大多數操作都與具體的實作系統相關,這裡隻列出部分以供參考而己。其中有兩個操作

pCluster->GetTransformMatrix(transformMatrix);

pCluster->GetTransformLinkMatrix(transformLinkMatrix);

需要特别說明一下,兩個操作分别得到兩個Matrix,前者transformMatrix(記為Mt)用來描述目前映射時刻(初始的映射狀态下)Mesh的變換矩陣(頂點的變換矩陣),

後者transformLinkMatrix(記為Mtl)用來描述目前映射時刻Bone的變換矩陣(可以參考kfbxcluster.h中的說明)。假設通過目前的Cluster可以關聯頂點V和骨骼B,而其對應的空間變換矩陣分别為MV、MB,因而有

MV = Mt; MB = Mtl

而在Mesh到Skeleton的蒙皮中需要由Skeleton的空間位置變換得到Mesh(頂點)的空間位置,是以就需要這樣一個變換矩陣M使得

基于FBX SDK的FBX模型解析與加載

通過簡單的變換即可得到

基于FBX SDK的FBX模型解析與加載

該M在動畫更新時就可以用來做Skeleton到Mesh之間的映射計算。

然後,即可以通過Skeleton的更新而完成對Mesh的更新,進而得到對整個模型的動畫。比如下列圖所示的一套動作:

基于FBX SDK的FBX模型解析與加載
基于FBX SDK的FBX模型解析與加載
基于FBX SDK的FBX模型解析與加載
基于FBX SDK的FBX模型解析與加載

9. 其它一些問題

雖然FBX SDK提供了對FBX模型的很友好的操作接口,但是目前的釋出版本也有一些相應的問題:

  • FBX SDK提供的FBXImporter目前不支援中文路徑,因而提供的fbx源檔案位址中應不含有中文字元。
  • 3D Max或Maya中的FBX導出插件計算得到的Tangent會一些問題,特别是在那些具有對稱屬性UV的部位。
  • 導出的具有Smooth特性的Normal也會在某些網格接口處出現不平滑的現象。

後兩個問題某些情況下的影響會比較嚴重,但是既然已經将原始的幾何資料加載到自己的引擎中了,因而也就可以在引擎中對Tangent與Normal進行再計算。

前述内容介紹了使用FBX SDK來對FBX進行加載時涉及到的比較常見的操作,如加載網格、材質以及動畫等,也給出了部分實作的代碼,但畢竟不同的系統對各種資源(如 Animation、Skeleton、Material等)有不同的管理方法,代碼也不能完全直接使用,适宜地修改是必不可少的。而且其中的錯誤也是難 免的,是以上述介紹内容隻作為參考,具體的實作還需要好好研究與參考Autodesk的相關doc。