【Three.js】day102-geometry-transform

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

Day 102 · 几何与变换 — 用零件组装一台设备

前三天你有了:场景、循环、光照。但只有一个立方体。今天做"组装"——几何体(零件的形状)、三变换(position/rotation/scale)、以及 3D 组装的灵魂 Group(把零件焊成一台可以整体搬动的设备)。做完你会明白:3D 场景的复杂性不是靠一个个孤立的物体堆出来的,而是靠层级组织出来的——这正是数字孪生里"设备-部件-零件"结构的雏形。


目录


一、常用几何体清单:你的零件库

1.1 六种核心几何体

几何体

形状

关键参数

工业场景用途

BoxGeometry

立方体

w/h/d

机身、柜体、厂房、管道截面

SphereGeometry

球体

radius/宽高段数

罐体、阀门球、指示灯(Day 101 用过)

CylinderGeometry

圆柱

顶底半径/高/段数

立式罐、轴、法兰、管道

ConeGeometry

圆锥

半径/高/段数

料斗、锥形管道过渡

TorusGeometry

圆环

主半径/管径

法兰、密封圈、圆环结构

PlaneGeometry

平面

w/h

地面、墙面、屏显(双面注意)

1.2 口诀与用法

// 零件工厂:统一封装"几何 + 材质 = Mesh"(避免散落创建)
import * as THREE from "three";

/**
 * 创建工业零件:给定形状与颜色,返回 Mesh
 * 几何/材质分开传,便于复用(见第五节)
 */
export function part(
  geometry: THREE.BufferGeometry,
  material: THREE.Material
): THREE.Mesh {
  return new THREE.Mesh(geometry, material);
}

记住:几何体描述形状,材质描述表面Mesh = 几何 + 材质。任何复杂设备都是一堆 Mesh 的拼装。


二、三变换:position / rotation / scale

2.1 三个属性的语义(与 2D 对照)

属性

3D 含义

2D 对应物

常用方法

position

世界/父级坐标中的位置

left/top 或 transform

.set(x,y,z)

rotation

绕轴旋转(弧度)

transform: rotate

.rotation.y = 弧度

scale

缩放倍数

transform: scale

.set(sx,sy,sz)

2.2 关键注意点

  • rotation 用弧度,不是角度:45° = Math.PI / 4。习惯 rotation.y += speed * delta 的写法(Day 100 时间纪律)

  • scale 可整体也可单轴.scale.set(1, 2, 1) 把一个立方体拉成"板"

  • position 是相对父级的——父是 Group 时,这个意义极其关键(见第四节)

// 变换示例:把一个"板"摆到桌面上方
const plate = new THREE.Mesh(new THREE.BoxGeometry(1, 0.1, 2), mat);
plate.position.set(0, 0.5, 0);      // 放到 (0,0.5,0)
plate.rotation.y = Math.PI / 4;     // 绕 Y 轴转 45°
plate.scale.set(1, 1, 1);           // 不缩放(默认就是 1)

三、Group:把零件焊成设备

3.1 为什么需要 Group

直接往 scene.add() 塞一堆 Mesh,你会得到:

  • 想整体平移设备?得一个一个改 position(还要算相对位移)

  • 想让风扇转但机身不动?不好区分"动的"和"不动的"

  • 想整体旋转?噩梦

Group 就是 3D 的"容器/文件夹"——把相关物体打包,对 Group 的变换会作用于其所有子物体

import * as THREE from "three";

// 组装:设备 = Group(机身 + 风扇 + 指示灯)
const device = new THREE.Group();            // 空容器,位置默认在原点
const body = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 1), bodyMat);
body.position.set(0, 1, 0);                  // 相对 Group:向上 1 格
const fan = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.4, 0.2, 24), fanMat);
fan.position.set(1.2, 1, 0);                 // 相对 Group:右 1.2
device.add(body, fan);                       // 焊进 Group
scene.add(device);                           // Group 进场景

// ✨ 整体搬动:只改 Group 一个位置,所有零件跟着走
device.position.set(3, 0, 2);
device.rotation.y = Math.PI / 6;

3.2 Group 层级树(数字孪生的结构雏形)

scene
 └── device(Group)            ← 设备:整体变换
      ├── body(Mesh:机身)     ← 相对设备:静止
      ├── fan(Mesh:风扇)      ← 相对设备:自转(改 fan.rotation)
      └── indicator(Mesh:指示灯)

