照片一键变模型又热一轮:这次的说法是"换模型不用重搭流程"
【免费下载链接】3d-model-anythingCapture iPhone LiDAR and photos, reconstruct editable Three.js rooms, and explore them in a browser.项目地址: https://gitcode.com/gh_mirrors/3d/3d-model-anything
"照片变 3D"这件事每隔一段时间就会被重新喊一次。早期的卖点是"一句话/一张图出模型",今年的新说法更有工程味道:换模型,不用重搭流程。
这句话如果要成立,重点根本不在模型本身,而在于有没有一层东西是跟模型解耦的。模型可以每周换一个,但坐标系、单位、照片位姿、对比工具、交付接口不能跟着一起换。本文结合3d-model-anything这个开源仓库的真实源码,拆开看看一个"可换模型"的重建流程,究竟要靠哪些契约撑住。
先把话说准:这个仓库并不承诺"一键出模型"
值得先泼一盆冷水。仓库根目录的 README 写得很直白:
The app records the evidence. The skill guides an agent through measurement, modeling and matched-camera visual comparisons. This is an iterative reconstruction workflow, not a one-click scan-to-perfect-model converter.
翻译过来:App 负责记录证据,Agent 负责测量、建模与同机位比对,这是一个迭代重建流程,不是一键扫描出完美模型的转换器。
而社区里那轮"照片变 3D"的热度,主角通常是模型侧:腾讯混元 3D-1.0 开源、TripoSR 在无 GPU 条件下 0.5 秒单图出模、Stable Zero123 把训练效率拉高一个量级,再到 2026 年各类"文生/图生 3D + Unity 工作流"的落地文章。这些进展是真的,但它们解决的是生成质量与速度;而"换模型不用重搭流程"解决的是工程结构。两件事经常被混为一谈。
这个仓库恰好把两者分开了。
三层结构:证据层、转换层、交付层
把目录摊开看,职责切得非常干净:
| 层 | 位置 | 职责 | 换掉它意味着什么 |
|---|---|---|---|
| 证据层 | apps/house-capture | 一次 ARSession 内录制 RoomPlan 布局、LiDAR 网格、带位姿照片、深度帧 | 换采集设备/采集方式 |
| 转换层 | .agents/skills/scan-to-threejs 及其scripts/ | 把 bundle/GLB/RoomPlan JSON 转成房间坐标,测量、建模、比对 | 换重建模型或重建策略 |
| 交付层 | packages/room-runtime/types.ts + demos/index.ts | 向浏览器交付可编辑房间、灯光、剖切、物理、对比机位 | 换渲染/交互栈 |
最关键的一句写在 skill 的定义里:
Use captured geometry as measurement evidence and posed photos as visual evidence.
几何是测量证据,照片是视觉证据。这句话把"模型"从"最终答案"降级成了"证据的一种形态"。一旦模型只是证据,那它当然可以被替换——换成更好的网格、换成高斯泼溅、换成别的重建服务,只要它还能提供"米制、+Y 朝上、地板 y=0"这套可读的量。
这正是"换模型不用重搭流程"能被说出口的前提,而不是模型的功劳。
为什么现在这个点更重要:迭代的是输出格式,不是下游需求
情报里那一串模型进展有个共同特征:它们的输出形态并不统一。Shap-E 给隐式表示,Point-E 给点云,MVS 类方案给多视角一致性图,TripoSR 给网格,Hunyuan3D 给带纹理的网格,工程侧还要再经 GLTFast / glTF-Transform 之类工具转一手 GLB。
而下游要的东西从来没变过:墙是墙、门是门、地板在 y=0、家具不能是一堆包围盒、相机能站进房间里走。
所以真实的风险不是"模型不够强",而是每次模型变强,团队就要重写一遍坐标系对齐、材质尺度和对比脚本。仓库对此的态度很明确:把"会变的部分"压缩到一层可替换的实现里,把"不会变的部分"写成契约。
考验一:坐标系能不能被写成契约
这是最容易翻车、也最能看出抽象是否干净的地方。
iPhone 端每个 session 都活在自己的 ARKit 世界里。ScanBundle.swift的注释写得很清楚:米制、+Y 逆重力朝上、右手系(与 three.js 一致)。而进入房屋坐标系要经过:
point_house = zone.toHouse × session.alignment.toZone × point_session这段公式同时出现在 apps/house-capture/README.md 与 house-capture 参考文档 里,两处必须一致——这就是契约的样子。
更硬的证据在alignment上。office 的第四版重建写明:
- 90.005° 偏航(
data/demos/office-v4/alignment.json中的yawDegrees) - 平移约
[-0.00417, -0.00103, -0.02399]米 - 墙面平面 RMS 2.6 cm
并且 demos/office-v4/BRIEF.md 特意加了一句:
Do not assume identity or use bounding-box scale.
不许假设单位矩阵,不许用包围盒缩放去"凑"。这条禁令不是洁癖。只要允许"bbox 缩放"这种近似存在,任何一次换模型都会引入新的尺度漂移,而漂移会被下游的灯光强度、材质 repeat、物理碰撞体放大成完全不同的画面。
转换脚本也把这条守住了。house_to_rooms.py 里的rigid()会逐项校验:16 个有限浮点、仿射矩阵底行、旋转块正交、行列式为 +1(反射坐标系直接拒绝)。不满足就抛错,而不是"尽力而为地放进去"。
同一份脚本还定义了失败语义:未对齐的房间只报告、不猜测,部分导出返回退出码 2。这种"宁可失败也不编造"的设计,才是换模型时敢复用的底气。
考验二:参考层与成品层是否物理隔离
很多"扫描重建"项目在换模型时崩掉,是因为扫描网格被当成了场景本身:一旦模型升级,旧场景的墙体、剖切、碰撞全部要跟着改。
这个仓库用的是reference 与 model 双轨,并且在类型系统层面就固定下来。types.ts 里的BuiltRoom有这么两个字段:
/** the source capture for Scan/Overlay: the textured scan, or RoomPlan's boxes */ loadReference(onProgress?: (p: number) => void): Promise<THREE.Object3D>; /** reference materials are unlit/baked (scan) or schematic (RoomPlan) */ referenceAvailable?: boolean; referenceKind: 'scan' | 'layout';三个信息量很大的细节:
loadReference是按需加载的,不参与主场景构建;referenceKind只有'scan' | 'layout'两种,扫描网格和 RoomPlan 包围盒被明确归类为"示意/证据",不是成品;referenceAvailable允许为false——公开 demo 就是这样发布的(demos/office-latest/index.ts 里room.referenceAvailable = false,且room.photos = [])。
这一步的意义是:换重建模型时,你换掉的是loadReference背后的那个东西,而不是房间本身。Scan/Overlay 视图变差或变好,都不会污染成品几何。
反过来,RoomPlan 的定位也被写死了。skill 的 modeling.md 里有一句:
RoomPlan boxes are bounds, not finished furniture.
所以仓库里那套WallDef才是权威:origin是内表面、左端、落地;u是"从室内看,从左到右"的墙向;n指向室内;并且强制u × up = n。这是一套可以被脚本生成、可以被断言、不依赖任何模型品牌的中间表示。模型换了,这层不用动。
考验三:比对工具只负责对齐,不负责打分
这是我认为整个仓库最值得抄的一个设计决定。
.agents/skills/scan-to-threejs/SKILL.md里明确说了:
These tools capture and stitch comparisons; they do not evaluate similarity.
也就是说,cmp.sh/cmp_photo.sh只做一件事:用同一个相机把两张图拍下来拼在一起,绝不输出"相似度 0.87"这种看起来客观、实际上会骗人的数字。
实现上很克制。capture.ts 支持photo=N参数,直接用房间数据里记录的position / forward / up / fovY / aspect构造PerspectiveCamera:
const cam = new THREE.PerspectiveCamera(photo.fovY, photo.aspect, 0.02, 50); cam.position.copy(pos); cam.up.set(photo.up[0], photo.up[1], photo.up[2]); cam.lookAt(pos.clone().add(new THREE.Vector3(...photo.forward)));注意这里的位姿不是"估"出来的,是从photos/photos.json里读出来的。而PhotoRecord的字段在 ScanBundle.swift 里定义为相机到世界、列主序 4×4,并特别注明:JPEG 像素是传感器原始横向图像,intrinsics与它匹配,投影时忽略 EXIF。这类坑不写进契约,换一次采集管线就要重新踩一遍。
拼图脚本同样有底线。cmp_photo.sh 里有一句注释:
# cap.sh captures #wrap exactly. Never crop black scene pixels: that changes the camera framing.不许裁掉黑边。因为扫描的空洞本身就是证据。如果为了"看起来对齐"去裁黑边,比对就从"验证"退化成了"美化"。
再往前一步,cmp.sh 会检查两张图尺寸是否一致,不一致直接raise ValueError:
if a.size != m.size: raise ValueError(f'Camera frame mismatch: {a.size} vs {m.size}')机位对不上就失败,而不是"缩放到一样大再拼"。这就是把"换模型"的成本从"人肉怀疑自己"降成"脚本报错"。
转化层本身的替换点,也是显式列出来的
"换模型不用重搭流程"最实在的检验方式:同一批证据,能不能喂给不同的转换实现?
可以,而且入口是并列的:
- iPhone bundle → bundle_to_room.py,输出
worldToRoom、polygon、walls、objectes、photos(含position/forward/up/fovY/aspect) - 整屋
house.json→ house_to_rooms.py,处理 zone/session 变换,输出index.json记录housePlacement - GLB/glTF(例如 Polycam 导出)→ measure_scan.mjs,输出地板/天花板高度、墙向、墙平面,必要时出 2 cm/px 的俯视图
- 没有网格、只剩深度帧 → reconstruct_bundle.py,TSDF 融合 + 按"真的看得见"的照片逐顶点上色
reconstruct_bundle.py的 docstring 值得单独一提:它给--faces的默认值是 0,注释写的是:
Open3D quadric simplification target (0 = off: it tears holes in fused meshes; simplify the GLB with gltf-transform instead)
简化会把融合网格撕出洞,所以默认不做,改到 GLB 阶段用别的工具做。这句话的潜台词是:重建算法的输出质量差异,应该被隔离在"证据生成"这一步里,而不是让下游为了一个破洞去改墙体和材质。
注意 office 的输入规模:第五版是165 张照片、1112 帧深度、501651 个三角面(见 demos/office-v4/BRIEF.md)。这个体量下,换一个重建后端绝不只是"换个 API key",所以可替换性必须来自结构,而不是愿望。
对团队的意义:把"试用成本"降成"配置成本"
落到协作层面,最有说服力的是 demos/index.ts 那三行:
import type { RoomModule } from '../packages/room-runtime/types'; export const ROOMS: RoomModule[] = [{id:'office-v4', label:'Wes’s office', build:async () => (await import('./office-v4')).officeV4.build()}]; export const roomById = (id:string|null) => ROOMS.find(r=>r.id===id) ?? ROOMS[0];新增一个房间 = 加一个对象,实现build(): Promise<BuiltRoom>。而BuiltRoom已经把交付面定死了:group、lights、walls(带剖切分组)、ceiling、wallTop、walkable()、camera、environment、exposure、bloom、render、optimize?。
试用一个新模型/新重建方案,成本因此变成:
- 跑转换脚本,产出新的
scan.json/ 房间坐标; - 在
demos/<id>/写一个模块,或者像 office-v4 那样复用上一版并只叠加差异; - 用
cmp_photo.sh按同机位拍几张,人眼判断。
第 2 点是关键。office 的四个版本不是四个平行重建,而是层层叠加的差异:office-latest复用buildDetailedOffice({ revision: 'october-7' }),再应用十月变化;office-v4直接await officeLatest.build(),只改桌面设备、材质、灯光与建筑细节。代码里甚至写明了原则:
Historical geometry and its materials are never mutated: modified materials are cloned.
旧几何与旧材质永不原地修改,改动一律 clone。这句话在团队协作里等价于"旧版本随时可以回滚比对",也就是把模型迭代的风险关进了一个目录里。
再回到"配置成本"这个说法:换模型不再是"重搭流程",而是"换一个 evidence producer + 一组注册项 + 一次同机位验收"。这三件事都可以并行分派给不同的人,不必等架构师重画架构图。
但有些成本换不掉,说清楚比吹更有用
如果只讲"接口干净",这篇文章就没有价值了。这个仓库反过来把不可替换的部分写进了文档:
第一,证据质量换不掉。skill 里明确要求:先把地板、墙平面、开口、天花板高差、家具尺寸定下来,再补细节;遇到歧义用正交扫描渲染加多张照片交叉确认;记录不确定性,而不是发明测量值。
第二,几何只是包络。RoomPlan 给的是 bounds。真实重建仍要写墙、开洞、做线脚、做材质,这部分在modeling.md里有大量具体做法(u × up = n的墙约定、profile sweep 的左/右偏移符号、onWall的局部坐标系、坐垫间隙 6 mm、靠枕倾斜 0.12 rad)。这些是人类审美与建筑常识,不是模型能替你决定的。
第三,视觉验收换不掉。工具只拼图不打分,最终判断仍是"看图、找出最大差异、修掉、再比"。skill 有一条很硬的提醒:
a build passing does not establish visual accuracy
构建通过 ≠ 视觉正确。这句话应该贴在每个试图用"换模型"省略验收的团队墙上。
结语:真正被沉淀下来的,是"证据 → 坐标 → 交付"这条链
"照片一键变模型"这一轮热度,模型侧确实在进步:单图重建更快、开源权重更多、纹理与材质更完整。但如果把它当成"流程可以随便换"的理由,很快就会在坐标漂移、包围盒缩放、机位对不上这三件事上还债。
3d-model-anything给出的答案不性感,但很耐用:把采集写成文件契约,把几何降级为测量证据,把重建实现关进scripts/,把交付收敛成RoomModule/BuiltRoom,把验收交给同机位拼图而不是相似度分数。
这样当下一代模型出现时,你要改的是loadReference背后那一个函数,而不是整个房间。
【免费下载链接】3d-model-anythingCapture iPhone LiDAR and photos, reconstruct editable Three.js rooms, and explore them in a browser.项目地址: https://gitcode.com/gh_mirrors/3d/3d-model-anything
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考