python fbx sdk 读取面部 blendshape 系数

这个功能一直想实现,但是没有能力,现在终于实现了~

主要代码

自行下载编译 python fbx sdk

主要参考的是一个 c++ 的实现
以及 chatgpt 的代码提示(错误非常多,但是也有点帮助)

注意: 代码的一些"超参"和特定的fbx结构有关,如果有问题可以分析自己的fbx 数据稍作修改,就比如选择第几个 mesh,以及 layer 之类的。 最后要除以100。

import fbx
from fbx import *
import FbxCommon
import numpy as np
import tqdm

# 主要的参考代码
# https://github.com/metarutaiga/FBXSDK/blob/350d9856c9f560a3817df915bf99fd937c011174/samples/ViewScene/DrawScene.cxx#L308C6-L308C6
fbx_file_path="xxx.fbx"
fblendshape="xxx.npy"

#fbx_file_path="coco_talking01_01_05.fbx"
#fblendshape="coco_talking01_01_05.npy"

lSdkManager, lScene = FbxCommon.InitializeSdkObjects()
lResult = FbxCommon.LoadScene(lSdkManager, lScene, fbx_file_path)

AnimStackType = fbx.FbxCriteria.ObjectType(fbx.FbxAnimStack.ClassId)
nbAnimStacks = lScene.GetSrcObjectCount(AnimStackType)

print(f"'{nbAnimStacks}' anim stacks found.")
for i in range(nbAnimStacks):
    lAnimStack = lScene.GetSrcObject(AnimStackType, i)
    stackName = lAnimStack.GetName()
    print("stackName", stackName, i) # 'Unreal Take'

AnimLayerType = fbx.FbxCriteria.ObjectType(fbx.FbxAnimLayer.ClassId)
nbAnimLayers = lAnimStack.GetSrcObjectCount(AnimLayerType)

for i in range(nbAnimLayers):
    lAnimLayer = lAnimStack.GetSrcObject(AnimLayerType, i)
    layerName = lAnimLayer.GetName()
    print("layerName", layerName,i) # 'Base Layer'

tSpan = lAnimStack.GetLocalTimeSpan()
t = tSpan.GetStart()
frameBegin = t.GetFrameCount()

t = tSpan.GetStop()
frameEnd = t.GetFrameCount() + 1

frameIds = list(range(frameBegin, frameEnd))
_time = fbx.FbxTime()

pRoot = lScene.GetRootNode()

def isMesh( pNode: fbx.FbxNode):
    #_type=fbx.FbxNodeAttribute.EType.eMesh
    _type=fbx.FbxNodeAttribute.eMesh
    return pNode.GetNodeAttribute() and pNode.GetNodeAttribute().GetAttributeType() == _type

def walk(pNode):
    mesh_nodes = []
    if isMesh(pNode):
        mesh_nodes.append(pNode)

    for idx in range(pNode.GetChildCount()):
        pChild = pNode.GetChild(idx)
        mesh_nodes += walk(pChild)

    return mesh_nodes

mesh_nodes = walk(pRoot)
print( mesh_nodes )
pNode = mesh_nodes[0] 
print( pNode, pNode.GetNodeAttributeCount() )
mesh = pNode.GetNodeAttribute()
print( mesh )
lBlendShapeChannelCount = mesh.GetDeformerCount(FbxDeformer.eBlendShape)
print("BlendShape deformerCount: ", lBlendShapeChannelCount)

for lBlendShapeIndex in range(lBlendShapeChannelCount):
    blendshape_deformer = mesh.GetDeformer(lBlendShapeIndex, FbxDeformer.eBlendShape) 

blendshape_channel_count = blendshape_deformer.GetBlendShapeChannelCount()
print(f"BlendShape channel count: {blendshape_channel_count}")

blenshape_data=np.zeros( (len(frameIds), blendshape_channel_count), np.float32 )
for i, frameId in tqdm.tqdm(enumerate(frameIds)):
    _time.SetFrame(frameId)
    frameStamp = _time.GetSecondDouble()
    #print( "frameStamp", frameStamp )
    pTime=_time

    for channel_index in range(blendshape_channel_count):
        channel = blendshape_deformer.GetBlendShapeChannel(channel_index)
        channel_name = channel.GetName()
        #print(f"Channel name: {channel_name}")
            
        lFCurve = mesh.GetShapeChannel(lBlendShapeIndex, channel_index, lAnimLayer);
        if not lFCurve: 
            print("Warn: ", channel_name, "not exist")
            continue 
        
        lWeight = lFCurve.Evaluate(pTime);
        #print("lWeight", lWeight) # ??? 为什么会有两个值
        
        blenshape_data[i,channel_index] = lWeight[0]
        # 注意这里有个知识点要看上面那个链接,但是这里的 target shape 只有一个

        target_shape_count = channel.GetTargetShapeCount()
        #print(f"Target shape count: {target_shape_count}")

np.save(fblendshape, blenshape_data / 100 )

注意事项

最初有一部分是这么实现的

deformerCount = mesh.GetDeformerCount()
print("deformerCount: ", deformerCount)
for idx in range(deformerCount):
    deformer = mesh.GetDeformer(idx)
    deformerType = deformer.GetDeformerType()
    if deformerType ==  FbxDeformer.eBlendShape:
        blendshape_deformer = deformer
        lBlendShapeIndex = idx
        break

发现 lBlendShapeIndex 不对,因为后面GetShapeChannel用的 idx 其实是 blendshape 的 idx ,比如有2个deformer ,但是第二个才是 blendshape,就会引发问题。 (需要的是 0 ,给的是 1)

补充一个blender 的实现方式

(同事写的)也需要根据实际情况来修改,不过速度比较慢。

import bpy
import os
import numpy as np
import sys
print(sys.executable)

fbx_filename = str(sys.argv[-2])
bs_filename = str(sys.argv[-1])

print(f'fbx filename:{fbx_filename}')
print(f'bs filename: {bs_filename}')

# 导入动画文件
bpy.ops.import_scene.fbx(filepath=fbx_filename)
print('import fbx successfully')

action = bpy.data.actions[1]
frame_range = action.frame_range


# 获取包含Blendshape系数的形状关键帧数据
bs_names = []
shape_key_frames = []
for fcurve in action.fcurves:
    if fcurve.data_path.startswith('key_blocks['):
        bs_names.append(fcurve.data_path.replace('key_blocks["', '').replace('"].value', ''))
        shape_key_frames.append(fcurve.keyframe_points)

bs_num = len(shape_key_frames)
frame_num = len(shape_key_frames[0])
# 导出Blendshape系数
with open(bs_filename, 'w') as file:
    for frame_idx in range(frame_num):
        print(f'frame idx: {frame_idx}')
        for bs_idx in range(bs_num):
            coefficient = shape_key_frames[bs_idx][frame_idx].co[1]
            file.write(f"{coefficient} ")
        file.write('\n')

你可能感兴趣的:(python,开发语言,Fbx,SDK)