Clayman's Graphics Corner

DirectX,Shader & Game Engine Programming

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Working with FBX SDK (2)

仅供个人学习使用,请勿转载,勿用于任何商业用途

作者:clayman

 

更新2012.5:  *****fbx sdk 2013以后的版本做了大幅更新,大量API都进行了修改和更名,本文不再适用,请参考新版SDK文档******

   上一篇文章介绍了fbx sdk的基本用法,接下来我们继续讨论如何取得normal,tangent,binormal和uv信息。先介绍一些关于KFbxLayer对象的概念。KFbxLayer对象是一个容器,对mesh来说,它包含了除控点,多边形信息以外大部分数据,比如normal,tangent,vertex color,uv等等。一个mesh可以包含多个KFbxLayer对象,不同layer之间的元素类型,个数通常都不相同。下面是一个简单的mesh结构关系:

mesh ---- layer 0 { KFbxLayerElementNormal, KFbxLayerElementTangent, KFbxLayerElementUV…..}

      |

      |------layer 1 {KFbxLayerElementUV………}

      |  

      |-- ………………..

      |

      |-- layer n

 

         虽然FBX允许有多层layer,但很多软件包括maya和max都只处理包含在第一个layer中的normal等数据! 每种保存在KFbxLayer的元素都继承于KFbxLayerElement,比如KFbxLayerElementNormal对应normal数据,KFbxLayerElementTangent对应tangent的数据。可以通过KFbxLayer中定义的各种Get函数,返回需要的KFbxLayerElement,如果为空,则说明当前layer中没有这种元素。KFbxLayerElement还中包含了两个非常重要的属性KFbxLayerElement::EMappingMode和KFbxLayerElement::EReferenceMode。


     MappingMode定义了当前类型的元素如何映射到mesh上。举例来说,对于KFbxLayerElementNormal,eBY_POLYGON_VERTEX表示如果一个顶点被n个多边形共享,那么这个顶点就有n条法线与之相对应;eBY_CONTROL_POINT则表示每个顶点无论被几个多边形共享,都只有一条normal;eBY_POLYGON则表示构成多边形的n个顶点只对应着一条normal。某些MappingMode只对特定的KFbxLayerElement有效,请详细参考文档。通常对于有hard edge的模型来说,MappingMode只能是eBY_POLYGON_VERTEX,而平滑模型则可以是eBY_CONTROL_POINT。

 

      ReferenceMode定义了如何访问相关的数据。同样举例来说,每个KFbxLayerElement内部通常可能包含两个数组,分别称为DirectArray和IndexArray。如果reference mode为eDIRECT,则第i个控点相对的element元素就在DirectArray的第i位置(第i个控点的normal在KFbxLayerElementNormal.DirectArray[i]中) ,此时IndexArray为空。eINDEX_TO_DIRECT通常和eBY_POLYGON_VERTEX一起使用,因为一个控点可能对应多个值,所以这时必须用多边形顶点索引(也就是GetPolygonVertexCount()返回的值)来获得某个多边形顶点所对应的值: KfbxLayerElement.DirectArray[ IndexArray[vertexIndex]]。下面代码演示了如何遍历所有layer,获得每个顶点/控点对应的法线:

Get normal
KFbxLayerElementNormal* leNormal = pMesh->GetLayer(0)->GetNormals();
if (leNormal)
{
    
switch (leNormal->GetMappingMode())
    {
        
case KFbxLayerElement::eBY_CONTROL_POINT:
            
switch (leNormal->GetReferenceMode())
            {
            
case KFbxLayerElement::eDIRECT:
                KFbxVector4 normal 
= leNormal->GetDirectArray().GetAt(lControlPointIndex));
                
break;
            
case KFbxLayerElement::eINDEX_TO_DIRECT:
                {
                    
int id = leNormal->GetIndexArray().GetAt(lControlPointIndex);
                    KFbxVector4 normal 
= leNormal->GetDirectArray().GetAt(id));
                }
                
break;
            
default:
                
break// other reference modes not shown here!
            }
            
break;

        
case KFbxLayerElement::eBY_POLYGON_VERTEX:
            {
                
//polygonID = triange 1,2,3.....n
                
//positionId = 1,2,3 for triange 
                //vertex!!!! 
                int vertexIndex = pMesh->GetPolygonVertex(polygonID,positionId);
                
switch (leNormal->GetReferenceMode())
                {
                    
case KFbxLayerElement::eDIRECT:
                    
case KFbxLayerElement::eINDEX_TO_DIRECT:
                        {
                            Display2DVector(header, leUV
->GetDirectArray().GetAt(vertexIndex))
                        }
                        
break;
                    
default:
                        
break// other reference modes not shown here!
                }
            }
            
