code-built-3d

用代码从零件搭 3D 场景并能反过来拆开:每个物件是一个往 Assembly 里加零件的函数(盒子、圆柱、球、线),渲染成技术插画(白灰三档面、细黑描边、白雾),正交等距相机在镜头之间连续运镜,外壳可以降下去露出内部,选中的部分能爆炸成零件并列出零件清单,场景按块无限生成。零图片、零外部资源,three.js 已内置。演示是「无尽楼」:向上无尽的公寓楼,电梯一直往上走,点任意一层进去看、拆开。当用

SKILL.md

GitHub

代码搭 3D 场景

这个 skill 做两件事:从零件把一个东西搭出来,再把它拆开。两件事用同一套零件数据。

环节 怎么做 在哪
零件 每个物件是一个函数,往 Assembly 里加 box / cyl / cone / ball / line。每个零件带名字、角色(shell 是能降下去的外壳)、尺寸,可选爆炸方向 assets/kit.js 的 Assembly
画法 面的明暗按朝向分三档(顶面最亮,朝镜头的两面依次暗一档),每条边描细黑线;离焦点越远(竖向距离或深度)越融进白色 createLook(),一个着色器
镜头 正交相机。镜头 = 目标点、缩放、方位角、仰角,换镜头时用 smootherstep 连续过渡,缩放按对数插值。不硬切 Director
去壳 外壳按 y 方向缩到 0,像墙沉进地板。看房间时,上面的楼层整体抬起 setShell(),demo 里的 LIFT
拆开 把选中的部分改成逐零件建模,零件分层往上升,水平方向不超出给定范围;manifest() 给出零件清单(名字、尺寸、位置) exploder()、Assembly.manifest()
交互 悬停显示包围框和标签,点击切到对应镜头;DOM 气泡钉在 3D 点上,被雾吞掉时自动隐藏 picker()、outline()、labels()
无限 以一个移动的坐标为中心,只保留前后若干块,出范围的块回收;同一个种子永远生成同一座城 streamer()、rng(seed)

文件

  • assets/kit.js:引擎(ES 模块)。
  • assets/tower.js:演示用的零件库,包括人、椅子、桌子、灯、植物、空调外机、7 种房间、楼层、电梯轿厢、邻楼分段(5 种立面)、连廊、电缆。
  • assets/demo.html:无尽楼。一份 import map 指向 vendor/。
  • assets/vendor/:three.js r184 的 three.module.min.js、three.core.min.js、BufferGeometryUtils.js,以及 MIT 许可证 THREE-LICENSE。不依赖 CDN。
  • scripts/serve.mjs:静态服务器。ES 模块不能从 file:// 加载,必须用它打开。
  • scripts/check.mjs:验收脚本,通过页面上的 window.__scene 驱动。

本地打开演示:

node scripts/serve.mjs assets 8765   # 在本 skill 目录下运行

然后打开 http://127.0.0.1:8765/demo.html。

步骤

  1. 先列零件,再写代码。把要搭的东西拆成层级:场景 → 块 → 物件 → 零件。每个零件写清名字、大概尺寸、属于外壳还是内部。这份清单就是拆解线的部件清单,manifest() 会原样输出。
  2. 写零件函数。一个物件一个函数,参数放位置、朝向和随机数。照着 tower.js 的 person / chair / table / acUnit 写。尺寸用真实比例,比如人约 1.7、门约 2、层高 3.4,画面才会显得可信。
  3. 决定哪些合并、哪些单独建模。远处和不交互的整块合并,只占 2 次绘制。要拆、要点选的部分用 build({ separate: true }),等需要时再改成单独建模,用完合并回去。
  4. 定镜头。至少要有:远景、跟拍主角、进入内部、换一个角度。巡游按时长轮换;用户点了以后停止巡游,按 Esc 回到巡游。
  5. 加去壳和拆开。外壳零件统一标 role: 'shell'。拆开时给 bounds,防止零件穿出墙外。
  6. 接无限生成。块的大小要大于一屏的高度或宽度;前后保留的范围要覆盖远景能看到的范围,否则会看见楼顶或楼底。
  7. 暴露 window.__scene(形状见 demo),然后验收:node scripts/check.mjs <目录> --out <截图目录>,全部 ✓ 后再看截图。

硬规则

  • 零图片:不加载任何贴图或图片文件。窗户、文字以外的细节全部用几何体表现。气泡里的表情用内联 SVG 画。验收会拦截图片请求,以及任何离开所在目录的请求。
  • 只学手法:参考作品的题材、角色设定和文案不照搬。
  • 颜色克制:只有白、灰两档和黑色点缀,不加任何彩色。描边只用一种墨色。
  • 镜头不硬切:所有换景都要连续过渡。减少动态效果时不自动巡游,换镜头直接跳到位。
  • 逻辑和画面分开:楼层、焦点、是否已拆开这些状态在 JS 里,画面跟着状态走;advance(sec) 能不等真实时间直接推进模拟,测试靠它。
  • 性能:远处的东西合并成大块;单独建模只给当前在看的那一块。

常见坑

  • 往下看房间时,上一层的楼板和外墙会挡住房间深处。解决办法是把上面的楼层抬起来,或者降低仰角。光调雾不管用。
  • 用屏幕坐标点选时,要瞄准物体朝外那面墙的中点,不要瞄准物体的中心。从上往下看,中心对应的屏幕位置往往落在上一层上。
  • 深度雾必须按相机距离计算:相机距目标 400,所以 uDNear/uDFar 设在 404–452。设成绝对的 150–330,整个画面会全被雾盖住。
  • CSS 里 display:flex 会覆盖 hidden 属性,必须加 [hidden]{display:none!important}。
  • 相邻楼层的房型用"序号 ×3 + 0 或 1,再对 7 取余"来决定,可以保证相邻两层永远不重复。

验收记录

2026-10-27,assets/demo.html,Playwright Chromium 156 软件渲染,check.mjs 16/16 通过:

  • 画面:远景墨线占比 8%。
  • 巡游:相机确实在镜头之间移动。
  • 点选:点第 7 层外墙进入第 7 层;该层外壳降到 0,上一层抬起 3.6。
  • 拆开:点"拆开"后爆炸到 1,零件清单 24 行;点"装回去"能复原。
  • 无限:模拟 120 秒从 8.8 层升到 37 层,块数一直是 35、物体数一直是 416。
  • 资源:没有报错,没有图片请求,没有任何离开所在目录的请求。
  • 手机:390 宽无横向溢出,按钮都在屏幕内。
  • 减少动态效果:画面静止。
  • 帧率:软件渲染约 25fps,只作参考。

没验的:真实显卡的帧率;真机 Safari 和 Firefox;触屏操作。

和原作的差距(用户看过截图前算待验证):

  • 房间内部比原作稀疏。
  • 没有地面、街道和高低错落的屋顶。
  • 人物动作是静止的。