27 Three.js 几何体与材质基础开发
Three.js 几何体与材质基础开发
关联:索引
要解决的问题
- 为什么工业 3D 场景最常见的建模方式不是“一上来导入复杂模型”,而是先用基础几何体快速搭出雏形?
- 一个可渲染物体 Mesh 为什么一定是“几何体 + 材质”的组合?只改其中一项会带来什么结果?
- MeshBasicMaterial / MeshLambertMaterial / MeshPhongMaterial 的差异本质是什么?为什么很多人换了材质就“变黑”?
- 设备支架、分拣框、工作台这类工业部件,用哪些基础几何体最省事?哪些参数最常调?
- 我怎么用一套“最小测试清单”,快速验证:几何体创建没问题、材质设置生效、变换(position/rotation/scale)正确?
本讲定位(与上一讲衔接,避免重复)
- 已具备:Three.js 最小渲染闭环(Scene/Camera/Renderer/Mesh)、坐标轴/网格参照物、Vue 组件挂载与卸载 dispose 的基本工程化骨架。
章节内容(本讲核心)
- Three.js 基础几何体:BoxGeometry、SphereGeometry、CylinderGeometry(及常见参数)
- 基础材质对比:MeshBasicMaterial、MeshLambertMaterial、MeshPhongMaterial
- 网格 Mesh:几何体 + 材质组合、批量创建与组织(Group)
- 变换基础:position / rotation / scale 的操作与常见坑
- 材质配置:颜色(color)、透明(transparent/opacity)、线框(wireframe)
环境与先修(默认沿用上一讲工程)
先修要求:
- 已有 Vue3 + Vite + TypeScript 工程,并已安装 Three.js(上一讲完成)。
- 已能在页面中渲染出一个基础场景(能看到 AxesHelper 或 GridHelper)。
如你需要补装依赖(仅在未安装时执行):
# 安装 Three.js 运行时依赖(包含 Scene/Camera/Renderer/Mesh 等核心类)
npm i three
npm i -D @types/three
解释:
three:Three.js 核心库。@types/three:TypeScript 类型定义(用于类型提示与构建期校验)。
你要把一句话刻进脑子里:
Mesh = Geometry(形状) + Material(外观),Mesh 本体负责 position/rotation/scale(变换)。
落到工程上就是:
- 你换 Geometry:物体形状会变(立方体变球体)。
- 你换 Material:形状不变,但“怎么看起来”会变(颜色、是否受光照、是否有高光、是否透明、是否线框)。
- 你改 Mesh 的 position/rotation/scale:形状与外观都不变,但它在场景里的姿态变了。
1) BoxGeometry(设备底座 / 工作台面 / 分拣框侧壁)
import * as THREE from 'three';
// 创建一个长方体几何体(常用于台面、底座、箱体侧壁等)
// 参数含义:宽 width、高 height、深 depth(单位是“世界单位”)
const geometry = new THREE.BoxGeometry(2, 0.4, 1);
解释:
BoxGeometry(width, height, depth):宽/高/深都是“世界单位”,建议与上一讲的单位约定一致(例如 1 单位 ≈ 1 米或 1 单位 ≈ 10 厘米,班级统一即可)。
常用调参思路:
- 先把比例调对(例如台面:宽大、厚度小)。
- 再决定细分段数(本讲不强调细分,默认即可)。
2) SphereGeometry(传感器罩 / 警示灯罩 / 球形滚轮示意)
import * as THREE from 'three';
// 创建球体几何体(常用于罩壳、球形指示物、滚轮示意等)
// 参数含义:半径 radius、水平细分 widthSegments、垂直细分 heightSegments
const geometry = new THREE.SphereGeometry(0.35, 32, 16);
解释:
SphereGeometry(radius, widthSegments, heightSegments):radius:半径。widthSegments/heightSegments:细分越高越圆滑,但顶点更多;入门阶段用32/16通常足够。
3) CylinderGeometry(立柱 / 支架柱 / 滚筒输送线滚筒)
import * as THREE from 'three';
// 创建圆柱几何体(常用于立柱、支撑杆、滚筒等)
// 参数含义:上半径 radiusTop、下半径 radiusBottom、高度 height、侧面细分 radialSegments
const geometry = new THREE.CylinderGeometry(0.12, 0.12, 1.2, 24);
解释:
CylinderGeometry(radiusTop, radiusBottom, height, radialSegments):radiusTop/radiusBottom:上下半径,做“支架锥形”时可以让两者不同。height:高度。radialSegments:侧面细分段数,越大越圆。
最小组合方式:
import * as THREE from 'three';
// 1) 几何体:决定“形状”
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 2) 材质:决定“怎么看起来”(颜色、透明、是否受光照等)
const material = new THREE.MeshBasicMaterial({ color: 0x22c55e });
// 3) Mesh:把几何体 + 材质组合成可渲染对象(并拥有 position/rotation/scale)
const mesh = new THREE.Mesh(geometry, material);
解释:
MeshBasicMaterial:不依赖灯光,适合“先验证能看见”。new THREE.Mesh(geometry, material):把形状与外观组合成可渲染对象。mesh之后要scene.add(mesh)才会被渲染出来。
1) position:摆放位置
// 把物体移动到场景中的某个位置(x, y, z)
mesh.position.set(1.2, 0.2, -0.6);
解释:
position是三维坐标:x/y/z。- 建议总是配合
AxesHelper/GridHelper做“空间校验”,不要盲调。
2) rotation:旋转(单位是弧度)
// 旋转单位是“弧度”,不是角度:45° = Math.PI / 4
// 这里表示绕 Y 轴旋转 45°
mesh.rotation.set(0, Math.PI / 4, 0);
解释:
- rotation 的单位是弧度(rad),不是角度(°)。
- 常用换算:
90° = Math.PI / 2,45° = Math.PI / 4,180° = Math.PI。
3) scale:缩放(比例)
// 按比例缩放(x, y, z),例如把 y 方向拉伸为原来的 2 倍
mesh.scale.set(1, 2, 1);
解释:
-
这是比例缩放,不是“重新建模”;会把几何体按轴向拉伸。
-
工业建模里常用于快速调整“杆件长度/厚度比例”,但要注意保持统一尺度(避免越拉越失真)。
-
MeshBasicMaterial:不受光照影响,永远“能看见”,适合调试与纯色标记物。
-
MeshLambertMaterial:受光照影响,有明暗变化但没有明显高光,适合“哑光/非金属”的快速工业展示。
-
MeshPhongMaterial:受光照影响,支持高光(specular)与 shininess,适合“金属件/抛光件”的强调与对比展示。
一个非常重要的工程结论:
你只要用的是 Lambert/Phong,就必须加灯光,否则你很可能得到“黑的/灰的/看不清”的物体。
目标:用同一个 SphereGeometry,同时创建三颗球,分别使用 Basic/Lambert/Phong,肉眼看到差异。
建议新增组件(示例路径):
src/components/MaterialCompareLab.vue
src/components/MaterialCompareLab.vue:
<template>
<!-- Three.js 的 canvas 会被插入到这个容器里(renderer.domElement) -->
<div ref="containerRef" class="three-container"></div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as THREE from 'three';
// 容器 DOM 引用:用于获取尺寸、挂载 canvas
const containerRef = ref<HTMLDivElement | null>(null);
// Three.js 核心对象(用可空类型,便于卸载时释放)
let renderer: THREE.WebGLRenderer | null = null;
let scene: THREE.Scene | null = null;
let camera: THREE.PerspectiveCamera | null = null;
// requestAnimationFrame 的 id:卸载时需要 cancel
let rafId: number | null = null;
// 监听容器尺寸变化:让渲染器和相机跟着更新
let resizeObserver: ResizeObserver | null = null;
function resize() {
const container = containerRef.value;
if (!container || !renderer || !camera) return;
const width = container.clientWidth;
const height = container.clientHeight;
if (width <= 0 || height <= 0) return;
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
function animate() {
if (!renderer || !scene || !camera) return;
// 每帧渲染:把 scene 在 camera 视角下画到 canvas
renderer.render(scene, camera);
rafId = requestAnimationFrame(animate);
}
function createLights() {
// Lambert/Phong 这类“受光照影响”的材质,必须有灯光,否则大概率会变黑
const ambient = new THREE.AmbientLight(0xffffff, 0.35); // 环境光:整个场景四面八方均匀照过来的光 0xffffff白色,亮度0.35。无阴影、无立体感
const dir = new THREE.DirectionalLight(0xffffff, 1.2); // 平行光(太阳光):用来照亮场景、产生阴影、做出3D立体感
dir.position.set(3, 5, 2);
return [ambient, dir] as const;
}
function disposeScene(targetScene: THREE.Scene) {
// 示例里多个 Mesh 共享同一份 geometry,所以需要去重再 dispose
const geometries = new Set<THREE.BufferGeometry>();
const materials = new Set<THREE.Material>();
targetScene.traverse((obj) => {
const mesh = obj as THREE.Mesh;
if (!mesh.isMesh) return;
geometries.add(mesh.geometry);
const material = mesh.material;
if (Array.isArray(material)) material.forEach((m) => materials.add(m));
else materials.add(material);
});
geometries.forEach((g) => g.dispose());
materials.forEach((m) => m.dispose());
}
onMounted(() => {
const container = containerRef.value;
if (!container) throw new Error('Three container not found');
// 1) Scene:场景容器
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b1220);
// 2) Renderer:渲染器,负责把画面输出到 canvas
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(renderer.domElement);
// 3) Camera:相机(透视)
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
camera.position.set(4, 2.5, 6);
camera.lookAt(0, 0.8, 0);
// 辅助参照物:用于快速判断坐标系方向与尺度
scene.add(new THREE.AxesHelper(2));
scene.add(new THREE.GridHelper(10, 10));
// 4) 灯光:让 Lambert/Phong 的明暗与高光生效
const [ambient, dir] = createLights();
scene.add(ambient, dir);
// 5) 共享几何体:三个球形状相同,便于只对比材质差异
const geometry = new THREE.SphereGeometry(0.45, 32, 16);
// Basic:不受光照影响(用于“先验证可见”)
const basic = new THREE.MeshBasicMaterial({ color: 0x22c55e });
// Lambert:受光照影响(有明暗,但没有明显高光)
const lambert = new THREE.MeshLambertMaterial({ color: 0x3b82f6 }); // 漫反射材质,哑光、不反光、没有亮光亮点
// Phong:受光照影响(有高光),shininess 越大高光越锐利
const phong = new THREE.MeshPhongMaterial({ color: 0xf97316, shininess: 80, specular: 0xffffff }); // 冯氏材质、支持光照、高光和反光。
// 三个 Mesh:形状相同,外观不同(用于对比观察)
const m1 = new THREE.Mesh(geometry, basic);
const m2 = new THREE.Mesh(geometry, lambert);
const m3 = new THREE.Mesh(geometry, phong);
// 横向摆开,避免重叠,便于肉眼对比
m1.position.set(-1.2, 0.5, 0);
m2.position.set(0, 0.5, 0);
m3.position.set(1.2, 0.5, 0);
scene.add(m1, m2, m3);
// 监听容器尺寸变化,自动触发 resize
resizeObserver = new ResizeObserver(() => resize());
resizeObserver.observe(container);
// 首次同步尺寸后再开始渲染循环
resize();
animate();
});
onBeforeUnmount(() => {
// 1) 停止渲染循环和观察器
if (rafId !== null) cancelAnimationFrame(rafId);
if (resizeObserver) resizeObserver.disconnect();
// 2) 释放几何体与材质(GPU 资源)
if (scene) disposeScene(scene);
// 3) 释放渲染器并移除 canvas 节点
renderer?.dispose();
if (renderer?.domElement && renderer.domElement.parentNode) {
renderer.domElement.parentNode.removeChild(renderer.domElement);
}
// 4) 清空引用,帮助 GC
rafId = null;
resizeObserver = null;
camera = null;
scene = null;
renderer = null;
});
</script>
<style scoped>
.three-container {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
解释(你应该能“看懂差异从哪里来”):
- 灯光是关键:
AmbientLight提供整体底光,避免黑死。DirectionalLight提供方向性明暗与高光来源。- 三种材质的可视化差异:
- Basic:无论灯光怎么调,都保持纯色“平涂感”。
- Lambert:有明暗变化,但不会出现明显镜面高光。
- Phong:在 directional light 的作用下会出现高光点,
shininess越大高光越“锐利”。 - 相机
lookAt(0, 0.8, 0):让视线对准球体区域,减少“物体在场景里但没看见”的误判。
把材质当成“外观配置对象”,常见需求是:上色、半透明、线框检查结构。
示例:半透明线框(适合调试分拣框边界)
const material = new THREE.MeshBasicMaterial({
// 颜色:十六进制 RGB
color: 0x60a5fa,
// 透明:需要显式开启,否则 opacity 可能不生效
transparent: true,
// 透明度:0~1(越小越透明)
opacity: 0.35,
// 线框模式:用于结构检查/边界对齐/排错
wireframe: true,
});
解释:
color:颜色(可用十六进制)。transparent: true:开启透明通道,否则 opacity 可能不会按预期表现。opacity:0~1,越小越透明。wireframe: true:线框模式,常用于结构检查与排错。
- 页面有 Canvas(renderer.domElement 已插入容器)。
- 能看到 AxesHelper 或 GridHelper(说明相机/渲染链路有效)。
- 至少 3 个几何体同时可见(Box/Sphere/Cylinder)。
- position 生效:至少一个物体明显不在原点。
- rotation 生效:至少一个物体明显旋转(用 45°/90° 这类易观察角度)。
- scale 生效:至少一个物体明显拉伸或缩放。
- 材质对比生效:Basic 与 Lambert/Phong 在“是否受光照”上表现不同。
- 材质配置生效:透明或线框至少命中一项,并能说明使用场景(调试/结构检查/强调轮廓)。
2) 最小代码自检(不依赖测试框架)
目标:在运行时快速检查“关键对象是否创建成功”,减少肉眼误判。
function assert(condition: boolean, message: string) {
// 条件不满足就抛异常,让错误“立刻可见”
if (!condition) throw new Error(message);
}
function runSmokeChecks(options: {
scene: THREE.Scene | null;
camera: THREE.Camera | null;
renderer: THREE.WebGLRenderer | null;
meshCountMin: number;
}) {
// 关键对象必须存在:否则后续所有渲染/调参都无意义
assert(!!options.scene, 'scene is null');
assert(!!options.camera, 'camera is null');
assert(!!options.renderer, 'renderer is null');
// 粗粒度检查:至少要有一定数量的 Mesh(防止“scene 空空如也”)
let meshCount = 0;
options.scene?.traverse((o) => { // traverse遍历
if ((o as THREE.Mesh).isMesh) meshCount += 1;
});
assert(meshCount >= options.meshCountMin, `mesh count < ${options.meshCountMin}`);
if (options.renderer?.domElement) {
const canvas = options.renderer.domElement;
// canvas 尺寸为 0 通常意味着容器高度为 0 或 setSize 没成功
assert(canvas.width > 0 && canvas.height > 0, 'canvas size is 0');
}
}
解释:
assert:失败就直接抛错,便于你在控制台第一时间定位问题。meshCountMin:用“数量下限”做粗粒度判断,不追求精确枚举。canvas.width/height:可以快速暴露“容器高度为 0 导致 setSize 无效”的问题。
把它接入你组件的方式(示例):
// 示例:期望场景里至少有 3 个 Mesh(例如 Box/Sphere/Cylinder)
runSmokeChecks({ scene, camera, renderer, meshCountMin: 3 });
解释:
- 建议放在你完成
scene.add(...)之后、开始animate()之前。 - 一旦抛错,优先修“工程链路错误”(容器尺寸、相机看向、scene/add)再谈材质和参数。
作业
参考与延伸
- Three.js Mesh:https://threejs.org/docs/#api/en/objects/Mesh
- Three.js BoxGeometry:https://threejs.org/docs/#api/en/geometries/BoxGeometry
- Three.js SphereGeometry:https://threejs.org/docs/#api/en/geometries/SphereGeometry
- Three.js CylinderGeometry:https://threejs.org/docs/#api/en/geometries/CylinderGeometry
- Three.js MeshBasicMaterial:https://threejs.org/docs/#api/en/materials/MeshBasicMaterial
- Three.js MeshLambertMaterial:https://threejs.org/docs/#api/en/materials/MeshLambertMaterial
- Three.js MeshPhongMaterial:https://threejs.org/docs/#api/en/materials/MeshPhongMaterial