AiRobotNews 人工智能机器人技术网

Opus 5.5 写的 Blender Python,是给另一个 Blender 写的——而渲染图不会告诉你

Opus 5.5 写的 Blender Python,是给另一个 Blender 写的——而渲染图不会告诉你

模型在 Blender 里建 3D 模型,翻车的地方有两处,都不是几何本身。一是 API 在它的训练数据底下挪走了;二是真正要命的缺陷,渲染图上看不出来。

下面按顺序讲这两件事怎么修。

Change constants in the parameter block only. Do not restructure, do not
rename, do not add geometry.

Apply exactly: <discrepancy list>
Return a diff.

一、是两个失败,不是一个

四套 Blender 构建上跑 30 个脚本任务的成绩对比

一次评测拿 30 个脚本任务,对着四套 Blender 版本、十个模型,每个脚本都在无头模式下执行,每个任务带断言。一共 1,190 份判分结果。

跑得通的脚本:Blender 3.6 上 91%,5.0 上掉到 70%。模型还能正确说出「哪儿改了」的比例:从 81% 掉到 35%。

有三类任务在 5.0 上对所有模型都是零分——合成器(compositor)、布尔求解器、序列编辑器。

最清楚的信号是 EEVEE 那个标识符。它先是 BLENDER_EEVEE,4.2 改成 BLENDER_EEVEE_NEXT,5.0 又改回 BLENDER_EEVEE。学过这次改名的模型现在反而错,没学过的模型瞎猫碰上死耗子,通过了。

这不是推理问题,是靶子问题。

二、先钉版本,再钉别的

命令行里固定版本与无头执行参数的写法

现在的最新版是 Blender 5.2.2 LTS,另一条还在维护的线是 4.5。Python 在 5.1 从 3.11 跳到 3.13,而 bpy 的 wheel 只针对某一个 Python 版本编译——没有「差不多就行」这回事。

所以要在提示词里写明版本,然后让每次运行自己证明这一点:

blender --background --factory-startup \
        --python-exit-code 1 \
        --python build.py -- --out ./dist --mm 20

两个细节,各能让人浪费一天。不加 --python-exit-code,脚本里的 traceback 不会改变 Blender 的退出码——CI 会在崩溃的时候亮绿灯。参数顺序也有讲究:-o 要放在 blend 文件之后,否则加载文件时会把你的输出路径覆盖掉。-- 之后的所有内容才作为 sys.argv 传给 Python。

三、算子(operator)是给界面用的,脚本该写数据

算子调用与数据层写法的区别

Blender 自己的文档写明了限制:算子不接受数据,只返回「做完了没有」;poll 失败会抛 RuntimeError: ... context is incorrect,而且不带原因,因为解释这套机制的部分「目前用得不多」。

在无界面的后台运行里,每一次 bpy.ops.mesh.* 都在等一个还不存在的编辑网格上下文。

模型通常这么写:

bpy.ops.object.modifier_apply(modifier="Boolean")

能在无头运行里活下来的是这种:

dg   = bpy.context.evaluated_depsgraph_get()
mesh = obj.evaluated_get(dg).to_mesh()

布尔求解器值得单说,因为默认值本身就是坑。FLOAT 的文档写着「不支持重叠几何」;MANIFOLD「只在流形网格上工作」;只有 EXACT 能扛住乱七八糟的输入。把生成的几何丢给默认求解器,不算 bug——那是文档写明的行为。

四、那个躲过所有 review 的比例 bug

三条常识叠在一起,造成了大部分无声的 100 倍误差:

大家以为的实际成立的
primitive_cube_add() 生成 1 单位立方体它生成 2 单位立方体,size= 是整条边长
unit_settings.scale_length = 0.001 表示「按毫米工作」只影响显示。几何、物理和导出都不看它
导出器会带上你的单位STL 和 OBJ 不带任何单位——要自己传 use_scene_unit 或 global_scale

glTF 是例外,也正因为它才存在这条约定:规范强制要求米和 Y 轴朝上,导出器根本没有比例选项。Blender 单位 1:1 出去,于是「1 单位 = 1 米」被悄悄定死了。

所以在脚本最上面,把约定大声定一次:1 个 Blender 单位就是 1 米,20 毫米的零件写成 0.02。

MM = 0.001

五、体检:渲染图答不了的十一个问题

非流形、零面积面这类缺陷在渲染图里完全看不出来

非流形边渲染得完美。零面积面、自相交、内部空腔、比喷嘴还薄的壁,也一样完美。Blender 自己的 MCP 文档里记过一个例子:模型漏掉了一个 Solidify 修改器,多边形的数量其实翻了倍——因为那个修改器不影响视口。

先量,再看:

import bmesh
bm = bmesh.new(); bm.from_mesh(obj.data)