记忆钩子:Group = DOM 里的 <div>。子元素的位置相对父容器;父容器一动,子元素跟着动。3D 的"分层组装"和 HTML 的"嵌套布局"是同一个思想。


四、变换的层级与坐标系

4.1 父级变换 → 子级继承

这是 Group 最核心的机制:子物体的 position/rotation/scale 都是相对父级的。父级自身的变换会被"叠加"到子级头上。

世界坐标 = 父级变换 × 子级变换(一层层套下来)
// 演示:父级旋转,子级跟着转但保持相对姿态
const parent = new THREE.Group();
parent.position.set(3, 0, 2);
const child = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), mat);
child.position.set(1, 0, 0);      // 相对父级:右 1 格
parent.add(child);
scene.add(parent);

parent.rotation.y = Math.PI / 2;  // 父转 90°,child 跟着绕父级原点转 90°

4.2 组装时的定位技巧

先在脑子里给设备画坐标:以 Group 原点为"设备中心/地面中心",所有零件用相对坐标摆放。好处:

  • 设备旋转/移动时,零件相对关系不变

  • 风扇这类"独立旋转件"放进自己的子 Group,只转它不转别的(见第六节)


五、性能纪律:几何体与材质的复用

5.1 一个 geometry 可以给多个 Mesh

同样的形状(如 4 根相同的管道)只需建一个 geometry,多个 Mesh 共用——GPU 只需要一份顶点数据:

// ✅ 复用:一份几何体,四个 Mesh
const pipeGeo = new THREE.CylinderGeometry(0.1, 0.1, 1, 12);
const pipes = [0, 1, 2, 3].map((i) => {
  const m = new THREE.Mesh(pipeGeo, pipeMat);
  m.position.x = (i - 1.5) * 0.6;   // 一字排开
  return m;
});

// ❌ 反例:每根管道 new 一次 geometry——浪费显存

5.2 材质也可以共享(只要改的是引用不是对象)

const sharedMat = new THREE.MeshStandardMaterial({ color: 0x16233a });
// 多个 Mesh 共用 sharedMat;想单独变色时再 clone
// const other = sharedMat.clone(); other.color.setHex(0xff6b35);

纪律:循环里绝不 new 几何/材质(Day 100 坑 4);初始化时能复用就复用。这直接决定 Day 105 性能验收的 draw call 数。


六、完整示例:组装一台带风扇的设备

把今天所有内容合成一台"可整体搬动、风扇可独立自转"的设备:

// src/core/three/build-fan-device.ts
import * as THREE from "three";
import { threeTheme as T } from "./theme";

/**
 * 组装一台带风扇的设备(Group 层级示例)
 * 返回 device(整体变换)与 fan 组(风扇独立旋转)
 * —— 结构:device > (body, fanGroup > (blades, hub))
 */
export function buildFanDevice(): {
  device: THREE.Group;      // 整台设备:可整体平移/旋转
  fanGroup: THREE.Group;    // 风扇组:改 rotation.z 让它转
} {
  const device = new THREE.Group();

  // 机身:金属质感盒子
  const body = new THREE.Mesh(
    new THREE.BoxGeometry(2, 1.4, 1),
    new THREE.MeshStandardMaterial({ color: T.panel, metalness: 0.7, roughness: 0.4 })
  );
  body.position.y = 0.7;
  device.add(body);

  // 底座:扁立方体,用 scale 拉长
  const base = new THREE.Mesh(
    new THREE.BoxGeometry(1.2, 0.2, 1),
    new THREE.MeshStandardMaterial({ color: T.panel, metalness: 0.5, roughness: 0.6 })
  );
  base.position.y = 0.1;
  device.add(base);

  // —— 风扇:独立子 Group(只转它,不动机身)——
  const fanGroup = new THREE.Group();
  fanGroup.position.set(1.3, 1.0, 0);       // 风扇在机身右侧

  // 扇叶:用立方体拉成"叶片"绕中心转一圈(4 片)
  const bladeGeo = new THREE.BoxGeometry(0.18, 0.5, 0.04);
  const bladeMat = new THREE.MeshStandardMaterial({ color: T.primary, metalness: 0.6, roughness: 0.3 });
  for (let i = 0; i < 4; i++) {
    const blade = new THREE.Mesh(bladeGeo, bladeMat);
    blade.position.y = 0.3;                 // 叶片离中心一段距离
    blade.rotation.z = (i / 4) * Math.PI * 2;   // 均匀分布四片
    fanGroup.add(blade);
  }
  // 轴心:小球盖住中心
  const hub = new THREE.Mesh(
    new THREE.SphereGeometry(0.08, 12, 12),
    new THREE.MeshStandardMaterial({ color: T.panel, metalness: 0.8, roughness: 0.2 })
  );
  fanGroup.add(hub);

  device.add(fanGroup);
  return { device, fanGroup };
}

