【Three.js】day105-boss-3d-workshop

作者:mario 发布时间: 2026-09-07 阅读量:4 评论数:0

Day 105 · BOSS 战 — 第一个 3D 车间:把 7 天零件拧成一件作品

第 15 周收官。今天不做新知识点,做组装与验收:把 Day 99-104 攒下的零件——场景、循环、光照、几何、边界、交互——拧成第一个"会转、会亮、可点、边界清晰"的 3D 车间。上午组装(车间布局 + 多设备 + 交互),下午验收(功能清单 + 性能 + 边界文档定稿)。结束时,你拥有阶段 3 的第一件作品,和第 16 周建模学习的起点。


目录


一、BOSS 战定位:组装不是堆砌

1.1 今天干什么、不干什么

✅ 今天做:
   1. 车间布局:地面网格 + 多设备就位(复用 Day 102 的 buildFanDevice)
   2. 交互闭环:hover 高亮 + 点击详情面板(Day 104 引擎)
   3. 边界定稿:docs/boundary-three-vue.md 从初稿到可交付
   4. 双验收:功能清单 + 性能表,留档进 docs/acceptance/
   5. 交付资产:30 秒演示 + README 条目 + 简历条目

❌ 今天不做(记入第 16 周待办):
   阴影/后处理/动画曲线 —— Week 16 建模与资产
   WS 实时数据驱动 3D —— 第 6 个月(Week 26+)孪生主线
   draw call 深度优化 —— Day 106+ 性能主线

红线第 1 条的精神:视觉先于优化。今天的目标是"看得出、能演示、可验收",不是"最优化"。

1.2 复用清单(不许重写)

零件

出处

今天的角色

createMiniScene

Day 99

场景三件套

渲染循环三段式

Day 100

tick 骨架

三灯法则 + theme token

Day 101

光照与配色

buildFanDevice

Day 102

车间设备单元

SceneBridge/SceneEvents 契约

Day 103

边界与桥

createInteractiveEngine

Day 104

交互引擎

deviceLayout 配置

Day 103

车间布局数据(Vue 侧)


二、车间整体规划(先画图再动手)

2.1 车间平面图(俯视图)

          X 轴 →
     ┌─────────────────────────────┐
     │  (-6,2)   (-2,2)    (2,2)    │  ← 上排:3 台
     │  [泵1]    [泵2]    [冷却塔]  │
     │                             │
     │        ┌─────┐              │
     │        │ 地  │              │
     │        │ 面  │   (0,0)      │
     │        └─────┘              │
     │                             │
     │  (-6,-2)   (-2,-2)  (2,-2)  │  ← 下排:3 台
     │  [风机1]  [风机2]  [主机]   │
     └─────────────────────────────┘
       设备间距 4 单位,绕中心对称,方便旋转观察

2.2 布局配置(Vue 侧数据,入向桥喂入)

// src/config/device-layout.ts(Day 103 契约的实装输入)
export interface DeviceLayout {
  id: string;          // 设备 id(出向桥传的就是它)
  type: "pump" | "fan" | "tower" | "main";
  position: [number, number, number];   // 世界坐标
}

// 6 台设备:3 种类型,车间对称布局
export const deviceLayout: DeviceLayout[] = [
  { id: "pump-01",   type: "pump",  position: [-6, 0, 2] },
  { id: "pump-02",   type: "pump",  position: [-2, 0, 2] },
  { id: "tower-01",  type: "tower", position: [ 2, 0, 2] },
  { id: "fan-01",    type: "fan",   position: [-6, 0,-2] },
  { id: "fan-02",    type: "fan",   position: [-2, 0,-2] },
  { id: "main-01",   type: "main",  position: [ 2, 0,-2] },
];

2.3 地面对齐(小细节,大观感)

所有设备 position.y = 0,Group 内部零件从 y=0 往上摆(Day 102 的"地面中心"定位技巧)——保证设备"站"在地面上,不悬空不陷地


三、组装一:车间布局与设备就位

把布局配置变成场景里的 Group,给每个设备挂上 userData.deviceId(Day 104 拾取的基础):

// src/core/three/build-workshop.ts(组装核心:布局配置 → 场景树)
import * as THREE from "three";
import { buildFanDevice } from "./build-fan-device";
import type { DeviceLayout } from "../config/device-layout";
import { threeTheme as T } from "./theme";

/**
 * 根据布局配置组装 3D 车间
 * 产出:
 *   groups    —— 设备 Group 列表(可拾取对象,带 userData.deviceId)
 *   fanGroups —— 风扇 Group 列表(循环里驱动自转)
 */
