本文由 Codex 根据五一期间的聊天记录整理撰写,内容经我指定主题、结构和写作约束后生成。
五一使用 Codex 做 Blender MMD 自动化的一次尝试五一期间,我把一部分时间花在了 Blender、MMD 模型和 Codex 的组合上。起点很简单,我想知道一个 agent 能不能帮我把 MMD 相关的重复操作脚本化。以前这些事情主要靠手工流程完成:打开 Blender,导入 PMX,套 VMD,找材质,调描边,试渲染,再根据画面一点点修。真正做起来以后,我发现这次尝试的重点逐渐落在一条可以反复运行、可以检查中间结果、可以迁移到远程机器上的流程。
这篇记录只写主要脉络。很多中间图、调试图和临时版本没有必要全部展开,这些属于工作台上的草稿。真正有价值的是我怎么把 Blender 里的隐式操作变成脚本,怎么让 Codex 在错误反馈里继续推进,以及哪些地方暴露出 agent 使用上的边界。
从读取 blend 开始一开始我问的是一个很小的问题:test.blend 里有什么文件。这个问题看似只是查看内容,实际给后续所有自动化打了底。我需要读取 .blend 内部的数据块,包括场景、集合、对象、网格、材质、图片、外部库、文本块。
Codex 先用 Blender 4.5 后台打开文件,然后写了一个 inspect_blend.py。这个脚本能在命令行里列出 .blend 内部结构,也能显示网格顶点数、对象材质、图片是否被打包。之后我又让 Codex 检查 Blender 4.5 装了哪些插件,于是有了 inspect_blender_addons.py。这一步确认了两个关键插件:官方 mmd_tools 和本地的 mmd_tools_append。
真正的突破点来自继续读 mmd_tools 的源码。MMD 模型导入对应 bpy.ops.mmd_tools.import_model,动作导入对应 bpy.ops.mmd_tools.import_vmd。这些名字找出来以后,MMD 的 GUI 操作就有了脚本入口。我让 Codex 写了一个导入 PMX 和 VMD 的脚本,并用 Furina 的 PMX 做了真实导入测试。第一次完整导入成功以后,我对这件事的判断变了:Blender MMD 自动化的核心问题,是把一串按钮变成稳定的工程流水线。
例如模型和动作导入最后就是这样的调用。这里会先选中目标 MMD root,再让 mmd_tools 把 VMD 应用到当前对象上,同时过滤掉当前 Blender 版本不支持的参数。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def import_vmd_motion (vmd_path, root, scale=0.08 , frame_margin=10 ): select_only(root) directory = os.path.dirname(vmd_path) filename = os.path.basename(vmd_path) kwargs = { 'directory' : directory, 'files' : [{'name' : filename}], 'scale' : scale, 'margin' : frame_margin, 'bone_mapper' : 'PMX' , 'use_pose_mode' : False , 'update_scene_settings' : True , } supported = { prop.identifier for prop in bpy.ops.mmd_tools.import_vmd.get_rna_type().properties } return bpy.ops.mmd_tools.import_vmd( **{key: value for key, value in kwargs.items() if key in supported} )
把一次搭场景变成流水线接下来我把目标扩大到完整 AutoMMD 项目。我准备了模型、场景、动作、表情、相机和参考 .blend,要求 Codex 从 project/autommd/blank.blend 开始,每完成一个阶段就保存一个中间 .blend。
这个约束非常重要。Blender 自动化最怕一条脚本跑到底,最后只得到一个失败文件或一张坏图。中间结果让问题可以定位:到底是舞台导入错了,角色导入错了,动作没套上,表情没绑定,材质重建错了,还是相机和渲染设置出了问题。
这条流水线后来变成 src/scene/project/assemble_autommd.py。脚本依次清理 blank,导入舞台,导入角色,调用 mmd_tools.morph_slider_setup(type='BIND') 把表情变形绑定到 .placeholder,再把舞蹈 VMD、facial VMD、BGM 和相机 VMD 接进场景。角色描边优先调用 mmd_tools.edge_preview_setup(action='CREATE'),按材质拆分则保留成可选参数,因为这个操作会改变模型结构。
表情绑定和刚体烘焙来自插件源码里的入口。这一步把插件按钮背后的 operator 变成了可以重复调用的脚本。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def setup_morph_placeholder (root ): select_only(root) result = bpy.ops.mmd_tools.morph_slider_setup(type ='BIND' ) if 'CANCELLED' in result: raise RuntimeError('mmd_tools morph bind cancelled' ) placeholder = bpy.data.objects.get('.placeholder' ) if placeholder is None : placeholders = [ obj for obj in bpy.data.objects if obj.name.startswith('.placeholder' ) ] placeholder = placeholders[-1 ] if placeholders else None return placeholder def bake_with_mmd_operator (): bake_op = getattr (bpy.ops.mmd_tools, 'ptcache_rigid_body_bake' , None ) if bake_op is None : raise RuntimeError('mmd_tools rigid body bake is not available' ) return bake_op()
材质是最花时间的一块。我给了一个参考文件 伊涅芙人生重启.blend,希望能观察里面的节点配置。Codex 扩展了 inspect 脚本,把参考材质节点导出成 JSON。参考文件里有 Face、Body、Hair 等不同节点组,还有 lightmap、shadow ramp、rim 和 fresnel 相关参数。实际落地时,我选择按脸、皮肤、头发、眼睛、衣服、金属、舞台等类别重建本地可维护的 toon 节点。
材质节点的创建方式也逐渐固定下来。下面这段是简化后的核心逻辑:用 Diffuse BSDF 接 ShaderToRGB,再用 ColorRamp 做 toon 阴影,最后接到 Emission。法线图、贴图、rim 和透明混合都沿着这个基础继续加节点。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 tree = material.node_tree nodes = tree.nodes links = tree.links nodes.clear() output = nodes.new('ShaderNodeOutputMaterial' ) diffuse = nodes.new('ShaderNodeBsdfDiffuse' ) shader_to_rgb = nodes.new('ShaderNodeShaderToRGB' ) ramp = nodes.new('ShaderNodeValToRGB' ) emission = nodes.new('ShaderNodeEmission' ) ramp.color_ramp.elements[0 ].position = 0.45 ramp.color_ramp.elements[0 ].color = shadow_color ramp.color_ramp.elements[1 ].color = lit_color links.new(diffuse.outputs['BSDF' ], shader_to_rgb.inputs['Shader' ]) links.new(shader_to_rgb.outputs['Color' ], ramp.inputs['Fac' ]) links.new(ramp.outputs['Color' ], emission.inputs['Color' ]) links.new(emission.outputs['Emission' ], output.inputs['Surface' ])
这一步的成果是 05_final.blend,还有一组可回溯的中间文件。这些文件记录了空场景到最终自动搭建的路径,也让我后续可以基于同一条流水线替换角色,例如 SW999 和 Linaiya。
这张图来自后续反复调整后的 Furina 预览。它能代表这条流水线最终想解决的问题:模型、动作、材质、描边、灯光和相机都由脚本稳定落到一个可继续调整的 .blend 里。
渲染脚本和远程机器本地能跑以后,我很快遇到另一个现实问题:渲染应该放到适合的机器上。于是五一期间的另一个主线变成远程渲染。
第一步是资源打包。Blender 的 .blend 常常引用外部贴图、声音、字体、缓存或 library。如果直接把文件丢到 Linux 服务器,远端很容易缺资源。Codex 写了 pack_external_resources.py,核心使用 Blender 自己的 bpy.ops.file.pack_all(),再加上外部引用扫描和缺失资源检测。
第二步是后台渲染。render_blend_linux.py 支持单帧和动画渲染,可以设置输出路径、分辨率、采样、引擎和格式。随后我又追问 mmd_tools 的烘焙按钮能不能脚本化,于是补了 bake_mmd_rigid_body.py。这个脚本先调用 mmd_tools.ptcache_rigid_body_bake,后台上下文不合适时再走 Blender point cache 的兜底路径。我用短区间 1 到 2 帧验证过,刚体缓存可以在后台完成烘焙。
本地预览和远程渲染的底层调用都绕不开 bpy.ops.render.render。静帧渲染会设置当前帧、输出格式和路径,然后调用 write_still=True。1 2 3 4 5 6 7 8 scene = bpy.context.scene scene.render.engine = 'BLENDER_EEVEE_NEXT' scene.frame_set(args.frame) scene.render.image_settings.file_format = 'PNG' scene.render.filepath = str (Path(args.output).resolve()) print (f'Rendering frame {scene.frame_current} to: {scene.render.filepath} ' )bpy.ops.render.render(write_still=True )
第三步是把这些拼成远程渲染流程。后来形成了 scripts/remote_render:本地打包,上传到 spark,远端 tmux 跑 Blender,实时跟日志,完成后把结果、日志、manifest 和 status.env 下载回来。实际试跑时还暴露了几个很真实的问题。spark 上的 Blender 命令来自 zsh alias,非交互 bash 读不到,需要直接指定 /app/blender/bin/blender。Windows 传到远端的脚本要处理 CRLF,避免 bash 参数里混进回车。Blender 5.0.1 对 MP4/FFMPEG 输出枚举和旧脚本不兼容,Python 异常时进程退出码也可能还是 0,所以渲染脚本必须主动在异常时返回非零。
远程第 120 帧当时大概是这样提交的。命令本身很普通,背后串起了打包、上传、远端执行、日志跟踪和结果回传。1 2 3 4 5 6 .\scripts\remote_render\Invoke-RemoteRender .ps1 ` -Server spark ` -Blend .\project\autommd\05 _final.blend ` -Mode frame ` -Frame 120 ` -Output renders/05 _final_frame_0120.png
远程第 120 帧成功以后,我又让 Codex 渲染前 10 帧,并加上耗时统计。日志里能看到远端 GPU probe 使用了 NVIDIA/OpenGL,后来脚本也补了 render engine、输出格式、Cycles device 和可见设备的明确记录。这个阶段让我感到 agent 适合做工程脚手架:一次手工 ssh 渲染,可以被整理成可重复使用的工具链。
这张图是 spark 远程渲染后回传的第 120 帧。它对我来说是一张链路验证图:本地打包、远端 Blender、GPU、tmux 日志和结果下载都跑通了。
材质调试的难点流程脚本主要考验 API 和工程耐心,材质调试需要视觉判断。SW999 的调试就很典型。身体材质大量叫 Body.xxx,泛用分类一开始把露肤部分也归成 cloth,导致皮肤发白。后来我用材质调试图定位,发现红色区域对应的 Body 才是主要露肤材质,于是把这一组纳入 body_skin。
这个调试图会临时把指定材质替换成纯色 Emission,再渲一张图。它能回答一个很具体的问题:画面里这块颜色到底来自哪个材质槽。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def make_debug_material (name, color ): material = bpy.data.materials.new(name) material.use_nodes = True nodes = material.node_tree.nodes links = material.node_tree.links nodes.clear() output = nodes.new('ShaderNodeOutputMaterial' ) emission = nodes.new('ShaderNodeEmission' ) emission.inputs['Color' ].default_value = color links.new(emission.outputs['Emission' ], output.inputs['Surface' ]) return material for slot in obj.material_slots: if slot.material and slot.material.name in debug_colors: slot.material = make_debug_material( f'Debug_{slot.material.name} ' , debug_colors[slot.material.name], )
中间还有一次误判。我以为举起的前臂变成了白色皮肤,于是继续分析 メツN.png 的 alpha,尝试用透明区域做皮肤 mask,又检查 Body.006、Body.008 的贴图来源,甚至做了把 Body.006 整块当皮肤的测试。最后我意识到那块白色区域本来就是衣服,另一只手的肤色已经正常。这个插曲很有意思,因为 Codex 能沿着我的假设快速做验证,假设本身仍然需要我用画面和常识校正。
Linaiya 的材质研究复杂一些。这个模型包里既有发布者提供的 PMX、贴图、normalmap 和 MME 配置,也有我从游戏里提取的 DDS、vertex buffer 文本和 index buffer 文本。我让 Codex 先研究文件格式,再尽量利用 game_extracted。Codex 解析 DDS 头,识别 DX10 BC7 格式,读取 vb0 和 ib 文本里的 position、normal、tangent、UV、index 信息,并把 faceA 和 wingsA 重建成单独的调研 .blend。
在 PMX 材质侧,Linaiya 的 profile 接入了发布包里的 normalmap,也把游戏提取的 face normal、material map、unknown map 和 wings diffuse 接进节点。这里我没有把游戏 diffuse 粗暴替换到所有 PMX 材质上,因为 UV、alpha 和区域含义并不一定一致。我采用了保守做法:把能确认用途的贴图弱接入,保留材质属性和节点元数据,再通过预览判断这些贴图对画面的贡献。
后面我又把镜头改成竖屏,做结尾正脸特写,并尝试让眼睛跟随相机。第一次驱动写入后眼睛方向不对,原因是计算基于世界轴,头骨转动后方向判断会偏。修正以后,驱动改成基于头骨局部轴向,并删除原眼部旋转曲线,最终能让 目.L 和 目.R 更自然地朝向镜头。
偏黄问题的排查Linaiya 后期还有一个典型问题:画面整体偏黄。角色设定里鞋子是黑色,裙子是银白色,头发是粉色。我一开始怀疑 lift、ramp、灯光和后处理都可能有关系。
排查以后,主因落在材质 ramp 上。lift 数值很小,场景里确实有暖主光,角色主体走 Emission,灯光主要影响 toon ramp 的明暗分区,并不会像 PBR 材质那样直接把最终颜色乘黄。我让 Codex 渲了同一帧的暖灯和全白灯,差异很小,这基本排除了灯光作为主因。
真正麻烦的是二次重刷材质时的一个 bug。reapply_character_materials.py 重新应用 profile 时,base_image() 把之前加入节点的 normalmap 误当成主贴图,导致衣服和头发在诊断图里偏蓝偏紫。修复逻辑以后,脚本会优先使用保存过的 autommd_original_base_image,并跳过 normal、lightmap、ramp、unknown 这类非颜色贴图。随后我把 Linaiya profile 调回暖粉白方向,重新渲了一版完整竖屏视频。
另一个常用 debug 脚本是直接打印材质节点。它不看最终画面,只看节点树里到底用了哪些图片、色彩空间和 shader。偏黄、偏蓝、透明错误这类问题,最后基本都要回到这里确认。1 2 3 4 5 6 7 8 9 10 11 12 13 for mat in bpy.data.materials: if not mat.node_tree: continue print ('MAT' , mat.name, 'blend' , mat.blend_method) for node in mat.node_tree.nodes: if node.bl_idname == 'ShaderNodeTexImage' and node.image: print ( ' image' , node.name, node.image.name, node.image.filepath, node.image.colorspace_settings.name, )
这个问题让我对 agent 协作有了具体感受。只说画面偏黄太模糊,Codex 会检查代码和参数;给出鞋子、裙子、头发的目标颜色后,排查就能收敛到材质 ramp、贴图平均色、灯光测试和节点恢复逻辑。视觉问题需要把审美判断翻译成可检查的假设。
我对这次尝试的感受这几天最有价值的部分,是我看到了 agent 在 Blender 自动化里的合适位置。Codex 适合读插件源码,找 operator 名称,写后台脚本,补 CLI 参数,记录中间产物,追日志,修跨平台兼容问题。GPT 的多模态能力也确实很有用,渲染图出来以后,可以直接观察画面效果,再围绕皮肤、衣服、灯光、颜色和镜头继续调试。
在风格化渲染这件事上,距离我想象的状态仍有不足。白色袖子是否为皮肤,某张 unknown DDS 的通道到底代表什么,裙子应该是冷银白还是暖粉白,这些问题很难只靠代码确认。我需要不断把观察反馈给 Codex,再把反馈变成下一次实验。它能提升实验速度,风格判断本身还需要人来把方向说清楚。
这次我还想过另一个方向:能不能让 AI 进行编舞。例如输入一段音乐,自动根据音乐风格、节奏和台词设计舞蹈,再输出 VMD 或 FBX 这样的动作序列。我简单调研以后感觉,目前还没有很成熟的产品或模型。现在常见的是生成视频或生成概念结果,直接输出可编辑的人体动作序列还比较少。
这可能和舞蹈本身的复杂性有关。舞蹈要满足人体骨骼约束,要处理动作复杂性和创新性,本身还是一个时序问题。不同语义段里可能同时存在周期性和趋势性,音乐节奏、歌词含义、镜头表现和身体动作之间也存在复杂关系。
所以五一这次尝试给我的一个判断是,很多文艺创作里的自动化可能会走两条线并行。一条线是用编码脚本把套路化、重复性的东西自动化,例如导入、绑定、材质、渲染、打包和远程执行。另一条线是用生成模型解决偏创意的问题,例如风格参考、画面判断、动作构想和局部方案。把这两者结合起来,看起来是一条现实一些的路径。
五一这次尝试最后留下的东西不少:AutoMMD 的阶段式 .blend 生成流程,材质 profile,参考节点检查,MMD 刚体烘焙,远程渲染脚本,Linaiya 的游戏提取资源研究,还有一批渲染预览和视频。重要的是,我开始把 Blender MMD 制作理解成一个可编程的工程,手工调整也可以逐步沉淀为脚本和文档。Codex 在这里的价值,是把反复试错变成可以保存、可以重跑、可以继续扩展的流程。