Python FBX SDK踩坑记录

lotte-de-brabander-shot02

Python FBX SDK 的安装

我们首先要在官网上下载Python FBX SDK

image-20211029110110820

下载之后打开文件会得到编译好的fbx.pyd文件

image-20211029110315973

把这三个文件放到相对应的Python site-packages文件夹里面,你就会发现用不了,踩的第一个坑。后面查阅了各路大佬的资料,说要编译自己对应Python版本的fbx.pyd才能使用,我看了一下有两种方法进行编译:

1.智伤帝大佬的一个一个手动编译的高难度方法(我没成功复刻,不知道哪里出了问题)

指路地址

2.懒人的救星!手残党的救世主!一键自动编译!按照操作说明一步一步来就行了,非常的方便快捷

指路地址

把编译的文件放到site-packages文件夹,终于把环境弄好了

image-20211029111140035

FBX结构分析

在使用FBX SDK时,最先遇到的是FbxSdkManagerFbxScene

FbxSdkManage是sdk的中心类,负责整个sdk的内部状态,很多其他类的创建也需要FbxSdkManage,在程序中只需要一个FbxSdkMange实例即可。

FbxScene代表着一个场景,包含着fbx文件的所有信息,一个fbx文件只需要一个FbxScene实例

FBX的数据组织方式

FBX的数据组织方式是场景树,根节点包含一系列的子节点FbxNode,每个子节点又有自己的子节点,这样的好处是可以通过循环递归遍历到每一个节点。

# 获取场景的根节点
pNode = pScene.GetRootNode()

大概的结构如下

preview

层次Layer

法线,纹理坐标等都是存储在层次Layer里面的,每个节点都能有多个层次,每个层次都能包含一套纹理,法线。

每种保存在Layer的元素(比如UV)都继承于FbxLayerElement,FbxLayerElementNormal对应Normal数据,FbxLayerElementUV对应UV数据。

FbxLayerElement中有两个非常重要的属性EmappingMode和EReferenceModeMappingMode定义了当前类型元素如何映射到mesh上。

MappingMode

总共有三种类型:

1.MappingMode = eBY_POLYGON_VERTEX表示如果一个顶点被n个多边形共享,那个这个顶点就有n条法线信息。

2.MappingMode = eBY_CONTROL_POINT表示每个顶点无论被n个多边形共享,都只有一条法线信息

3.MappingMode = eBY_POLYGON表示构成多边形的n个顶点只对应这一条法线信息

ReferenceMode

ReferenceMode定义了如何访问相关数据。一个FbxLayerElement内部通常包含着两个数组DirectArray和IndexArray

总共有两种类型:

1.ReferenceMode = eDIRECT,第n个顶点相对的element元素就在DirectArray的第n个位置,此时IndexArray为空。

2.ReferenceMode = eINDEX_TO_DIRECT通常和eBY_POLYGON_VERTEX一起搭配使用,因为一个控点对应着多个值。

Mesh节点

mesh存储了模型的重要数据,包含顶点颜色,UV,法线,顶点坐标等等信息。由RenderDoc导出的模型一般都是三角面片。

顶点坐标

Mesh有控制点(Control point)和顶点(Polygon point)两个概念。控制点只包含位置信息,顶点通过索引可以得到

控制点和顶点的关系:根据MappingMode来决定,

1.eBY_POLYGON_VERTEX 则需要分裂控制点,一个控制点可以出现在多个三角面上作为顶点,一个控制点对应多个顶点,通常这个更为常用

2.eBY_CONTROL_POINT 控制点与顶点的数量一一对应。

控制点和顶点概念解释这个解释得更加通俗一点

动手动手!

通过了解了FBX模型的代码层结构,我们就可以动手了解决问题

QtDesigner制作工具界面

我们需要一个好看的界面,界面参考Wanderson大佬的界面风格,大致的使用方法就是选择csv文件,并读入相对应的列标题

界面参考

image-20211103192449260

Widget拖拽界面

因为为了美观,界面隐藏了Window的菜单栏

# 隐藏菜单栏
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)

这时候会出现无法拖拽界面的问题,我们可以通过以下代码实现Widgets拖拽界面的效果

        # 顶部栏拖拽绑定
        self.frame_label_left.mouseMoveEvent = self.moveWindow
        self.frame_label_left.mousePressEvent = self.mousePress

    # 记录拖拽按下的Pos坐标位置
    def mousePress(self, event):
        self.dragPos = event.globalPos()

    # 顶部拖拽事件
    def moveWindow(self, event):
        # 判断是否为左键点击
        if event.buttons() == QtCore.Qt.LeftButton:
            self.move(self.pos() + event.globalPos() - self.dragPos)
            self.dragPos = event.globalPos()
            event.accept()

FBX SDK使用