export function buildWorkshop(layout: DeviceLayout[]) {
  const groups: THREE.Group[] = [];
  const fanGroups: THREE.Group[] = [];

  layout.forEach((cfg) => {
    // 根据类型建不同设备(本周先统一用风扇设备,Week 16 换模型资产)
    const { device, fanGroup } = buildFanDevice();
    device.position.set(cfg.position[0], cfg.position[1], cfg.position[2]);
    device.userData.deviceId = cfg.id;      // ← 拾取要用的 id(Day 104)
    groups.push(device);
    fanGroups.push(fanGroup);
  });

  return { groups, fanGroups };
}
// 在地面加网格线(视觉锚点,让空间有尺度感)
const grid = new THREE.GridHelper(24, 12, 0x00c6ff, 0x16233a);
grid.position.y = 0.01;   // 略微抬高,避免与地面 z-fighting(坑 1)
scene.add(grid);

GridHelper 是 Three 自带的网格辅助线——工业场景的"地面标尺",一屏就能看出设备间距与空间尺度。留意 y=0.01 的微调:这是防 z-fighting(两个面重合时闪烁)的标准姿势。


四、组装二:交互闭环(hover + 点击 + 面板)

4.1 状态设计(边界纪律:Vue 只存 id)

<!-- src/views/WorkshopView.vue(车间页:Vue 层组装交互) -->
<script setup lang="ts">
/**
 * 车间视图:3D 引擎 + 详情面板
 * 边界:hover/selected 只存 id(string),引擎实例普通变量持有
 */
import { onMounted, onUnmounted, ref } from "vue";
import { createInteractiveEngine } from "../core/three/interactive-engine";
import { buildWorkshop } from "../core/three/build-workshop";
import { deviceLayout } from "../config/device-layout";

const canvas = ref<HTMLCanvasElement>();
const hoverId = ref<string | null>(null);       // hover 面板数据源
const selectedId = ref<string | null>(null);    // 详情面板数据源
const detail = ref<Record<string, unknown> | null>(null);   // 设备详情(本周静态)

let dispose: (() => void) | null = null;

onMounted(() => {
  const engine = createInteractiveEngine(canvas.value!);

  // 布局配置 → 场景树(入向桥的实装:本周静态,第 6 个月换 store 快照)
  const { groups, fanGroups } = buildWorkshop(deviceLayout);
  engine.setDevices(groups, fanGroups);

  // 出向桥:引擎报 id → Vue 更新状态(边界:只传 id)
  engine.events.on((e) => {
    if (e.type === "hover") hoverId.value = e.deviceId;
    if (e.type === "select") {
      selectedId.value = e.deviceId;
      detail.value = getDeviceDetail(e.deviceId);   // 静态详情(本周)
    }
  });

  const stop = engine.start();
  dispose = () => { stop(); engine.dispose(); };
});

onUnmounted(() => dispose?.());
</script>

<template>
  <div class="workshop">
    <canvas ref="canvas" class="workshop__canvas" />
    <!-- 详情面板:id 驱动,纯 Vue 组件,零 Three 依赖 -->
    <div v-if="selectedId" class="workshop__panel">
      <h3>{{ selectedId }}</h3>
      <p>类型:{{ detail?.type }}</p>
      <p>状态:<span class="dot ok" /> 运行正常</p>
      <p>温度:{{ detail?.temp }}℃</p>
      <button @click="selectedId = null">关闭</button>
    </div>
  </div>
</template>

收尾前自查一遍边界:Vue 侧没有 import 任何 Three 类型(除引擎返回的结构)、没有持有任何 Mesh/Group 引用、交互数据全是 string。这就是 Day 103 契约的验收方式。


五、边界文档定稿(Day 103 初稿 → 今天定稿)

Day 103 写了初稿,今天组装完对照实做补全:

# 3D 场景 × Vue 边界决策文档(Day 105 定稿)

## 数据裁决表
(Day 103 表格,实做后确认无出入)

## 桥契约
(SceneBridge / SceneEvents 实装版,含方法注释)

## 响应式纪律(实做验证)
- Three 对象只用普通变量持有(引擎内部),确认未进 ref/reactive
- 出向桥只传 deviceId(string),确认 Vue 组件零 Three 依赖

## 反例推演记录
- "场景树进 ref" → 掉帧(Day 103 实验 T4 实测数据贴这里)

## 复审触发条件
- 第 6 个月接 WS:updateStates 分频与脏检查
- 设备数 > 500:draw call 与可见性剔除
- 新增设备配置面板:光照/布局配置归属重审