nonmanifold = sum(1 for e in bm.edges if not e.is_manifold)
flipped     = sum(1 for e in bm.edges if e.is_manifold and not e.is_contiguous)
loose       = sum(1 for v in bm.verts if not v.link_faces)
degenerate  = sum(1 for f in bm.faces if f.calc_area() <= 1e-4)

from mathutils.bvhtree import BVHTree
tree = BVHTree.FromBMesh(bm, epsilon=1e-5)
selfhit = len(tree.overlap(tree))

assert (nonmanifold, flipped, loose, degenerate, selfhit) == (0, 0, 0, 0, 0)
assert abs(obj.dimensions.x - 0.020) < 1e-5     # it is the size you asked for

出了 Blender,trimesh 给的是另一个属性,它才真正代表「这是个实体」:is_volume——封闭、绕序一致、法线朝外,三者同时成立。单看 is_watertight 要弱一些,而在一个根本不是实体的网格上,体积这个数字没有意义。

写脚本之前还有一件要知道的事:3D-Print Toolbox 已经不再随 Blender 打包,现在是社区扩展,无头流水线得自己装。它的默认值——最小厚度 1 毫米、悬垂角 45°——恰好等于打印圈的共识,但前提是一个单位就是一米。

六、闭环,以及驱动它的四段提示词

六视图拼成一张接触表,用来做人眼判断

渲染是为了做接触表,不是为了下结论。实用的做法是用离屏 GPU 绘制,而不是跑完整渲染——六个命名视角拼进一张图。两个约束要记住:Cycles 不会往离屏视图里画任何东西,所以要用 EEVEE 或 Workbench;Blender 5.2 专门加了 gpu.init(),让 --background 下也能拿到 GPU 后端。

SYSTEM:

Target Blender 5.2 LTS, Python 3.13. Do not use any API removed before it.

Rules:
- bpy.data and bmesh only; no bpy.ops.mesh.* and no bpy.ops.object.*
  except where I have named one
- one Blender unit is one metre; declare MM = 0.001 and use it
- every dimension, clearance and wall thickness is a named module-level
  constant, never inline
- boolean solver is EXACT unless I state the inputs are manifold
- end the script with the census asserts, not with a render
- after the code, list every API you used that changed in 4.0 or later
  and state which release you are targeting for each

GENERATE:

Build the part described below as a single Python file.

<spec: outer dimensions, wall, clearances, holes, tolerance, export path>

Output the parameter block first as a table. I will assert against it.

CRITIQUE——这一段要拿六视图那张图当输入:

Six orthographic views of one part: front, back, left, right, top,
three-quarter. The census already passed, so do not comment on topology.

Judge only what geometry cannot: proportion against the stated dimensions,
whether features sit where the spec says, parts that read as floating or
fused, and anything present that the spec did not ask for.

List discrepancies against: <parameter table>

PATCH:

Change constants in the parameter block only. Do not restructure, do not
rename, do not add geometry.

Apply exactly: <discrepancy list>
Return a diff.

MCP 这边简单说:现在有一个官方 Blender Lab MCP Server(插件 v1.0.3,需要 Blender 5.1+),它自己的页面写着它会「在 Blender 里执行 LLM 生成的代码,且没有任何防护」,建议放到虚拟机里跑。社区那个服务器有大约 28.7k star,在 localhost:9876 上讲裸 TCP,没有认证也没有加密,还背过一个路径穿越 CVE(1.6.5 修掉)。两个都当开发机工具看待。

七、生成的网格、打印参数,和一次循环的成本

从生成模型导入后的修复链条

如果你是从生成式 3D 模型导入的网格,体检要更狠,不是更松。2026 年的前沿反而在离开「水密」这个目标:TRELLIS.2 把非流形几何和开放曲面当卖点——对衣服和植被是对的,对任何要打印的东西就是错的。主流的托管模型没有一个给水密保证。

导入之后,脚本化的链条按顺序走:校验、体检、用 bmesh.ops 修(remove_doubles、recalc_face_normals、holes_fill、dissolve_degenerate)、修比例和朝向,然后才做布尔,最后如果还想要能用的拓扑,再上 quadriflow_remesh 或 voxel_remesh。

打印数字这块:四份公开指南在几乎每个数字上都差两到三倍。它们都同意的是四个:悬垂角 45°、最小孔径 Ø2.0 毫米、装配间隙 0.5 毫米、最小壁厚大致 1.0–1.2 毫米。把这四个作为命名参数传进去,所有有争议的留成带默认值的参数。写死在生成器里的常数,对一台打印机是对的,对下一台就是错的。

Opus 5.5,每百万 token
输入$4
输出$20
缓存命中$0.20
批处理输入 $2 · 输出 $10

一次循环是:一段缓存的 system 块、一份规格、一次生成、一次无头运行、一次体检、一次数值补丁。成本是几美分。Blender 那次运行花的是几秒钟。

也就是说,版本钉死和那十一个断言不算额外开销。它们是「看起来写完了的脚本」和「零件真的在那儿」之间唯一的两道墙。

资料来源