ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

img2threejs:图片转Three.js代码的AI流水线拆解

img2threejs:图片转Three.js代码的AI流水线拆解

1. 一张图变 3D 模型,这个项目到底在解决什么问题

第一次看到 img2threejs 这个项目的时候,我正在折腾一个电商产品的三维展示页面。客户给了一堆产品白底图,要求每个都能在网页里旋转、缩放、看细节。按传统路子,要么找建模师一个个手工建,要么用摄影测量搞一圈照片做重建,前者贵后者慢,而且出来的模型动辄几十兆,网页端加载直接卡成幻灯片。img2threejs 给出的思路完全不一样——它不生成传统的网格模型文件,而是让 AI 直接写 Three.js 代码,用代码来描述三维场景。这个思路第一次见会觉得有点绕,但仔细一想非常合理:Three.js 场景本身就是用 JavaScript 代码搭出来的,与其生成一个二进制模型再解析,不如直接生成能跑的代码。

这个项目目前在 GitHub 上有 8.7k Star,核心定位是一个把二维图片转成三维可交互场景的代码生成流水线。它的输入是一张普通的图片,输出是一段可以直接在浏览器里运行的 Three.js 代码,运行后你就能看到一个根据图片内容重建出来的三维场景。适合谁来参考?我觉得三类人最值得看:一是做前端可视化、想快速给页面加三维元素的开发者;二是对 AI Agent 和代码生成感兴趣、想研究多阶段流水线怎么搭的人;三是做电商、教育、数字展馆这类需要大量轻量三维内容的团队。它解决的核心痛点不是“生成最精确的模型”,而是“用最低的成本、最快的速度,得到一个能在网页里跑起来的三维表达”。

我先把结论放前面:img2threejs 的价值不在于它生成的模型有多逼真,而在于它把“图片理解、场景拆解、代码生成、渲染验证”这一整套流程拆成了可独立调试的代码流水线。你可以只用它的一部分,也可以把整条链路接进自己的 Agent 系统。下面我按自己复现和拆解的过程,把这条流水线一层层扒开讲。

2. 整体架构拆解:为什么是代码流水线而不是端到端模型

2.1 从“生成模型”到“生成代码”的思路转变

传统图生 3D 的做法大致分两类。一类是基于深度学习的三维重建,比如用 NeRF、3D 高斯泼溅或者各种 3D 卷积自编码器,直接从多视角图像回归出体素、点云或隐式场。这类方法精度上限高,但有两个硬伤:一是需要多视角输入或者大量训练数据,单张图重建本身就是一个病态问题;二是产出的模型文件体积大,网页端渲染需要额外的加载器和优化流程。另一类是基于模板的检索拼接,从模型库里找相似的部件拼一个出来,灵活度差,遇到没见过的物体就歇菜。

img2threejs 走的是第三条路:把三维场景的构建过程抽象成代码生成任务。图片进来之后,系统先理解图里有什么、这些东西的空间关系大概是什么样,然后把这些理解翻译成 Three.js 的 API 调用序列——几何体用什么、材质怎么设、灯光怎么打、相机放哪里。最终产物是一段可读、可改、可版本管理的 JavaScript 或 TypeScript 代码。这个转变带来的好处非常实际:代码是文本,文本可以 diff、可以 review、可以增量修改;代码体积小,几 KB 就能描述一个场景;代码可以直接在浏览器里执行,不需要额外的模型加载环节。

提示:这个思路的代价是几何精度受限于基础几何体的表达能力。它擅长的是由立方体、球体、圆柱、平面等基本体组合而成的场景,比如房间、简单机械、抽象装置。对于人脸、复杂曲面这类需要高精度网格的对象,代码生成路线目前还打不过传统重建。

2.2 流水线的四个核心阶段

我把整个项目拆成了四个阶段,每个阶段都有明确的输入输出,可以单独替换或调试。

第一阶段是图像理解与场景解析。输入是原始图片,输出是对图中物体、材质、空间布局的结构化描述。这一步通常会调用多模态大模型,让它用文字把图片“讲清楚”——有哪些物体、大概什么颜色、什么材质、前后遮挡关系如何、整体是什么视角。这一步的质量直接决定后面代码生成的天花板,如果模型把图里的沙发认成了床,后面怎么调都是错的。

