12 天 5800 星,这个 AI 3D 工具不生成模型,它生成代码
- 输出 TypeScript 代码而非二进制模型,实现可 diff、可修改、语义化
- 分阶段雕刻流水线含 8 个门控 build pass,模拟传统建模流程
- Divine Eye 评估器零 token 消耗,纯 Python 实现确定性图像对比
- 自校正循环中 Agent 视觉审查与门控结合,确保逐步逼近
- 零依赖设计,仅用 Python 标准库,降低安装摩擦
如果你在 2026 年 7 月 15 日打开 GitHub trending,可能会看到一个叫 img2threejs 的项目。它说能「把一张参考图变成 Three.js 3D 模型」,听起来像是又一个 AI 生成的噱头。
但你看一眼它的输出,会发现事情不对。
它的输出不是 .glb、不是 .fbx、不是任何二进制 mesh 文件。它输出的是一个 TypeScript 文件,里面是一个 createGlockGhostProtocolModel() 函数,返回一个 THREE.Group。这个 Group 里有 pivot、有 socket、有 collider、有 userData.sculptRuntime 暴露的运行时层级。你拿到的不只是一个静态模型,而是一个可以直接塞进游戏引擎里做动画的代码组件。
这不是 image-to-3D。这是 image-to-code。
一句话定位
img2threejs 是一个 Claude Code / Codex / OpenCode 的 Agent Skill。给它一张物体参考图,它生成一个 TypeScript 的 Three.js 工厂函数,用 primitives、程序化 shader、生成的几何体重建图中的物体。整个过程是分阶段门控的,每个 build pass 必须通过 Agent 视觉审查才能进入下一个。
它不是 photogrammetry,不是 AI mesh 生成,不是下载的资产包。它走的是第三条路:用代码描述 3D 物体。
为什么「生成代码」比「生成模型」更聪明
传统 image-to-3D 工具的输出有几个痛点,说真的,我自己做 3D 相关项目时被这些问题搞得很烦:
第一,mesh 文件是二进制 blob。你拿到了一个 .glb,想改个颜色?得重新导出。想调整某个部件的比例?回建模软件。代码就不一样,改个参数,刷新浏览器,完事。
第二,mesh 文件不可 diff。你给同事发了个模型更新,他只能看到「文件变了」,看不到具体改了哪里。TypeScript 代码可以 git diff、可以 code review、可以逐行追溯。
第三,mesh 文件不包含语义。一个 .glb 里,枪的扳机和枪的握把是没有区别的,都是三角面片。但 img2threejs 生成的代码里,每个组件都有名字、有层级关系、有 userData 标注。扳机知道自己是扳机,握把知道自己是握把。
其实吧,这不只是方便不方便的问题。它背后是一个更深层的判断:在 AI Agent 时代,代码是比二进制文件更好的媒介。Agent 可以读代码、改代码、理解代码的语义结构。Agent 不能「理解」一个 mesh 文件,它只能把它当作黑盒传过去。
分阶段雕刻:不是一步到位,是一层一层雕
img2threejs 的核心是一个分阶段雕刻流水线(staged sculpting pipeline)。这个设计本身就很值得拆。
流水线分 8 个 build pass,顺序固定,前一个没通过就不能进下一个:
blockout → structural-pass → form-refinement → material-pass
→ surface-pass → lighting-pass → interaction-pass → optimization-pass
你想想看,这跟传统 3D 建模的工作流是呼应的,先搭大形(blockout),再上结构(structural),再修细节(form),再上材质(material),再打光(lighting),最后加交互(interaction)和优化(optimization)。但区别在于,每个 pass 都有门控。
具体怎么做呢?在 forge/stage3_build/orchestrate_passes.py 里,pass_acceptance() 函数会读取 spec 中每个 build pass 的 acceptance 列表,这些是硬性条件,不满足就不解锁。比如 blockout 阶段的 acceptance 可能包括「silhouette 与参考图 IoU ≥ 0.85」,structural-pass 可能要求「每个组件都有明确的层级归属」。
门控的硬性部分由脚本执行,软性部分由 Agent 视觉判断。这个分工是 img2threejs 最精妙的设计。
Divine Eye:零 token 消耗的确定性评估器
说到 Agent 视觉判断,你得先理解一个背景:AI Agent 做视觉审查是很贵的。每次把截图发给模型让它「看看像不像」,都在烧 token。
img2threejs 的答案是 Divine Eye(forge/stage4_review/divine_eye.py),一个完全确定性、零 token 消耗的多信号渲染评估器。
它是一个 ensemble 体系,包含两类信号:
硬门控(一个 fail 就拒绝):
- 轮廓 IoU(< 0.85 → reject)
- 比例偏差(> 0.08 → reject)
软信号(ensemble 加权):
- 比例/宽高比匹配度
- 双边对称性
- pHash 结构相似度
- 全局 SSIM(亮度通道)
- 边缘图重叠度(Sobel 线稿)
- 过曝区一致性
- 平面区域比例(检测「死板填充」)
- 色调/对比度一致性
这些信号全是纯 Python 标准库算出来的,struct 读 PNG、zlib 解压、手写 Sobel 算子、手写 SSIM。没有 PIL、没有 numpy、没有 OpenCV。
坦白讲,这个设计哲学很工业化:把确定性工作推给脚本,把模型 token 留给真正需要判断的地方。Divine Eye 的模块 docstring 写得很清楚:「The single authority the correction loop asks “how close is this render to the reference, and what’s wrong?”」,它是整个自校正循环的唯一权威。
自校正循环:不是生成完就完事了
每个 pass 生成代码后,流程是这样的:
- 生成当前 pass 的 Three.js 代码(
forge/stage3_build/generate_threejs_factory.py) - 在浏览器中渲染,截图
- 打包成一张 side-by-side 对比图(
forge/stage4_review/make_comparison_sheet.py) - Agent 视觉审查这张对比图
- 评分低于阈值 → 自校正(
refine-spec或refine-code) - 评分通过 → 解锁下一个 pass
自校正有五种动作:continue、refine-spec、refine-code、request-input、stop。refine-spec 修复错误或浅薄的 spec 并重新验证,refine-code 修复不符合 spec 的几何体或材质。
这里有个细节值得注意:forge/stage4_review/correction_loop.py 的 decide() 函数中,如果历史上出现 2 次 revert(oscillating = reverts >= 2,第 79 行),后续所有 decide() 调用都会被永久路由到 refine-spec。Issue #18 的审计者用合成历史验证了这一点,2 次早期 revert 之后,即使后面 6 次连续 clean pass,仍然被强制 refine-spec。这可能是有意为之的保守策略,但「永久粘性」和「方向翻转检查只看最近 3 条但 revert 计数看全部历史」的不一致窗口,看起来像是未审视的后果。
零依赖:一个极端的工程选择
img2threejs 的另一个标志性特征是零依赖。所有 Python 脚本只需要 Python 3.10+ 标准库,没有 pip install。
PNG 解析?用 struct.unpack 手写。JPEG 解析?跳过 SOI/APP 标记段手写。颜色空间转换?手写 sRGB→CIELAB 的矩阵运算。连 CIEDE2000 色差公式都是手写的,forge/_shared/color_metrics.py 中的 ciede2000() 函数,通过了 Sharma 标准测试对的验证。
这个选择的代价是明显的。比如 load_image() 函数(forge/stage1_intake/extract_pbr_evidence.py 第 189-207 行)的非 PNG 路径只支持 macOS 的 sips 命令,JPEG 引用在 Linux/Windows 上直接报错。而且 16-bit 和隔行 PNG 即使在 macOS 上也不行,因为 sips 的 png→png 转换保留了原始编码,read_png 照样拒绝。
但好处也很直接:零安装摩擦。Agent 不需要先装依赖再干活,clone 下来就能跑。在 Agent 自动化场景里,少一个 pip install 就少一个可能的失败点。
那个 CS2 武器流水线
img2threejs 最让人印象深刻的 demo 是 CS2 武器重建。Glock-18、M9 Bayonet、Classic Knife,这些 demo 的还原度相当高。
v1.4 的「武器更新」引入了一套完整的 CS2 专用流程:
- 来源感知的 intake(
forge/stage1_intake/detect_cs2.py):用启发式信号(宽高比、颜色分布、已知 CS2 武器轮廓比例)判断参考图是否是 CS2 武器。这不是视觉分析,只是文件元数据和基本图像统计,真正的 CS2 识别由 Agent 视觉层完成。 - 家族特定适配器(
forge/stage2_spec/cs2_adapters.py):knife、pistol(Glock-18)各有独立的组件树合约。Glock-18 的适配器有 slide、frame、magazine、trigger-guard、control、barrel、internal-mechanism 七个独立合约,不会复用 knife 的拓扑。 - 投影优先的 finish:对于 Doppler/Gamma/Marble/Fade 这类图案皮肤,不走程序化材质,而是把参考图的像素直接投影到 mesh 上。README 里说得很直白:「procedural finish for a patterned skin reads visibly wrong against the reference」。
- 结构审查门控(
docs/cs2/review-gates.md):组件覆盖率检查、map-stripped blockout 证据、厚度和长轴视角,确保「真实的几何结构」不会被「逼真的纹理」掩盖。
但这里也有一个值得注意的 trade-off。v1.4.1 新增的 --map-stripped-render 门控(要求 blockout 阶段提供去掉贴图的渲染证据)在 SKILL.md 的文档中完全没有提及。Issue #33 的提交者发现,一个严格按照 SKILL.md 文档操作的用户,会在第一个 pass 就撞上 ValueError,因为 append_review.py 的第 282 行要求 --map-stripped-render 参数,但 SKILL.md 第 163 行的标准调用里没有这个参数。同时 3 个测试在 clean clone 上直接失败。
这种文档/代码失同步在快速迭代的开源项目中很常见,但对于一个「必须按文档操作」的 Agent Skill 来说,这会让 Agent 在第一步就卡住。
不能只看演示,要看它测了什么
Issue #18 是一份 10 项已确认 bug 的审计报告,每个 bug 都有复现脚本和根因分析。我把关键的几个挑出来:
1. Divine Eye 的 scale 硬门控是死代码
proportion_delta 返回的是 snake_case 的 key(scale_delta、aspect_ratio_delta),但 evaluate() 读的是 camelCase(scaleDelta、aspectRatioDelta),默认值 0.0。这意味着两个硬门控之一,scale 偏差检测,永远不会触发。一个 16×64 的矩形 vs 112×24 的矩形,真实 scale_delta 是 1.625(阈值 0.08),但 Divine Eye 读到的是 0.0,评分通过。
2. 色盲的 fidelity 评分
Divine Eye 的 ensemble 加权信号全是亮度或形状派生的。hueZoneParity 和 specularWash 是 report-only,不影响 fidelity、不影响硬门控、不影响路由。用红色参考图 vs 蓝色渲染的测试中,verdict: pass, fidelity: 0.9485,而 hueZoneParity 正确读到 0.0 但被忽略了。_shared/color_metrics.py 里已经有完整的 CIEDE2000 实现,只是没接入加权评分。
3. 缺失的 lathe/extrude/tube profile 静默替换为默认形状
generate_threejs_factory.py 的 geometry_for() 函数中,当 descriptor 缺少 profile 数据时,用 _DEFAULT_LATHE_PROFILE(一个沙漏形状)等默认值替换,没有警告,没有标记,exit 0。这跟四行之前的 GeometryNotImplementedError 文档说明直接矛盾:「Never silently substitute a box… with no signal that anything was wrong」。
4. 角色 anatomy 数据被测量、传递、然后静默丢弃
new_sculpt_spec.py 的 make_character_component_tree(anatomy) 接受了 anatomy 参数但从未在函数体中使用。所有比例都是硬编码的 hu = 0.28。每个角色 spec 的「通用」人形模板硬编码了一个特定开发者的特征,Orioles 胸标、侧分头发、眼镜、耳机。
5. 图片解码只支持 macOS
load_image 的非 PNG 路径依赖 macOS 的 sips 命令。同样的模式在 extract_landmarks.py、delight_albedo.py、build_detail_inventory.py、make_comparison_sheet.py 中重复了 5 次。
说真的,10 个 bug 里前 3 个意味着自动化审查循环在它应该拒绝的时候批准了渲染(错误的比例、分离的轮廓、错误的颜色)。但反过来想,这也说明项目有一个愿意深入审计的社区,Issue #18 的提交者用 stdlib 的最小复现脚本逐个验证,每个 bug 都有根因和修复建议,质量极高。
12 天,2 个人,一个野心勃勃的路线图
img2threejs 从创建到现在只有 12 天。12 天里发布了 5 个版本(v1.0 到 v1.4.1),累计 5859 颗星,453 个 fork。
但贡献者只有 2 个人。bus factor = 2,实际上 = 1,因为 30 个贡献者中只有 2 个,主作者承担了几乎全部代码。
路线图倒是野心勃勃:v1.5 角色重建、v1.6 环境生成、v1.7 游戏引擎导出、v1.8 自动绑定、v1.9 AI Studio、v2.0 程序化世界生成。整个弧线是:资产 → 世界 → 生产 → AI 游戏资产平台。
坦白讲,这个路线图的时间跨度至少是年为单位,2 个人即使全职也不太可能在短期内完成。但换个角度看,开源项目不一定需要「做完」才有价值。img2threejs 的核心贡献不是它能生成多少模型,而是它提出了一个范式:在 AI Agent 时代,image-to-3D 的答案不是更好的 mesh 生成模型,而是更好的代码生成流水线。
代码优于网格:一个可命名的模式
img2threejs 最值得带走的东西,不是它的 3D 效果(虽然 CS2 武器的 demo 确实不错),而是一个可以复用的判断:
在 AI Agent 时代,代码是比二进制资产更好的输出格式。
这个判断不限于 3D 建模。你可以把它迁移到任何 Agent 驱动的创作场景:
- 图片生成 → 输出 SVG 代码,而不是 PNG
- UI 设计 → 输出 React/Vue 组件,而不是 Figma 截图
- 视频编辑 → 输出编辑脚本,而不是渲染后的 mp4
- 数据分析 → 输出可复现的 notebook,而不是静态图表
代码可 diff、可 review、可修改、可组合、可版本控制。Agent 可以理解代码的语义结构,可以增量修改,可以基于上下文做出更好的决策。而二进制文件对 Agent 来说就是黑盒。
这就是 img2threejs 的选择,它不跟 photogrammetry 比精度,不跟 AI mesh 生成比速度,它换了一个维度竞争:输出格式本身的可操作性。
如果你在做 Agent 工具,这个模式值得记住:不是你生成的东西有多好,是你的 Agent 能不能接着改它。
什么场景该用,什么场景不该用
img2threejs 适合的场景:
- 需要程序化生成、可参数化调整的 3D 资产(游戏道具、场景装饰)
- 需要动画就绪的运行时层级(pivot、socket、collider 都在代码里)
- 需要版本控制和团队协作的 3D 资产(代码可 diff)
- 想用 Agent 自动化 3D 资产管线的场景
不适合的场景:
- 需要照片级真实感的高精度重建(单张图无法还原背面,角色是风格化的)
- 跨平台批量处理(目前只支持 macOS,Linux/Windows 的 JPEG 路径 = 报错)
- 需要稳定的生产级输出(10 个已确认 bug,文档和代码不同步,没有 CI)
- 有机体/复杂生物模型(角色 anatomy 数据被静默丢弃,硬编码了一个特定人的模板)
如果你只是想快速把一个参考图变成能跑在浏览器里的 3D 模型,而且不介意它还在快速迭代中,img2threejs 是一个值得关注的方向。但如果你需要的是生产级的 3D 资产管线,等到它解决完 issue #18 那 10 个 bug 再说。
评论互动