---
name: code-built-3d
description: "用代码从零件搭 3D 场景并能反过来拆开：每个物件是一个往 Assembly 里加零件的函数（盒子、圆柱、球、线），渲染成技术插画（白灰三档面、细黑描边、白雾），正交等距相机在镜头之间连续运镜，外壳可以降下去露出内部，选中的部分能爆炸成零件并列出零件清单，场景按块无限生成。零图片、零外部资源，three.js 已内置。演示是「无尽楼」：向上无尽的公寓楼，电梯一直往上走，点任意一层进去看、拆开。当用户说「用代码搭一个 3D 场景」「等距 3D 城市」「做成 Train of Thoughts 那种」「全程代码生成不要图片」「能剖开 / 去壳 / 爆炸的 3D」「无限生成的场景」「three.js 做构造」时使用。只在明确请求时触发，忽略顺带提及。"
---

# 代码搭 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` 驱动。

本地打开演示：

```bash
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；触屏操作。

和原作的差距（用户看过截图前算待验证）：
- 房间内部比原作稀疏。
- 没有地面、街道和高低错落的屋顶。
- 人物动作是静止的。