第二阶段是场景图构建与几何规划。把上一步的自然语言描述转成结构化的场景图,每个节点代表一个物体,节点上挂着位置、尺寸、旋转、材质这些属性。这一步的关键是坐标系的统一和比例的估算。图片是二维的,要还原成三维,必须做合理的假设,比如“地面是水平的”“物体底部贴地”“相机是透视投影”。img2threejs 在这一步会做很多启发式推断,比如根据物体在画面中的位置和大小反推它在三维空间中的大致坐标。

第三阶段是代码生成。把场景图翻译成 Three.js 代码。这一步不是简单的模板填充,因为 Three.js 的 API 组合方式非常灵活,同一个场景可以有无数种写法。项目里通常会约束生成风格,比如统一用MeshStandardMaterial、统一用Group组织层级、统一用OrbitControls做交互。约束越明确,生成结果越稳定。

第四阶段是渲染验证与迭代。生成的代码在无头浏览器里跑一遍,截图,和原图做对比,如果偏差太大就回退到上一步重新生成或调整参数。这个闭环是保证输出质量的关键,也是“流水线”这个词的题眼——它不是一次生成就完事,而是有反馈、有重试的。

2.3 为什么用 TypeScript 而不是纯 JavaScript

项目主体用 TypeScript 写,这个选择在 Agent 类项目里越来越常见。原因很直接:流水线各阶段之间传递的数据结构非常复杂,场景图、材质描述、相机参数、生成结果,每一层都有嵌套结构。用 TypeScript 把这些结构定义成 interface,编译期就能挡住大量字段拼写错误和类型不匹配的问题。我在复现的时候深有体会,如果没有类型约束,光是“position 到底是数组还是对象”这种问题就能耗掉半天。另外 TypeScript 的类型定义本身就是最好的文档,新接手的人看一遍 interface 就知道每个阶段要什么、给什么。

3. 核心细节解析:图像理解与场景图构建的实操要点

3.1 图像理解阶段怎么问,模型才答得准

这一步是整个流水线的地基。我试过几种不同的提示词策略,效果差异非常大。最差的问法是“描述这张图片”,模型会给你一段散文式的描述,信息密度低而且不可解析。好一点的问法是结构化提问,明确要求模型按固定字段输出。img2threejs 这类项目通常会要求模型输出 JSON,字段包括物体列表、每个物体的类别、颜色、材质、大致位置、尺寸估计。

我实测下来,提示词里必须包含几个关键约束。第一,要求模型只输出 JSON,不要有任何额外解释,否则解析器要写一堆容错逻辑。第二,要求位置用归一化坐标,比如物体中心在画面中的相对位置用 0 到 1 表示,这样后面转三维坐标时不用再猜像素尺度。第三,要求模型标注置信度,对于它不确定的物体给低分,后面流水线可以据此决定是否重试。第四,明确告诉模型“只描述你确实看到的东西”,避免它脑补出画面里没有的物体。

注意:多模态模型对空间关系的判断经常出错,尤其是前后遮挡和深度顺序。我的做法是在提示词里单独加一个字段叫depth_order,让模型按从近到远给物体排序,然后把这个顺序作为三维坐标 Z 轴排序的参考。这个技巧对重建正确性提升很明显。

3.2 场景图的数据结构设计

场景图是连接图像理解和代码生成的中间层,设计得好不好直接决定后面代码生成顺不顺。我参考项目里的做法,把场景图定义成树形结构,根节点是场景本身,子节点是各个物体,物体下面还可以挂子物体。每个节点至少包含这些字段:唯一 ID、类别标签、几何类型、位置向量、旋转向量、缩放向量、材质描述、父子关系。

