BlenderWorld 的任务分层、Agent 从单物体建模到整场景组装的能力跨度

文章导读
总分只能说明 Agent 在这一批任务上的平均表现,不能说明 Agent 是在单物体建模阶段就无法完成,还是在整场景组装阶段失分。要判断标题里说的能力跨度,可行的做法是把同一批任务按场景复杂度切成几层,每层单独运行、单独记录失败点,再看失败类型在哪一层发生变化。下面的分层标准、记录字段和检查脚本都可以在本地 .blend 场景文件与 Agent 日志上直接执行;阈值属于人为约定,需要结合自己的任务
📋 目录
  1. 一 把任务清单按场景复杂度分组
  2. 二 逐层跑分并记录失败类型
  3. 三 在单物体任务里检查几何闭合与尺寸
  4. 四 在组合场景任务里检查相对位置与层级
  5. 五 按层写出能力边界结论
A A

总分只能说明 Agent 在这一批任务上的平均表现,不能说明 Agent 是在单物体建模阶段就无法完成,还是在整场景组装阶段失分。要判断标题里说的能力跨度,可行的做法是把同一批任务按场景复杂度切成几层,每层单独运行、单独记录失败点,再看失败类型在哪一层发生变化。下面的分层标准、记录字段和检查脚本都可以在本地 .blend 场景文件与 Agent 日志上直接执行;阈值属于人为约定,需要结合自己的任务集确认。

总分看不出 Agent 卡在哪一层,要看失败类型随场景复杂度的迁移:单物体层看几何闭合与尺寸,组合场景层看相对位置与父子层级,复杂场景层看灯光与依赖管理。做法是按物体数、材质数、灯光数切三层,逐层运行并记录失败类型,再读取 .blend 中的 dimensions、parent 等字段与任务清单比对。风险边界:分层阈值是人为约定,换任务集需重新校准,单层结论不能外推。

把任务清单按场景复杂度分组

分组依据建议用三个可数的量:物体数、去重后的材质数、灯光数。这三个量都能从任务清单或场景文件里直接读到,不依赖人工主观判断,也不依赖渲染结果。下面是一组保守的初始阈值,可以先用它跑通流程,再按自己的任务集调整:

  • L1 单物体:物体数 ≤ 1,材质数 ≤ 1,灯光数 0
  • L2 组合场景:物体数 2–8,材质数 2–6,灯光数 1–2
  • L3 复杂场景:物体数 > 8,或材质数 > 6,或灯光数 > 2,或存在需要指定父子的挂载关系

分组脚本骨架如下,放在任务清单同目录执行。脚本默认任务清单里每个物体带 type 和 materials 字段;如果你的清单字段名不同,改这两处取值即可,脚本不对物体做几何检查,只做计数分层。

import json

def tier_of(task):
    objs = len(task['objects'])
    mats = len({m for o in task['objects'] for m in o.get('materials', [])})
    lights = sum(1 for o in task['objects'] if o.get('type') == 'LIGHT')
    if objs <= 1 and mats <= 1 and lights == 0:
        return 'L1_single'
    if objs <= 8 and mats <= 6 and lights <= 2:
        return 'L2_composite'
    return 'L3_complex'

tasks = json.load(open('tasks.json', encoding='utf-8'))
for t in tasks:
    t['tier'] = tier_of(t)
json.dump(tasks, open('tasks_tiered.json', 'w', encoding='utf-8'),
          ensure_ascii=False, indent=2)
print(sorted({t['tier'] for t in tasks}))

如果任务清单本身没有物体级字段,只有提示词文本,就不要强行用脚本分组;这类任务集应改为在跑完 Agent 后,直接对输出场景文件统计物体、材质和灯光数量,再按统计值归层。两种做法得到的分层结果可能不同,需要在结论里写明是按输入清单分层还是按输出场景分层。

逐层跑分并记录失败类型

分层之后,同一个 Agent 在每层上各跑一遍,失败类型要分得足够细,否则所有问题都会被归成“生成失败”,看不出能力边界。建议先按下面的分类表打标签,一条任务只记主失败类型,并在备注里写次生现象。

失败类型典型表现主要出现在
无输出脚本报错、超时、没有产出 .blend全层
物体缺失清单要求的物体在场景中找不到L2 / L3
几何不闭合存在非流形边、面法线方向不一致L1
尺寸越界dimensions 与规格偏差超出容差,或缩放未应用L1
材质错误材质槽为空、材质数量不符、贴图路径丢失L2 / L3
位置偏移物体间相对间距明显偏离描述L2
层级错误应挂载的物体挂在场景根节点L2 / L3
穿插重叠包围盒相交且无合理接触语义L2 / L3
灯光缺失或过曝场景无灯、灯数量不符、渲染整体溢出L3

记录字段建议至少包含这些,用 CSV 或 JSONL 都行,保证每一行能回溯到原始日志:

task_id, tier, object_count, material_count, light_count,
status, failure_type, first_error_step, elapsed_sec,
blend_path, log_path, render_path, note

