这个功能一直想实现,但是没有能力,现在终于实现了~
自行下载编译 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)
(同事写的)也需要根据实际情况来修改,不过速度比较慢。
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')