几何类型这里有个取舍。如果直接用 Three.js 的几何体名称,比如BoxGeometry、SphereGeometry、CylinderGeometry,好处是后面代码生成几乎零转换,坏处是场景图就和 Three.js 绑死了,换渲染引擎要重写。如果抽象成中性的几何描述,比如box、sphere、cylinder,灵活度更高但多一层映射。项目里选的是后者,我觉得这个选择更合理,因为流水线的价值就在于各阶段解耦。

材质描述我建议单独抽出来,不要和几何混在一起。一个物体可以换材质,一个材质可以复用在多个物体上。材质描述里至少要有颜色、粗糙度、金属度、透明度这几个字段,对应 Three.js 的MeshStandardMaterial参数。颜色用十六进制字符串,方便直接塞进代码。

3.3 坐标换算:从二维像素到三维空间

这是整个流程里最需要动脑子的部分。图片是二维的,要还原三维,必须做假设。项目里用的是一套基于透视投影的启发式换算。核心思路是:先假设一个虚拟相机,它的视场角、位置、朝向是固定的;然后根据物体在画面中的位置反推它在三维空间中的射线方向;再根据物体的估计尺寸和画面中的像素尺寸,反推它到相机的距离。

具体算的时候,我用的简化公式是这样的。假设相机在原点,朝向 Z 轴负方向,视场角为 fov,画面高度为 H 像素。一个物体在画面中的垂直位置是 y_pixel,那么它在相机坐标系下的垂直角度就是(y_pixel / H - 0.5) * fov。水平方向同理。有了角度,物体在三维空间中的方向就确定了。距离的估算靠物体尺寸:如果物体实际高度估计为 h_world,在画面中占 h_pixel 像素,那么距离d = h_world / (2 * tan(fov/2) * h_pixel / H)。这个公式假设物体正对相机,实际会有偏差,但作为初值够用了。

这套换算的误差来源很多:物体实际尺寸估计不准、相机视场角假设不对、物体有旋转导致投影尺寸失真。所以项目里会在代码生成后做一轮渲染对比,根据对比结果微调位置和尺寸。这个反馈环节不能省,否则出来的场景经常是物体飘在空中或者陷进地里。

4. 代码生成与渲染验证:让 AI 写出能跑的 Three.js

4.1 代码生成的约束策略

让大模型直接写 Three.js 代码,最大的问题是它太自由了。同一个场景,它可能这次用BoxGeometry加MeshBasicMaterial,下次用BoxBufferGeometry加MeshPhongMaterial,再下次把几何体和材质合并成一个Mesh的构造参数。这种不确定性对流水线是灾难,因为后面没法做统一的验证和优化。

项目里的做法是给代码生成加约束。第一,固定 Three.js 的版本,不同版本 API 有差异,锁死版本能避免很多兼容问题。第二,提供代码模板,规定场景初始化、相机设置、灯光添加、渲染循环这些固定部分必须按模板来,模型只负责生成物体创建和摆放的部分。第三,限制可用的几何体和材质类型,给一个白名单,模型只能从里面选。第四,要求生成的代码必须包含导出函数,比如export function createScene(),方便外部调用和测试。

我自己的经验是,约束越细,生成成功率越高,但灵活度越低。这个平衡点要根据你的应用场景来定。如果是做通用工具,约束可以松一点;如果是做批量生产,约束必须严,宁可牺牲一些表现力也要保证稳定性。

4.2 渲染验证的具体实现

渲染验证这一步在 Node.js 环境里跑,用 Puppeteer 或 Playwright 起一个无头浏览器,把生成的代码注入到一个预设的 HTML 页面里,等场景渲染完成后截图。截图之后和原图做对比,对比的指标可以用结构相似性或者简单的像素差异。如果差异超过阈值,就把差异信息反馈给代码生成阶段,让它调整。

这里有个细节很关键:无头浏览器渲染 Three.js 需要 WebGL 支持,默认配置下可能跑不起来。我踩过的坑是 Chromium 在无头模式下 WebGL 被禁用,需要加启动参数--use-gl=swiftshader或者--enable-unsafe-swiftshader来启用软件渲染。软件渲染速度慢,但胜在稳定,适合做验证。如果追求速度,可以用 GPU 加速的无头方案,但配置更复杂。