首先创建必要的相关属性,像Manager和Scene,Node,Mesh

        # fbx相关创建
        fbxManager = FbxManager.Create()
        fbxScene = FbxScene.Create(fbxManager, "")
        rootNode = fbxScene.GetRootNode()
        newNode = FbxNode.Create(fbxScene, "myNode")
        FbxNode.AddChild(rootNode, newNode)

        # 创建一个Mesh
        mesh = FbxMesh.Create(fbxScene, "TestMesh")
        newNode.SetNodeAttribute(mesh)

接下来就是读取csv数据,并进行重新排序

        # 获取顶点的总数
        vertexCount = len(DataFrame)

        # 去重
        DataFrame_change = DataFrame.drop_duplicates(subset=[" IDX"], keep="first", inplace=False)
        # 排序
        DataFrame_change = DataFrame_change.sort_values(by=" IDX", ascending=True)
        # 获取最小顶点索引
        minIndex = int(DataFrame_change.iloc[0][" IDX"])
        # 初始化控制点
        mesh.InitControlPoints(len(DataFrame_change))

接下来For循环往Mesh里面填数据就好,以Pos为例

        if (self.checkComboBox(self.comboBox_pos)):
            print("填入pos数据")
            # 利用切换进行最后一个字符的替换
            pos_x = self.comboBox_pos.currentText()
            pos_y = pos_x[:-1] + "y"
            pos_z = pos_x[:-1] + "z"
            for i in range(0, len(DataFrame_change)):
                pos.append(np.array([DataFrame_change.iloc[i][pos_x], DataFrame_change.iloc[i][pos_y],
                                     DataFrame_change.iloc[i][pos_z]]))

            # FbxLayer
            for i in range(0, len(DataFrame_change)):
                # 对应的点索引
                # pointIndex = int(DataFrame.iloc[i][" IDX"])
                # 重组位置数据
                vertexPos = FbxVector4(pos[i][0], pos[i][1], pos[i][2])
                mesh.SetControlPointAt(vertexPos, i)

            # 重构三角面
            for j in range(0, int(vertexCount / 3.0)):
                # 将vertex与point相互绑定
                mesh.BeginPolygon(j)
                mesh.AddPolygon(int(DataFrame.iloc[j * 3][" IDX"]) - minIndex)
                mesh.AddPolygon(int(DataFrame.iloc[j * 3 + 1][" IDX"]) - minIndex)
                mesh.AddPolygon(int(DataFrame.iloc[j * 3 + 2][" IDX"]) - minIndex)
                mesh.EndPolygon()

最后就是导出,导出需要FBX export,我就是直接复制了官方的FBX例子的函数方法进行了导出

        # FBX导出
        saveScene(saveFileName, fbxManager, fbxScene, pAsASCII=True)
        fbxManager.Destroy()
        del fbxManager, fbxScene, DataFrame

        print("导出成功")

相关函数,引用地址

def saveScene(pFilename, pFbxManager, pFbxScene, pAsASCII=False):
    ''' Save the scene using the Python FBX API '''
    exporter = FbxExporter.Create(pFbxManager, '')

    if pAsASCII:
        # DEBUG: Initialize the FbxExporter object to export in ASCII.
        asciiFormatIndex = getASCIIFormatIndex(pFbxManager)
        isInitialized = exporter.Initialize(pFilename, asciiFormatIndex)
    else:
        isInitialized = exporter.Initialize(pFilename)

    if (isInitialized == False):
        raise Exception('Exporter failed to initialize. Error returned: ' + str(exporter.GetStatus().GetErrorString()))

    exporter.Export(pFbxScene)

    exporter.Destroy()


def getASCIIFormatIndex(pManager):
    ''' Obtain the index of the ASCII export format. '''
    # Count the number of formats we can write to.
    numFormats = pManager.GetIOPluginRegistry().GetWriterFormatCount()

    # Set the default format to the native binary format.
    formatIndex = pManager.GetIOPluginRegistry().GetNativeWriterFormat()

    # Get the FBX format index whose corresponding description contains "ascii".
    for i in range(0, numFormats):

        # First check if the writer is an FBX writer.
        if pManager.GetIOPluginRegistry().WriterIsFBX(i):

            # Obtain the description of the FBX writer.
            description = pManager.GetIOPluginRegistry().GetWriterFormatDescription(i)

            # Check if the description contains 'ascii'.
            if 'ascii' in description:
                formatIndex = i
                break

    # Return the file format.
    return formatIndex

最终效果

从平安京截取的模型,带有UV通道。

image-20211103194637329

引用参考

1.UE4.26 FBX Python Binding 编译

2.用代码控制模型,python for fbx,顶点法线修复的实现。

3.读取Fbx文件中的信息

4.官方PYTHON FBX SDK文档

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2021 Opda
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信