边界文档是这个项目最有含金量的交付物之一——它把"为什么这么分"写成了可复审的决策记录。面试被问"Three 和 Vue 怎么协作"时,这张表就是答案的骨架。


六、最终验收:功能清单逐项打勾

对照 week15 总览第五节,今天逐项执行并留档:

□ 场景:深色背景 + 网格地面 + 6 台设备对称就位(俯视/45° 各截一张)
□ 动画:设备缓慢自转 + 风扇独立旋转,转速与帧率无关(改 FPS 不转速)
□ 光照:三灯法则生效——设备有明暗立体感,指示灯恒亮
□ 交互:hover 设备变亮(蓝)、离开还原;点击弹详情面板(橙选中态)
□ 相机:OrbitControls 旋转/平移/缩放正常,拖拽不误触点击
□ 边界:Vue 组件零 Three 依赖;出向桥只传 id;边界文档定稿
□ 工程:vue-tsc 零报错;无 CDN 依赖;README 一键启动两条命令

🎯 每个勾都要留档证据(截图/录屏/实测表进 docs/acceptance/)——"我说通过了"和"有证据地通过了"是两回事(Day 70 原话)。


七、性能验收:FPS 与显存

场景

目标

我的实测

静态观察 10 分钟

≥ 50fps

___

旋转/缩放交互 5 分钟

≥ 50fps

___

反复进出车间页 30 次

显存不涨(Performance/Memory)

___

停循环后 CPU

回落(验证 stop 生效)

___

如果 fps 不达标:先查 Day 100 坑 4(循环里 new 对象)与 Day 102 坑 4(每设备 new 几何体)。本周不追求极致 draw call——那在第 16 周。


八、交付资产:演示视频 / README / 简历条目

8.1 30 秒演示脚本(本周小而精)

【00:00-00:05】开场:45° 视角车间全景(地面网格 + 6 设备)
  旁白:"基于 Three.js + Vue3 的第一个 3D 车间。"
【00:05-00:15】旋转观察:拖动视角绕车间一圈 → 滚轮缩放
  旁白:"OrbitControls 相机控制,俯视到平视自由切换。"
【00:15-00:25】交互高光:hover 设备变亮 → 点击弹出详情面板
  旁白:"Raycaster 射线拾取:点击设备高亮并联动信息面板。"
【00:25-00:30】收尾:风扇特写 + 淡出
  旁白:"边界清晰:场景图归引擎,业务状态归 Vue,走桥通信。"

8.2 README 要点(沿 Day 70 模板)

# 3D 智慧车间 Three.js Workshop
一句话:Vue3 + Three.js 的 3D 车间雏形——可旋转缩放的工业场景,
设备可点击查看详情,引擎与 Vue 边界清晰。
![车间截图](docs/workshop.png)