// —— 循环里:只转风扇,不动机身(Day 100 时间纪律)——
// const { device, fanGroup } = buildFanDevice();
// scene.add(device);
// const tick = () => {
//   fanGroup.rotation.z += 4 * clock.getDelta();   // 风扇每秒转 4 弧度
//   renderer.render(scene, camera);
// };

这就是"独立旋转件放进自己的子 Group"的典型:风扇整体绕着 Group 原点转,而叶片是相对 Group 摆的。想加第 2 台设备?buildFanDevice() 再调一次,.position.set() 摆开——函数返回 Group,设备即插即用


七、对照表第 3 行:手写 WebGL vs Three.js(几何体)

环节

手写 WebGL

Three.js

渲染循环

rAF + 手动矩阵 + draw

renderer.render(scene, camera)

光照

手写着色器算光照方程

new DirectionalLight(...)

几何体

手动写顶点坐标数组 + 索引缓冲(一个立方体 24 个顶点 36 个索引自己数)

new THREE.BoxGeometry(1,1,1) 一行

变换

手算 model 矩阵(mat4.translate/rotate/scale 再乘)

mesh.position/rotation/scale 自动合成矩阵

层级

自己维护父子矩阵链并逐级相乘

Group.add(child) 自动继承

结论:几何体与层级是"手写成本飙升"的第二个分水岭——手写一个立方体就要面对顶点数组,写一台设备要自己管矩阵链。Three.js 把这一切变成"摆零件"。你在 2D 学过的所有"组合/封装"思想,原封不动搬进 3D


八、常见坑点

坑 1:所有零件都直接加进 scene

症状:想整体移动设备时要逐个改 position,容易乱。修法:用 Group 打包,只改 Group 的变换

坑 2:忘记零件位置是相对 Group 的

症状:设备整体移动后,零件"原地漂移"或位置错乱。原因:把相对坐标当成世界坐标。修法:先在 Group 内定位,再整体摆 Group

坑 3:风扇想转,把整个设备转了

症状:机身跟着风扇一起转。原因:旋转加错了对象。修法:独立旋转件放进自己的子 Group

坑 4:每根管道都 new 几何体

症状:显存飙升、draw call 多。修法:同形状共享一个 geometry(见第五节)。

坑 5:rotation 用角度值

症状:旋转角度不对(如写 45 但转得很少)。原因:rotation 是弧度,45 ≠ 45°修法:Math.PI / 4 或用 THREE.MathUtils.degToRad(45)


九、自测挑战

T1 · 三变换实验(40 分钟)

单个立方体,分别调 position/rotation/scale,各截一张图记录变化。重点实验:scale.set(1,2,1)rotation.y = Math.PI/4 的叠加效果。

T2 · 组装风扇设备(70 分钟)

完成第六节的设备:机身 + 底座 + 四叶片风扇(独立自转)。给设备整体 .position.set(3,0,2).rotation.y,确认设备整体动、风扇照常转

T3 · 批量复用(30 分钟)

用同一份 geometry 造 6 台相同设备,沿 X 轴排成一排(间距 2)。打开 Performance 面板看 draw call 数量与显存——体会"共享几何体"的价值

T4 · 结构复盘(20 分钟)

把设备画成层级树(scene → device → body/fanGroup/…),标出每个节点的"相对坐标"。这张树状图就是 Day 105 车间结构的最小原型


十、总结

环节

要点

几何体

Box/Sphere/Cylinder/Cone/Torus/Plane——工业零件库

三变换

position 位置 / rotation 弧度旋转 / scale 缩放

Group

3D 的 <div>:对 Group 变换,子物体全跟随

层级

子级坐标相对父级,逐级继承——组装 = 画相对坐标

复用纪律

同形状共享 geometry,循环里绝不 new 几何/材质

对照表

第 3 行:几何体与矩阵链是手写成本第二分水岭

一台会转风扇的设备组好了,还能整台搬走。明天解决本周最重要的问题:Three.js 与 Vue 的边界——设备树归谁管,状态放哪。


明日预告:Day 103 Three × Vue 边界——3D 版三问法则:场景图归 Three 管、业务状态归 Pinia 管、哪些数据要 markRaw,以及组件与引擎之间的"桥契约"。

评论