break;
    }
}

 

        为了避免混淆,再强调一下控点和顶点的区别。首先,控点只包含位置信息,顶点则包含了位置,法线,纹理坐标等信息。如果mesh中所有layer中的所有元素MappingMode都是eBY_CONTROL_POINT,则控点数量和顶点一一对应。如果是eBY_POLYGON_VERTEX,则有可能需要分裂控点。比如一个控点被n个多边形共享,则对应着n条法线,需要分裂成n个顶点,但是,控点所对应的n条法线中有些可能是相同的(none hard edge)------所以eBY_POLYGON_VERTEX通常和eINDEX_TO_DIRECT配合使用----- 因此最终分裂出来的顶点数有可能小于n。Tangent,bionormal,vertex color的访问与此类似,而且一般来说,只需要读出第一个layer中的数据即可。如何根据不同的normal等信息分裂控点,组合顶点,需要我们自己来实现,这里不详细讨论。

 

         UV的访问方式和上面提到的方法类似,但稍稍有些区别。前面说过,虽然每个mesh都允许多个layer,但通常只会有一组normal,tangent等数据,uv则可能有多组(比如一组uv用于普通贴图,另外一组用于lightmap),并且有可能保存在同一layer中,也有可能分别保存在多个layer中。但是fbx文件中有一个奇怪的问题,很多模型虽然只有一组UV,但会被识别出多组UV出现在不同layer中,并且不是每个layer中存在的数据都相同或者有效!!

        

       上图中,第一个文件是正确的,2组UV分别在两个layer中;下面的文件则多出了2层只含UV的layer,注意多余的uv名称都是map1. FBX论坛上好像有人也遇到了同样的问题,不过都没有官方的解释,文档中也没有讨论。解决方法是我们可以通过检查每组UV的名称来确定某组UV是否是重复:

代码
foreach layer
{
    
int uvSetCount = layer->GetUVSetCount();
    
if(uvSetCount > 0)
    {
        
//iterate all uv channel indexed by element_texture_type
        for (int textureIndex = KFbxLayerElement::eDIFFUSE_TEXTURES;textureIndex<KFbxLayerElement::eLAST_ELEMENT_TYPE;textureIndex++)
        {
            KFbxLayerElement 
*uvElement = layer->GetUVs(KFbxLayerElement::ELayerElementType(textureIndex));
           
if(!uvElement)
                
continue;
            uvSetsName 
=  uvElement->GetName();
           
if(!CheckUVSetsNameExists(uvSetsName))
            {
             
//process uv data
            }
         }
    }
}

 

         GetUVs (KFbxLayerElement::ELayerElementType type) 返回对应type类型的UV,不存在则返回NULL。这里的type是KFbxLayerElement::ELayerElementType枚举中eDIFFUSE_TEXTURES 到eDISPLACEMENT_TEXTURES 之间的值。可以把这个枚举理解为UV的通道标识符,比如GetUVs(eDIFFUSE_TEXTURES)返回diffuse texture通道的纹理,注意,这里eDIFFUSE_TEXTURES并不指这组UV只能用于diffuse map,而只是一个标识符!对于只有一组uv的模型来说,纹理数据通常都在这个通道中。

 

         我们已经基本解析出模型中的几何信息。接下来看如何获得材质,特别是纹理信息。与前面的元素不同,material不保存在layer中,而是保存在node里,一个node可以包含多个材质。SDK文档中关于材质,纹理之间关系的介绍非常让人迷惑,有些接口也很常奇怪。虽然Layer中有一个名为GetMaterials()的方法,但其返回的KFbxLayerElementMaterial对象中GetDirectArray()只会返回空值,也就是说无法通过它获得真正表示材质的KFbxSurfaceMaterial对象。下面的代码展示了如何取得材质,以及相应的数值类参数。