其中 first_error_step 指 Agent 轨迹里第一次出现异常或开始偏离要求的那一步,取值可以是步骤编号或工具名。这一列是后面判断“卡在哪一步”的主要依据,比总的成败标记更有用。

在单物体任务里检查几何闭合与尺寸

L1 层的目的很窄:确认基础建模能力是否成立。检查项按下面顺序过一遍,任何一项不通过就记为 L1 失败,不要用后面的渲染效果掩盖前面的几何问题。

BlenderWorld 的任务分层、Agent 从单物体建模到整场景组装的能力跨度
  • 网格非空:len(obj.data.polygons) > 0
  • 无非流形边:逐条检查 edge.is_manifold
  • 法线方向一致:整体执行一次向外重算后,观察面的朝向是否变化
  • 尺寸在容差内:obj.dimensions 三个分量与提示词给出的规格比对,容差建议先设 5%–10%
  • 变换已应用:obj.scale 三个分量接近 1,否则 dimensions 的语义容易混乱
  • 原点位置合理:obj.location 与预期摆放点一致,避免“尺寸对但整体飘着”

在场景文件中读取这些字段,可以写一个独立脚本,用无界面模式执行:blender `--background` scene.blend `--python` inspect_object.py。

import bpy, bmesh

obj = bpy.data.objects['Cube']
print('dimensions', tuple(round(v, 4) for v in obj.dimensions))
print('scale', tuple(round(v, 4) for v in obj.scale))
print('location', tuple(round(v, 4) for v in obj.matrix_world.translation))
print('polygons', len(obj.data.polygons))

bm = bmesh.new()
bm.from_mesh(obj.data)
non_manifold = [e.index for e in bm.edges if not e.is_manifold]
print('non_manifold_edges', len(non_manifold))
print('volume', round(bm.calc_volume(signed=False), 6))
bm.free()

需要提醒的是,calc_volume 只有在网格闭合时才有物理意义,非流形边不为零时不要拿体积当判断依据。另外单体建模常见的隐性失败是“看起来像但内部有多层壳”,这类问题在非流形边和体积两项上通常会同时暴露,值得交叉看一眼。

在组合场景任务里检查相对位置与层级

L2 层考察的是场景组装:物体各自合格,摆放和挂载关系是否符合描述。matrix_world.translation 给出的是世界坐标,用世界坐标比较相对间距比用局部坐标更稳。层级关系则看 parent 字段,并结合任务清单里期望的挂载语义判断。

import bpy

def dump(o, depth=0):
    p = o.parent.name if o.parent else None
    loc = tuple(round(v, 3) for v in o.matrix_world.translation)
    print('  ' * depth + o.name + ' parent=' + str(p) + ' loc=' + str(loc))
    for c in o.children:
        dump(c, depth + 1)

for o in bpy.context.scene.objects:
    if o.parent is None:
        dump(o)

一次典型输出示例如下,可以直接和任务清单对照:清单要求两个杯子放在桌面上,输出里 Cup_01、Cup_02 都带 parent=Table 且 z 值高于桌面平面,说明挂载与接触关系成立;如果两个杯子的 parent 显示为 None,即便坐标看起来合理,也应记为层级错误,因为后续整体移动桌面时杯子不会跟随。

Table parent=None loc=(0.0, 0.0, 0.75)
  Cup_01 parent=Table loc=(0.12, -0.05, 0.79)
  Cup_02 parent=Table loc=(0.28, -0.05, 0.79)
Lamp parent=None loc=(0.0, 0.6, 1.65)

穿插检查可以先做粗筛:对每两个物体计算世界空间包围盒是否相交,相交的再人工看渲染图或切面图确认是否合理接触。这个粗筛会误报(比如杯子放在桌面上包围盒必然相交),所以只作为线索,不作为最终判定。

按层写出能力边界结论

每层写一句话,句子里必须包含任务集、层阈值和证据来源,否则结论无法被引用。模板如下,把尖括号部分替换成实际内容即可;没有跑到的层就写“未覆盖”,不要用推断补上。

L1 单物体(阈值:物体≤1、材质≤1、无灯光):在 <任务集名称> 上,Agent 的失败集中在 <几何不闭合 / 尺寸越界 / 无输出>,证据为 inspect 脚本输出的 non_manifold_edges 与 dimensions 两列。
L2 组合场景(阈值:物体 2–8、材质 2–6、灯光 ≤2):失败类型从几何问题转向 <位置偏移 / 层级错误>,证据为层级打印结果与任务清单 parent 期望值的逐条比对记录。
L3 复杂场景(阈值:物体>8 或多灯光):主要失败为 <物体缺失 / 灯光缺失 / 无输出>,证据为失败记录表 failure_type 分布与渲染图抽查。

写结论时有两条边界要守住。第一,分层阈值是人为设定,同样一个 Agent 换一套阈值可能落到不同层,结论里必须带上阈值,否则不可比。第二,L3 通常任务数量少、单条耗时长,样本很小,结论宜写成“在该任务集上观察到”而不是普遍判断。如果确实需要更强结论,做法是补充同类任务数量再重跑,而不是把现有结果说得更满。