用 text-to-3D 模型做完整 3D 世界的人几乎都踩过同一个坑:模型确实给你生成了一个带纹理的 GLB 文件,但你打开一看,整个场景是一个不可编辑的三角面 blob——没有区域划分,没有摄像机路径,没有可替换的资产实例,地形是一片随机噪声。你想把镜头飞过一座山的另一侧,但"另一侧"根本不存在。
2026 年 8 月 13 日,OpenMontage(我之前分析过的 Agentic 视频制作系统)合并了 commit 2702362——一次 3,979 行的新增,涵盖 42 个文件,标题是 feat: add production 3D world pipeline。这不是又一个"文生 3D"封装,而是一条完整的、可审计的 3D 世界生产管线。它的核心判断是:不要让 AI 生成整个世界,而要让 AI 规划世界,然后用确定性数学把规划物化成可编辑的 3D 场景。
这条管线解决什么问题
在 commit 2702362 之前,OpenMontage 的 2D 世界只有平面概念(卡片、图表、动画图形),缺一个真正的 3D 空间——不是单个产品模型的旋转展示,而是有地形起伏、有区域划分、有环境散布、有摄像机飞越的完整 3D 世界。
现有的 text-to-3D 模型(Atlas Cloud 的 tripo-h3.1、fal.ai 的 Hunyuan 3D v3.1)擅长生成单个物体,但管线明确指出:
Never ask a text-to-3D model to generate a whole cinematic world in one mesh. Generate hero objects, use licensed catalogs for high-volume scatter, and assemble everything in Blender from a semantic specification.
——来自 .agents/skills/3d-asset-generation/SKILL.md
这个判断是正确的。WorldClaw 论文(arXiv:2608.05248v1,2026)的思路也是如此:先建立结构化的场景规范,再逐层填充细节。区别在于 WorldClaw 的公开仓库只有论文没有可执行代码,而 OpenMontage 把这个架构思想变成了 5 个可调用的工具。
world_spec:Agent 创意与渲染引擎之间的语义契约
整条管线的核心是一个名为 world_spec 的 JSON 对象。它不是渲染配置文件,而是一份语义化的世界规范——告诉渲染引擎"这个世界里有什么",而不是"每帧画什么"。
区分显式约束和推断细节
管线首先要求 Agent 分离两类信息:
"explicit_constraints": ["a volcanic rift divides a living valley"],
"inferred_details": ["three semantic regions", "dawn atmosphere"],
explicit_constraints 只能放用户明确提出的事实;inferred_details 放 Agent 为了让世界可执行而补充的推断。这个分立在技能文档里被反复强调:
Never smuggle an inferred landmark, biome, or story beat into the explicit list.
这不是洁癖,而是为了保持可追溯性:当最终渲染的某个地貌与用户意图不符时,你能查到它到底来自用户的原始意图还是 Agent 的自由发挥。
world_spec 的完整结构
来自 .agents/skills/threejs-world-generation/references/world-spec.md 的最小示例:
{
"version": "1.0",
"title": "The Luminous Divide",
"seed": 2048,
"world": {
"size": 120,
"resolution": 160,
"elevation_scale": 18,
"water_level": -1.5
},
"atmosphere": {
"sky_color": "#07111f",
"fog_density": 0.009,
"sun_color": "#ffd7a3",
"sun_intensity": 3.2,
"sun_position": [45, 70, 20]
},
"regions": [
{
"id": "ember-rift",
"center": [-0.35, 0.12],
"radius": 0.58,
"landform": "ridge",
"frequency": 1.4,
"color": "#5b271f",
"scatter": {"rock": 90, "crystal": 22, "tree": 0},
"slope_limit": 1.8
}
],
"landmarks": [
{
"id": "rift-gate",
"type": "arch",
"region_id": "ember-rift",
"position": [-24, 0, 8],
"scale": 5.5
}
],
"camera_path": [
{"time": 0, "position": [72, 42, 72], "target": [0, 3, 0], "fov": 46},
{"time": 60, "position": [-54, 13, -32], "target": [-12, 4, 5], "fov": 40}
]
}
注意几件事:
- 区域坐标是归一化的
[-1, 1],而不是世界坐标。工具负责把归一化坐标转换成世界空间,这样 Agent 不需要关心世界的物理尺寸就能规划布局。 - 每个区域有自己的 landform 算子——7 种地形类型(plain、peak、ridge、dune、terrace、basin、canyon)各自定义了不同的高度场函数。
- 地标和摄像机路径是世界坐标,但
position[1]是相对地形的垂直偏移量,不是绝对 Y 坐标。运行时会采样该位置的地形高度并加上偏移量。
这套规范是整条管线的"空间契约"(spatial contract)。管线技能文档对它的定位是:
Create a persistent scene graph, not a sequence of unrelated 2D shots. Preserve the user's explicit constraints, infer missing construction details separately, establish the global terrain first, and refine selected regions without disturbing the world-wide spatial contract.
确定性地形生成:7 种地形算子 × 区域权重场
这是整条管线最巧妙的设计。地形高度不是从一张手绘 heightmap 读出来的,而是从区域权重场实时计算出来的纯数学函数。
区域权重场
threejs_world.py(732 行)中的 _region_weights 方法对每个点 (nx, nz) 计算它属于每个区域的权重:
@classmethod
def _region_weights(cls, spec, nx, nz) -> list[float]:
raw = []
for region in spec["regions"]:
dx = nx - region["center"][0]
dz = nz - region["center"][1]
radius = max(0.05, region["radius"])
distance = math.sqrt(dx*dx + dz*dz) / radius
softness = max(0.02, region["blend_width"])
value = math.exp(-max(0.0, distance - 0.05)**2 / (softness * 2.8))
raw.append(max(1e-5, value))
total = sum(raw) or 1.0
return [value / total for value in raw]
权重使用高斯衰减计算距离,然后归一化。关键在于:同一套权重被用于驱动高度、顶点颜色、散布密度和摄像机间隙检测。这意味着区域边界是连续过渡的,不会有硬接缝。
高度场函数
_height_at 方法将区域权重与地形算子结合:
@classmethod
def _height_at(cls, spec, x, z) -> float:
nx = x / (size / 2.0)
nz = z / (size / 2.0)
weights = cls._region_weights(spec, nx, nz)
seed = spec["seed"] * 0.01337
elevation = 0.0
for index, (region, weight) in enumerate(zip(spec["regions"], weights)):
frequency = region["frequency"]
noise = (
math.sin((nx * 3.1 + seed + index) * frequency * math.pi)
+ math.cos((nz * 2.7 - seed * 0.7 + index) * frequency * math.pi)
+ 0.5 * math.sin((nx + nz) * frequency * 7.3 + seed * 3.0 + index)
) / 2.5
landform = cls._landform(region["landform"], dx, dz, distance)
elevation += weight * (
region["base_elevation"]
+ region["amplitude"] * (noise * 0.48 + landform * 0.8)
)
return elevation * spec["world"]["elevation_scale"]
噪声函数用的是三组正弦/余弦叠加——不需要 Perlin/Simplex 噪声库,纯数学运算,且给定 seed 后完全确定性可复现。这一点至关重要:同一份 world_spec 在任何机器上运行,得到的地形逐顶点完全一致。
7 种地形算子
@staticmethod
def _landform(kind, dx, dz, distance) -> float:
if kind == "peak": return max(0.0, 1.0 - distance) ** 2.2
if kind == "ridge": return max(0.0, 1.0 - abs(dx * 1.8 + math.sin(dz * 5.0) * 0.16))
if kind == "dune": return (math.sin((dx + dz * 0.25) * 18.0) + 1.0) * 0.24
if kind == "terrace": return math.floor(max(0.0, 1.0 - distance) * 5.0) / 5.0
if kind == "basin": return -max(0.0, 1.0 - distance) ** 1.7
if kind == "canyon": return -max(0.0, 1.0 - abs(dx + math.sin(dz * 7.0) * 0.1)) ** 2.0
return 0.0 # plain
每个算子是一个独立的数学函数,定义了该区域的宏观地形特征。terrace 用 floor 制造阶梯效果,canyon 用负指数制造深谷,dune 用高频正弦制造沙丘起伏。这些函数足够简单,但效果区分度很高——至少在 blockout 级别足够用。
我的判断是:这种基于参数化算子的地形系统,在写实度上远不如 Houdini 或 World Machine 的 Erosion 节点网络,但它的优势在于完全确定性、零外部依赖、可在浏览器里实时计算。对于 Agentic 视频生产这种需要反复迭代和审计的场景,确定性比写实度更重要。
区域感知的资产散布
地形建好后,下一个问题是:树木、岩石、水晶该放在哪里?
管线不允许随机散布。scatterPoints 函数(运行在浏览器端 world-runtime.js 中)对每个候选点执行多层过滤:
function scatterPoints(region, count, salt) {
const random = mulberry32((WORLD_SPEC.seed ^ hashString(region.id + salt)) >>> 0);
const points = [];
for (let index = 0; index < count; index += 1) {
let accepted = null;
for (let attempt = 0; attempt < 14; attempt += 1) {
const angle = random() * Math.PI * 2;
const radius = Math.sqrt(random()) * region.radius * half;
const x = region.center[0] * half + Math.cos(angle) * radius;
const z = region.center[1] * half + Math.sin(angle) * radius;
// 过滤 1:主导区域检查
const dominant = dominantRegion(x, z);
if (dominant.region.id !== region.id || dominant.weight < 0.34) continue;
// 过滤 2:坡度限制
if (slopeAt(x, z) > region.slope_limit) continue;
accepted = { x, z, y: heightAt(x, z), rotation: random() * Math.PI * 2,
scale: 0.72 + random() * 0.72 };
break;
}
if (accepted) points.push(accepted);
}
return points;
}
两层过滤的逻辑是:
- 主导区域检查:采样该点的区域权重,如果主导区域不是当前区域,或主导权重低于 0.34,则拒绝。这确保树不会长到隔壁区域去。
- 坡度限制:如果该点坡度超过
slope_limit(默认 1.8),则拒绝。这防止资产出现在陡峭的山壁上。
随机数生成器是 mulberry32——一个确定的 PRNG,种子是 WORLD_SPEC.seed ^ hashString(region.id + salt)。同 seed 同规范,散布结果逐点一致。
在 Blender 端(blender-world-runtime.py),散布逻辑更进一步,支持 exclusion_zones——在聚落、道路、河流、地标周围排除资产:
exclusions = list(asset.get("exclusion_zones") or [])
# ...
if any(
math.hypot(x - zone.get("center", [0, 0])[0],
y - zone.get("center", [0, 0])[1]) < float(zone.get("radius", 0))
for zone in exclusions
):
continue
Blender 端还做了另一件事 Three.js 不做的:资产尺寸归一化。导入的 GLB 文件往往有各自的单位、原点和朝向,管线不靠目测调整,而是测量导入模型的 bounding box,按 target_height 归一化,再把 bounding box 底部对齐到地形高度。
两条渲染路径
管线提供了两条渲染路径,选择在 proposal 阶段锁定,不允许后续静默切换:
| 维度 | Three.js / HyperFrames | Blender |
|---|---|---|
| 用途 | 浏览器原生交付、快速迭代 | 参考级渲染、最终成片 |
| 引擎 | Three.js WebGL | Blender 4.5 EEVEE Next |
| 资产 | CC0 GLTF catalog 或程序化基本体 | 同左 + PBR 材质层 |
| 交互 | hf-seek 事件驱动自由视点 |
无交互,输出 PNG 序列 |
| 输出 | 可编辑 workspace(HTML + JS) | .blend 文件 + PNG 帧 + report.json |
| 渲染恢复 | N/A(实时) | resume: true 从缺失帧续渲 |
Three.js 运行时(world-runtime.js,478 行)是整条管线最复杂的单个文件。它在浏览器里重建了与 Python 端完全相同的地形函数、散布逻辑和摄像机插值——确保预览和最终渲染视觉一致。运行时监听 HyperFrames 的 hf-seek 事件,在任何时间点确定性渲染:
function renderAt(timeValue) {
const time = Math.max(0, Number(timeValue) || 0);
const state = cameraAt(time);
camera.position.copy(state.positionV);
camera.fov = state.fov;
camera.updateProjectionMatrix();
camera.lookAt(state.targetV);
// 水面透明度呼吸效果
if (water) water.material.opacity = 0.73 + Math.sin(time * 0.42) * 0.035;
// 阳光强度微弱浮动
sun.intensity = WORLD_SPEC.atmosphere.sun_intensity * (0.96 + Math.sin(time * 0.09) * 0.04);
renderer.render(scene, camera);
}
Blender 端(blender-world-runtime.py,434 行)则用 Blender 的 hetero_terrain 和 fractal 噪声替代正弦叠加,地形更自然。摄像机用 Bezier 插值的关键帧,支持焦距(lens)动画化。Blender 路径还支持"可见性窗口"——地标可以按时间精确出现/消失,用关键帧动画控制 hide_render:
visible_from = placement.get("visible_from_seconds")
if visible_from is not None:
reveal_frame = max(1, round(float(visible_from) * int(spec.get("fps", 30))))
instance.hide_render = True
instance.keyframe_insert("hide_render", frame=max(1, reveal_frame - 1))
instance.hide_render = False
instance.keyframe_insert("hide_render", frame=reveal_frame)
Fidelity Gate:blockout 到 production 的硬门禁
管线最值得注意的设计之一是fidelity gate——不是建议,而是一个会拒绝构建的硬校验。
threejs_world.py 中的 _fidelity_gate 方法对 production tier 检查以下条件,任一不满足就返回 error 而非 warning:
if len(asset_palette) < 8:
errors.append("Production tier requires at least 8 distinct asset-palette entries.")
if len(terrain_materials) < 3:
errors.append("Production tier requires at least 3 terrain material layers.")
if any(not item.get("catalog_id") or not item.get("model_id") for item in asset_palette):
errors.append("Every production asset-palette entry requires catalog_id and model_id.")
加上 CC0 许可证声明检查、catalog manifest 存在性检查、4 个语义类别最低要求。这意味着:你不能用一个空的 production tier 跑出"看起来还行"的结果。系统会直接拒绝构建,而不是降级到 blockout 然后假装通过了。
blockout tier 则有意宽松——允许程序化基本体、平面材质、无外部依赖,但会被明确标记为"不可呈现为参考级输出":
if quality_tier == "blockout":
return [], [
"Blockout tier may use procedural primitives and flat materials; "
"do not present it as reference-grade or production-fidelity output."
]
这个设计触及了一个 Agentic 系统的核心问题:AI Agent 天然倾向于用最简路径完成任务。如果没有硬门禁,Agent 很可能用 blockout 假装 production 交付。fidelity gate 把"你可以假装吗"这个问题从行为约束变成了技术约束。
3D 资产生成:何时调用 API,何时不调用
管线接入了三个外部 3D 资产生成服务,但用了一张精确的路由表来决定何时用哪个:
| 需求 | 工具 | 模型 | 单价(截至 2026-08-13) |
|---|---|---|---|
| 重复植被、岩石、通用道具 | threejs_asset_catalog |
CC0 Kenney catalog | 免费 |
| 文字描述的独特物体 | atlas_3d |
tripo-h3.1/text-to-3d | $0.33–$0.44/次 |
| 匹配概念图的物体 | fal_3d |
Hunyuan 3D v3.1 image-to-3D | $0.225–$0.375/次 |
| 从区域合成图提取多个物体 | fal_3d |
SAM 3D Objects | $0.02/次 |
Atlas Cloud 的代码有一段刻意注释引起了我的注意:
"""The tool deliberately exposes mesh generation as its own capability.
Atlas's HTTP endpoint happens to be named ``generateImage`` for historical
reasons; that implementation detail must not make 3D assets look like image
outputs to the OpenMontage registry or pipeline.
"""
Atlas 的 API 端点名叫 generateImage——历史遗留命名。管线代码主动纠正了这个命名误导,把 capability 声明为 3d_asset_generation 而不是 image_generation。这是小细节,但反映了一种工程纪律:注册表的语义分类不能被底层 API 的命名习惯污染。
每个 3D 资产工具都会输出一份 provenance manifest(来源清单),记录 provider、model id、prompt、seeds、source page、cost、output path。这份清单是审计链的一部分——在资产门禁审查时,审查者可以追溯到每个 mesh 的来源、参数和费用。
诊断视图:五个审查视点
管线不止生成一个最终渲染。_report 方法返回的诊断报告中包含五个必须审查的视图:
"review_views": ["global", "regional", "walk", "semantic", "wireframe"],
"diagnostic_passes": {
"cinematic": "lit beauty render for final review",
"semantic": "stable region-color pass for layout review",
"wireframe": "explicit terrain and asset geometry pass"
},
技能文档解释了为什么不能只看一个视角:
A wide aerial alone can hide broken contacts; a walk shot alone can hide an empty world.
——skills/creative/3d-world-generation.md
- global:鸟瞰,检查整体布局和区域分布
- regional:中等距离,检查区域过渡和地标关系
- walk:地面视角,检查资产接触和重复度
- semantic:饱和区域色,用于诊断布局问题
- wireframe:地形拓扑,用于诊断几何问题
摄像机路径报告还包括 minimum_camera_clearance——相机到地形的最近距离。如果低于 2.0 个世界单位,会发出警告:
if min_clearance < 2.0:
warnings.append(
f"Camera path minimum terrain clearance is {min_clearance:.2f}; "
"review for clipping."
)
这种多视图诊断在 AI 视频生成工具的横向对比中是我见过的最系统的设计。大多数项目要么只给最终渲染,要么给一堆不可控的随机预览。OpenMontage 的做法是:每个视图解决一类特定问题,且所有视图都从同一份 world_spec 确定性生成。
与 WorldClaw 的关系
技能文档里有一份专门的参考文档(worldclaw-principles.md)解释了这条管线与 WorldClaw 论文的关系。
WorldClaw(arXiv:2608.05248v1,Guo et al.,2026)提出了 agentic 3D 世界生成的十条架构原则。OpenMontage 采纳了这些原则但没有复用其代码——因为 WorldClaw 的公开仓库只有论文和素材,没有可执行的生成栈。
文档里有一张映射表,把 WorldClaw 的概念对应到 OpenMontage 的实现。最关键的几条:
| WorldClaw 概念 | OpenMontage 实现 |
|---|---|
| 结构化场景规范 | world_spec JSON + tool schema |
| 语义布局图 | 连续归一化区域权重场 |
| 区域感知高度场 | 加权程序化地形算子 |
| 可复用环境原型 vs 功能性地标 | Scatter 程序化实例;地标显式放置 |
| 渲染引导的有界精修 | HyperFrames 快照 + Agent issue 队列 |
文档也坦诚列出了范围差异:不做单视图物体重建、不做分割、不做生成的 PBR 纹理贴图、当前可运行路径不依赖 Blender 或 Unreal。
局限性
读完 3,979 行代码后,我看到几个值得注意的边界:
-
地形算子的表现力上限。7 种参数化算子覆盖了基本地貌类型,但无法生成侵蚀沟壑、河流切割、冰川 U 型谷等复杂地形。
dune的正弦波纹在大尺度下会显出周期性。如果需要写实地形,Blender 端的hetero_terrain+fractal噪声更好,但 Three.js 端只有正弦叠加。 -
资产多样性的天花板。production tier 虽然要求 ≥8 个资产模型和 ≥4 个语义类别,但 CC0 catalog(Kenney 三件套)的风格高度统一——低多边形、卡通风格。如果你需要写实植被或建筑,需要自己接入 Quixel Megascans 或类似资源,但管线目前不内置这条路径。
-
无角色和物理。管线明确声明不支持 articulated characters、physics、navmeshes 或交互游戏逻辑。这是一个"可飞越的世界",不是"可走进去的世界"。
-
摄像机路径是插值不是 AI。
camera_path由 Agent 显式规划为时间-位置-目标-视场的键列表,运行时用 smoothstep 插值。没有自动运镜、没有目标跟踪、没有碰撞规避——minimum_camera_clearance只报告不阻止。
这些限制大多是刻意的设计选择,而非遗漏。管线的定位是"Agentic 视频生产中的 3D 世界层",不是"通用 3D 世界引擎"。
结论
commit 2702362 的 3D 世界管线做了三件正确的事:
第一,它把世界生成从"AI 一次性生成"变成了"AI 规划 + 确定性物化"。 text-to-3D 模型负责生成单个物体,world_spec 负责描述世界结构,数学函数负责把结构变成地形。结果是可编辑、可审计、可复现的。
第二,它用 fidelity gate 把质量约束从行为层移到了技术层。 Agent 不能偷工减料用 blockout 冒充 production——系统会拒绝构建。这比在 prompt 里写"请注意质量"有效得多。
第三,它的诊断体系是多视点、结构化的。 五个视图各有分工,摄像机间隙、语义覆盖、散布密度都有量化报告。这不是"渲染完看一眼"的松散流程。
如果你在做 Agentic 视频或 3D 内容生产,这套架构值得研究。它的具体实现——正弦叠加地形、mulberry32 散布、Three.js 运行时——也许不是每个场景的最优解,但"语义规范驱动确定性渲染"这个架构思想是方向性的。当大多数 3D 工具还在追求"生成更逼真的 mesh"时,OpenMontage 在解决一个更难的问题:怎么让 AI 持续可控地生成可编辑的 3D 世界。
参考资料
- calesthio — OpenMontage commit 2702362: feat: add production 3D world pipeline (2026-08-13)
- Guo et al. — WorldClaw: Agentic 3D Open-World Generation at Scale (arXiv:2608.05248v1, 2026)
- Atlas Cloud — tripo-h3.1 text-to-3D model (截至 2026-08-13)
- fal.ai — Hunyuan 3D v3.1 Rapid (截至 2026-08-13)
- Kenney — Nature Kit / Fantasy Town Kit / Survival Kit (CC0-1.0)
- Three.js — r181 ESM modules
- Blender Foundation — Blender 4.5 LTS EEVEE Next
- 官小西 — OpenMontage 深度解析:全球首个开源 Agentic 视频制作系统 (本站, 2026-06-29)
- 官小西 — 2026 开源 AI 视频生成工具全景评测 (本站, 2026-07-21)