代码
for(int lIndex=0; lIndex < lNode->GetMaterialCount() lIndex++)
{
    KFbxSurfaceMaterial 
*lMaterial = lNode->GetMaterial(lIndex)
    
//convert to proper sub type
    if(lMaterial->GetClassId().Is(KFbxSurfaceLambert::ClassId) )
    {
        ((KFbxSurfaceLambert 
*)lMaterial)->GetAmbientColor()
        ((KFbxSurfaceLambert 
*)lMaterial)->GetDiffuseColor()
        ...
    }
    
else if(lMaterial->GetClassId().Is(KFbxSurfacePhong::ClassId))
    {
        ((KFbxSurfacePhong 
*) lMaterial)->GetAmbientColor()
        ((KFbxSurfacePhong 
*) lMaterial)->GetDiffuseColor()
        ...
    }

 

        逻辑上来说,KFbxSurfaceMaterial其实是个抽象类,需要把它转换为合适的两个子类,才能得到实际材质参数。纹理则要更特别一些(注意,虽然layer中也有GetTextures(),但我测试的时候总返回空值)。一个材质会包含多个纹理通道,每个通道同样以KFbxLayerElement::ELayerElementType中关于纹理的枚举作为标识符,每个通道可以包含多个KFbxTexture或者KFbxLayeredTexture,其中,KFbxTexture就对应着一张纹理,而KFbxLayeredTexture则又包含了多个KFbxTexture对象,类似如下结构:

KFbxSurfaceMaterial : contains one or more textureProperty, identified by KFbxLayerElement::ELayerElementType

textureProperty : contains one or more texture/layerTexture;

layerTexture: contains more than one texture

 

         下面的代码展示了如何获得纹理信息。

Get Texture
void FbxImporter::ParseMaterial(KFbxNode* fbxNode,HamsterEngine::Node *node)
{
    
//iterate all material
    for (int i = 0;i<fbxNode->GetMaterialCount();i++)
    {
        KFbxSurfaceMaterial 
*mat = fbxNode->GetMaterial(i);
        
if(mat)
        {
            
//iterate all texture channel
            for (int textureIndex = 0;textureIndex< KFbxLayerElement::LAYERELEMENT_TYPE_TEXTURE_COUNT;textureIndex++)
            {
                
//get current texture channel
                KFbxProperty property = mat->FindProperty(KFbxLayerElement::TEXTURE_CHANNEL_NAMES[textureIndex]);
                
//has channel?
                if(property.IsValid())
                {
                    
//layered texture?
                    if(layerCount > property.GetSrcObjectCount(KFbxLayeredTexture::ClassId);)
                    {
                        
//iterate all layered texture
                        for (int layerId=0;layerId<layerCount;layerId++)
                        {
                            KFbxLayeredTexture 
*layeredTex = KFbxCast<KFbxLayeredTexture>(property.GetSrcObject(KFbxLayeredTexture::ClassId,layerId));
                            
int numTex = layeredTex->GetSrcObjectCount(KFbxTexture::ClassId);
                            
//iterate all texture in this layer
                            for (int texId=0;texId<numTex;texId++)
                            {
                                KFbxTexture
* tex = KFbxCast<KFbxTexture>(layeredTex->GetSrcObject(KFbxTexture::ClassId,texId));
                                
if(tex)
                                {
                                    std::cout
<<"Texture  name:"<<tex->GetName()
                                        
<<"    fileName:"<<tex->GetFileName()
                                        
<<"    uvSet:"<<tex->UVSet.Get();
                                }
                            }
                        }
                    }
                    
else
                    {
                        
int numTextures = property.GetSrcObjectCount(KFbxTexture::ClassId);
                        
////iterate all simple texture
                        for (int texId = 0;texId<numTextures;texId++)
                        {
                            KFbxTexture 
*tex = KFbxCast<KFbxTexture>(property.GetSrcObject(KFbxTexture::ClassId,texId));
                            
if(tex)
                            {
                                std::cout
<<"Texture  name:"<<tex->GetName()
                                    
<<"    fileName:"<<tex->GetFileName()
                                    
<<"    uvSet:"<<tex->UVSet.Get();
                            }
                        }
                    }
                }
            }
        }
    }
}

 

        KFbxTexture.UVSet.Get()返回当前纹理所绑定的UVSet名称,可以由此获得纹理和UV的绑定关系。

       之前所说的KFbxLayerElementMaterial并不是完全没用,还必须用它获得mapping mode,对材质来说,最常见的两个值是:eALL_SAME和eBY_POLYGON,前者表示整个mesh的材质都相同,没太多可说的;后者表示材质只应用到mesh中的部分多边形,这就比较麻烦了,上图中第二个文件就是这种情况。不同材质意味着纹理或者shader改变,我们必须把eBY_POLYGON的mesh根据材质划分为不同子mesh才能导入到DirectX程序中。幸运的是sdk提供了这样的函数,让我们不用自己计算:

KFbxGeometryConverter lConverter(pSdkManager)
lConverter.SplitMeshPerMaterial(lMesh) 

 

 注意:

before and after the call to SplitMeshPerMaterial, you should see a difference in the number: there will be the old mesh, plus one new mesh (node attribute) for each material.

It will work only on mesh that have material mapped “per-face” (Mapping Mode is KFbxLayerElement::eBY_POLYGON). It does NOT work on meshes with material mapped per-vertex/per-edge/etc.It will create as many meshes on output that there are materials applied to it.If one mesh have some polygons with material A, some polygons with material B,and some polygons with NO material, it should create 3 meshes after calling this function.The newly created meshes should be attached to the same KFbxNode that hold the original KFbxMesh.The original KFbxMesh STAY UNCHANGED.Now, the new mesh will have Normals, UVs, vertex color, material and textures.

 

         以上介绍了模型导入时从fbx文件中提取,常见数据的方法,也还有很多方面没有讨论,比如skin info和animation。对skin来说,相应的权重等信息保存在KfbxDeformer对象中,可以通过KFbxNode获得。至于动画目前我暂时还没有时间研究,如果有好心人实现了,不妨也写篇教程顺便告诉我一声:)。另外最先说过,fbx是一种可扩展的格式,可以通过UserProperties属性添加很多自定义属性,这里介绍了如何在maya和max中添加自定义属性,SDK中的UserProperties sample则介绍了如何取得这些属性。文章中所涉及的函数只介绍了基本用法,详细信息请参考文档。另外文档中虽然没有太多示例代码,但sdk中附带的ImportScene Sample是一个非常好的例子,展示了解析fbx文件的方方面面,值得仔细研究。

 

 

posted on 2010-12-11 04:07  clayman  阅读(10676)  评论(19编辑  收藏  举报