对比的时候不要期望像素级一致,那不现实。图片和三维渲染出来的画面在光照、阴影、材质表现上天然有差异。我的做法是只对比物体的轮廓和位置,用边缘检测提取轮廓,然后算轮廓的重合度。重合度达到一定阈值就算通过,不通过的话优先调整物体的位置和尺寸,而不是去调材质和光照。

4.3 迭代重试的策略

一次生成就完美的概率很低,所以重试机制是必须的。但重试不能无脑重来,要有策略。项目里的做法是分级重试:第一次失败,把渲染截图和原图一起喂回给模型,让它看着差异改;第二次失败,回退到场景图阶段,调整物体的位置和尺寸参数;第三次失败,回退到图像理解阶段,重新解析图片。每退一级,成本更高,但成功率也更高。

我建议给重试设一个上限,比如最多三轮。超过三轮还没通过,就输出当前最好的结果并标记为低置信度,让人工介入。无限重试不仅浪费资源,而且有时候模型会陷入死循环,越改越离谱。

提示:重试的时候把历史尝试记录一起传给模型,告诉它“之前试过这些方案都失败了,不要再重复”。这个技巧能显著减少无效重试。

5. 常见问题与排查技巧实录

5.1 生成代码跑不起来怎么办

这是最常见的问题,表现是页面白屏或者控制台报错。排查顺序我总结成一张表。

报错类型常见原因排查方法
XXX is not a constructorThree.js 版本不匹配,API 名称变了检查 package.json 里 three 的版本,对照官方迁移文档
Cannot read property of undefined场景图里某个字段缺失,代码生成时没做空值检查在代码生成模板里加默认值,所有字段都有 fallback
WebGL 上下文创建失败无头浏览器没启用软件渲染加启动参数启用 swiftshader
场景全黑灯光没加或者相机位置不对先加一个环境光保证能看到东西,再调相机
物体位置离谱坐标换算出错或者单位不统一打印场景图里的坐标,和预期对比

我的经验是,大部分跑不起来的问题都出在版本和空值上。锁定 Three.js 版本、给所有字段加默认值,能解决八成问题。

5.2 生成的场景和原图差距大

差距大通常有三个原因。一是图像理解阶段就错了,模型把物体认错或者位置判断错。排查方法是把场景图打印出来,和原图对照,看物体列表和位置对不对。二是坐标换算的假设不成立,比如原图是俯视角度,但换算时假设的是平视。这种情况需要让图像理解阶段同时输出视角信息,换算时根据视角调整。三是代码生成时模型自由发挥,没按场景图来。排查方法是把场景图和生成代码对照,看每个物体是不是都按场景图创建了。

我遇到过一个典型情况:原图里是一张桌子,上面放着杯子,但生成的场景里杯子飘在桌子旁边。查下来是图像理解阶段把杯子的位置描述成了“在桌子右侧”,而代码生成时把“右侧”理解成了世界坐标的 X 轴正方向,但实际上应该是相对于桌子的局部坐标。解决办法是在场景图里明确区分世界坐标和局部坐标,父子关系要正确建立。

5.3 性能问题:生成的场景太卡

Three.js 场景卡顿通常是因为 draw call 太多或者几何体面数太高。img2threejs 生成的场景一般不会面数太高,因为用的都是基础几何体,但 draw call 可能很多,如果每个物体都是独立的 Mesh。优化方法有几个:把相同材质的物体合并成一个 Mesh,用InstancedMesh处理重复物体,把静态物体合并几何体。这些优化可以在代码生成阶段就做,也可以后处理。

另一个性能坑是阴影。如果场景里开了阴影,每个投射阴影的物体都会增加渲染开销。我的做法是默认不开阴影,只在需要的时候开,而且限制阴影贴图的分辨率。对于网页端展示,很多时候用假阴影(一个半透明的平面)比真阴影更划算。

5.4 独家避坑技巧汇总

第一个技巧:在图像理解阶段让模型输出一个“场景复杂度”评分,如果评分太高,说明物体太多,直接走简化流程,只保留主要物体,避免生成过于复杂的场景导致后面全链路失败。