## 快速启动
```bash
npm install
npm run dev   # 打开 http://localhost:5173

亮点

  • [x] 三灯法则光照 + 工业 token 配色(与 2D 大屏视觉一致)

  • [x] Group 层级组装(机身/风扇独立旋转)+ 设备复用

  • [x] Raycaster 拾取 + 事件桥(引擎报 id,Vue 渲染面板)

  • [x] 边界文档(docs/boundary-three-vue.md)


### 8.3 简历条目(150 字内)

> **3D 智慧车间**(Vue3 + Three.js,个人项目)
> 构建可交互的 3D 工业车间:OrbitControls 相机控制、三灯法则光照、Group 层级组装设备;基于 Raycaster 实现设备拾取与详情面板联动;设计引擎与 Vue 的边界契约(SceneBridge/SceneEvents),场景图归引擎、业务状态归 Vue,以事件桥单向通信、仅传设备 id,组件零 Three 依赖。稳定 50fps+。

> 简历自检(Day 70 纪律):每句话都能被追问 10 分钟——"三灯法则""边界契约""只传 id"都是你本周亲手做的,弹药充足。

---

## 九、第 15 周毕业总结与知识图谱

### 9.1 七天上台阶

Day 99 场景基础 → 第一个 3D 物体(Scene/Camera/Renderer)
Day 100 渲染循环 → 会自转的场景(rAF + Clock + FPS 纪律)
Day 101 光照材质 → 有质感的场景(三灯法则 + Standard)
Day 102 几何变换 → 会转风扇的设备(Geometry + Group 层级)
Day 103 Three × Vue → 边界图纸(3D 版三问法则 + 桥契约)★ 本周灵魂
Day 104 交互 → 可点击的场景(Raycaster + OrbitControls)
Day 105 BOSS 战 → 第一个 3D 车间(组装 + 验收 + 交付)


### 9.2 能力对照:第 15 周前 vs 后

| 能力 | 一周前 | 现在 |
|------|--------|------|
| 3D 渲染 | 无 | 场景/相机/渲染器/循环/光照/材质 |
| 场景组织 | 无 | Group 层级组装 + 复用纪律 |
| 架构 | 2D 边界经验 | 3D 版三问法则 + 双向桥契约 |
| 交互 | 2D 命中检测 | Raycaster 拾取 + 事件桥 |
| 交付 | 2D 大屏 | 第一个 3D 作品 + 边界文档 |

### 9.3 本周最有价值的三个认知

1. **3D 是整帧渲染**(Day 100):性能模型从"局部重绘"变成"每帧预算 16.7ms"——思考方式完全不同于 2D。
2. **边界是 3D × 框架的地基**(Day 103):场景树是自己的响应式系统,两个系统叠加 = 灾难。"传数据不传对象"这一条就值回本周全部时间。
3. **封装要知道它替你做了什么**(对照表):不手写矩阵/光照,但能讲清 Phong 三要素与 NDC 换算——这就是"会用"和"会讲"的分水岭。

### 9.4 待办清单(Week 16 输入)

  • 阴影与后处理(OutlinePass 描边高亮——替换今天的 clone 材质方案)

  • 模型资产(GLTF 导入真实设备模型,替代几何体拼装)

  • 材质系统深化(贴图/纹理/环境贴图——解决 metalness=1 发黑)

  • draw call 优化与实例化(设备数上量时)

  • WS 实时数据驱动 3D 状态(第 6 个月主线,边界已预留)


---

## 十、常见坑点(本周汇总)

### 坑 1:z-fighting(面闪烁)

地面与 GridHelper 同高度时疯狂闪烁。修法:网格线抬高 `y=0.01`,或地面用不同的 y 值。**两个重合面必闪烁——3D 里的"高度必须错开"纪律**。

### 坑 2:设备悬空/陷地

布局配置的 y 设成了 0 但 Group 内部零件从 y=0 摆(底边在 0 之上)→ 设备"浮着"。修法:统一"地面中心"定位,Group 的 y 恒为 0。

### 坑 3:点击永远选不中

排查顺序(Day 104 坑 1 的完整版):NDC y 取反?→ canvas 尺寸?→ pickables 包含 Group 且 `true` 递归?→ 相机 near/far?→ userData.deviceId 挂上了吗?

### 坑 4:交付日加需求

"再加个旋转动画""加点粒子"——组装日最容易失控。修法:红线纪律,需求进 Week 16 待办,今天只验收。

### 坑 5:边界悄悄破洞

验收时发现某个组件 import 了 Three 类型。修法:**用"Vue 组件零 Three 依赖"作为边界验收项**——不过关就重构到只走桥。

---

## 十一、自测挑战

### T1 · 车间组装(80 分钟)

完成第二至四节:6 台设备对称布局 + 网格地面 + 交互闭环(hover/点击/面板)。俯视与 45° 各截图留档。

### T2 · 双验收(40 分钟)

完成第六、七节:功能清单逐项打勾留档 + 性能表填数。任何不达标项定位并修复(优先查 Day 100/102 的经典坑)。

### T3 · 边界自检 + 文档定稿(30 分钟)

用第五节模板定稿 `boundary-three-vue.md`,并全项目扫描确认"Vue 组件零 Three 依赖"。

### T4 · 交付资产(40 分钟)

按第八节录 30 秒演示 + 写 README 条目 + 简历条目(150 字内)。**给"未来的自己"和面试官各留一份**。

---

## 十二、总结

| 环节 | 要点 |
|------|------|
| 组装 | 复用 Day 99-104 零件,只接不写(红线) |
| 布局 | 先画平面图 → 配置数据 → 入向桥喂入 |
| 交互闭环 | 引擎报 id → Vue 存 id → Vue 渲染面板 |
| 边界定稿 | 组件零 Three 依赖 = 边界的验收方式 |
| 双验收 | 功能清单 + 性能表,逐项留档 |
| 交付 | 30s 演示 + README + 简历条目(每句能追问) |
| 待办 | 阴影/模型/材质/优化/WS → Week 16 输入 |

---

**🎉 第 15 周毕业。** 你现在拥有:一个会转、会亮、可点、边界清晰的 3D 车间 Demo,和一周沉淀的"3D 版边界方法论"——这是阶段 3(数字孪生)的第一块地基。下周进入 Week 16:**建模与资产**——GLTF 模型导入、几何进阶、材质系统深化。开始第 16 周。

评论