第二个技巧:代码生成时给每个物体加一个userData字段,记录它在场景图中的 ID。这样渲染验证发现某个物体位置不对时,能精确定位到是哪个物体,反馈给模型时也更明确。

第三个技巧:把整个流水线的中间产物都存下来,包括原始图片、图像理解结果、场景图、生成代码、渲染截图。出问题的时候可以逐阶段对比,快速定位是哪一步出的错。这个习惯在调试复杂流水线时能省大量时间。

第四个技巧:如果批量处理图片,把相同类别的图片放一起处理,模型在连续处理相似图片时表现更稳定,因为上下文里有参考。这个现象我在多个 Agent 项目里都观察到过。

6. 把 img2threejs 接进自己项目的几种姿势

6.1 只借用图像理解加场景图这一段

如果你已经有自己的三维渲染方案,不需要它生成 Three.js 代码,那可以只用前两个阶段。把图片转成结构化场景图,然后用自己的渲染器去消费这个场景图。场景图是 JSON,任何语言都能解析。这种用法适合已经有成熟渲染管线、只是想加一个“图片快速建模”入口的团队。

6.2 把代码生成当作 Three.js 代码助手

如果你在写 Three.js 项目,经常需要搭一些重复的场景结构,可以把代码生成这一段单独抽出来用。给它一个场景图描述,让它生成对应的 Three.js 代码片段,然后你手动整合进项目。这种用法相当于一个懂 Three.js 的代码补全工具,能省不少敲样板代码的时间。

6.3 整条流水线接入 Agent 系统

如果你在做 Agent 相关的开发,img2threejs 的流水线设计本身就很值得参考。它展示了怎么把一个复杂任务拆成多个可验证的阶段,怎么在阶段之间传递结构化数据,怎么用渲染结果做闭环反馈。这套模式可以迁移到很多其他任务上,比如图片转 HTML、图片转图表、文字转场景等等。核心思想是一样的:不要让模型一步到位,而是拆成理解、规划、生成、验证四步,每步都有明确的输入输出和验收标准。

6.4 批量生产的工程化建议

如果要批量处理大量图片,有几个工程化点要注意。第一,把流水线做成队列,每个阶段是一个 worker,图片在队列里流动,失败的重试也走队列。第二,给每个阶段设超时,避免某张图片卡住整个队列。第三,结果缓存,相同图片不要重复处理。第四,监控各阶段的成功率和耗时,找出瓶颈。我实测下来,图像理解阶段通常最慢,因为要调多模态模型;代码生成阶段最快;渲染验证阶段耗时中等但资源占用高。

注意:批量处理时要注意 API 的速率限制,多模态模型的调用通常有 QPS 上限,队列要加限流。另外渲染验证用的无头浏览器很吃内存,并发数不要开太高,一般 2 到 4 个就够了。

7. 我对这条代码流水线的一些个人判断

复现完整个流程之后,我最大的感受是:img2threejs 的真正价值不在“图生 3D”这个结果上,而在它展示的工程方法论上。把生成任务拆成可验证的阶段、用结构化数据做阶段间契约、用渲染结果做闭环反馈,这套打法可以套用到很多 AI 生成任务上。它不追求单点技术的极致,而是追求整条链路的可控和可调试,这在工程上往往比追求 SOTA 更重要。

另一个感受是,代码作为三维场景的中间表示,这个选择被低估了。代码可读、可改、可版本管理、体积小,这些特性在协作和迭代场景下优势巨大。虽然它表达复杂曲面的能力有限,但对于大量轻量级的三维展示需求来说,够用了。我后续打算把这条流水线改一改,接到自己的项目里,专门用来处理电商产品图的快速三维化,先跑一批看看实际效果和成本。

最后分享一个我在调试时的小发现:把渲染验证的截图和原图并排放在一起看,比只看差异数值更能发现问题。数值只告诉你“差多少”,并排看能告诉你“差在哪”。很多时候模型改不对,是因为反馈信息太抽象,给它具体的视觉对比,它调整的方向会准很多。这个技巧在别的生成任务里也一样管用。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进