Three.js 几何体
针对 three.js r186。示例使用 WebGLRenderer;WebGPU 和 TSL 部分使用 WebGPURenderer。
本地覆盖(2026-11-10 由 Claude 为此 PC 添加;非原始技能的一部分)。 我们的动画短片管线 (
<ROOT>\studio,来自 mromidev/claude-animated-shorts) 固定使用 three 0.175 (r175)
- Remotion 4.0.290 + @react-three/fiber 9.1.2 + pmndrs postprocessing 6.37.2。保持 three 版本固定:shade.ts/cardshade.ts 会对 three 的着色器块进行字符串修补。当在该管线中工作时:
- 忽略需要 r176+ 的 "常见错误" / API 建议(如 HDRLoader、Timer、PCFSoftShadowMap 弃用、setEffects、 RenderPipeline、outputBufferType、mesh.pivot、Capsule
height、TSL/WebGPU)。首先检查该 API 是否存在于 r175 中。- 切勿使用 useFrame、Clock、Timer、delta time、setAnimationLoop 或 requestAnimationFrame 进行动画。Remotion 逐帧渲染:从
useCurrentFrame()/ fps 派生所有值(剪辑动画使用mixer.setTime(frame / fps))。- 后期处理使用 pmndrs
postprocessing,而非 three/addons EffectComposer:不要添加 OutputPass(双重 sRGB 编码)。- 卡通颜色:我们合成器中的色调映射是 NEUTRAL;注意 ACES 默认值和
tonemapping_fragment色调。- 对于其他 three.js 项目,下面原始的 r186 指导按原样适用。
快速开始
import * as THREE from "three";
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 5);
// 几何体是形状;多个网格可以共享一个几何体实例
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial());
scene.add(mesh);
renderer.setAnimationLoop(animate);
function animate() {
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
内置几何体
基本形状
new THREE.BoxGeometry(1, 1, 1); // 宽度, 高度, 深度, 宽度分段数, 高度分段数, 深度分段数
new THREE.SphereGeometry(1, 32, 16); // 半径, 宽度分段数, 高度分段数, phi开始, phi长度, theta开始, theta长度
new THREE.SphereGeometry(1, 32, 16, 0, Math.PI); // 半球(phi扫描,穿过极点切割)
new THREE.SphereGeometry(1, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2); // 圆顶(在赤道处切割)
new THREE.PlaneGeometry(10, 10, 1, 1); // 宽度, 高度, 宽度分段数, 高度分段数
new THREE.CircleGeometry(1, 32); // 半径, 分段数, theta开始, theta长度
new THREE.CylinderGeometry(1, 1, 2, 32); // 顶部半径, 底部半径, 高度, 径向分段数, 高度分段数, 是否开口, theta开始, theta长度
new THREE.CylinderGeometry(1, 1, 2, 6); // 六棱柱
new THREE.ConeGeometry(1, 2, 32); // 半径, 高度, 径向分段数, 高度分段数, 是否开口, theta开始, theta长度
new THREE.TorusGeometry(1, 0.4, 16, 100); // 半径, 管道半径, 径向分段数, 管状分段数, 弧长, theta开始, theta长度
new THREE.TorusGeometry(1, 0.4, 16, 100, Math.PI); // 半圆环
new THREE.TorusKnotGeometry(1, 0.4, 100, 16, 2, 3); // 半径, 管道半径, 管状分段数, 径向分段数, p, q
new THREE.RingGeometry(0.5, 1, 32, 1); // 内半径, 外半径, theta分段数, phi分段数, theta开始, theta长度
高级形状
new THREE.CapsuleGeometry(0.5, 1, 4, 8, 1); // 半径, 高度(仅中间部分), 顶部和底部半圆分段数, 径向分段数, 高度分段数
// 半径, 细节级别 - 每个细节级别细分每个面
new THREE.IcosahedronGeometry(1, 0); // 20 个面(细节级别 0)
new THREE.DodecahedronGeometry(1, 0);
new THREE.OctahedronGeometry(1, 0);
new THREE.TetrahedronGeometry(1, 0);
// 多面体 - 平面顶点数组, 索引, 半径, 细节级别
const vertices = [1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1];
const indices = [2, 1, 0, 0, 3, 2, 1, 3, 0, 2, 3, 1];
new THREE.PolyhedronGeometry(vertices, indices, 1, 0);
基于路径的形状
// Lathe - 绕 Y 轴旋转的 Vector2 轮廓: 点, 分段数, phi开始, phi长度
const profile = [
new THREE.Vector2(0, 0), new THREE.Vector2(0.5, 0), new THREE.Vector2(0.5, 1), new THREE.Vector2(0, 1),
];
new THREE.LatheGeometry(profile, 32);
// Extrude - 一个 2D 形状沿 Z 轴挤出
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(1, 0);
shape.lineTo(1, 1);
shape.lineTo(0, 1);
new THREE.ExtrudeGeometry(shape, {
steps: 1, depth: 1, bevelEnabled: true,
bevelThickness: 0.1, bevelSize: 0.1, bevelSegments: 3,
});
// Tube - 沿曲线进行圆形扫描: 路径, 管状分段数, 半径, 径向分段数, 是否闭合
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(1, 0, 0),
]);
new THREE.TubeGeometry(curve, 64, 0.2, 8, false);
文本几何体
import { FontLoader } from "three/addons/loaders/FontLoader.js";
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
// 一个 .typeface.json 字体文件,来自 three.js 仓库的 examples/fonts 目录
const font = await new FontLoader().loadAsync("fonts/helvetiker_regular.typeface.json");
const geometry = new TextGeometry("Hello", {
font: font,
size: 1, // 文本在世界单位中的尺寸
depth: 0.2, // 挤出深度
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
direction: "ltr", // 'ltr', 'rtl' (从右到左) 或 'tb' (从上到下)
});
geometry.center(); // 将字形重新居中到原点
scene.add(new THREE.Mesh(geometry, new THREE.MeshNormalMaterial()));
BufferGeometry
每个几何体的基类:顶点数据的类型化数组加上一个可选的索引。属性是每个顶点具有 itemSize 个值的命名缓冲区;position、normal 和 uv 是材质默认读取的属性。
自定义 BufferGeometry
const geometry = new THREE.BufferGeometry();
// 四个顶点,每个三个浮点数 (x, y, z)
const vertices = new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0]);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
// 索引在两个三角形之间重用顶点
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
// 法线驱动光照,UV 寻址纹理
const normals = new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
// 可选的逐顶点颜色,通过 material.vertexColors = true 渲染
const colors = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0]);
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
scene.add(new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ vertexColors: true })));
属性类型
const count = 4;
const geometry = new THREE.BufferGeometry();
// Float32 用于位置、法线、UV 和浮点颜色
geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(count * 3), 3));
// Uint16 索引寻址顶点 0 到 65534(65535 是图元重启值);setIndex([...]) 使用普通数组会自动选择 Uint16 或 Uint32
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(count), 1));
// Uint8 颜色需要 normalized = true(第三个参数),以便字节作为 0.0-1.0 到达着色器
geometry.setAttribute("color", new THREE.BufferAttribute(new Uint8Array(count * 3), 3, true));
更新 BufferGeometry
const geometry = new THREE.PlaneGeometry(2, 2);
const positions = geometry.attributes.position;
positions.setXYZ(0, -2, -2, 0); // 覆盖顶点 0
positions.getX(0); // 使用 getX / getY / getZ 读取单个分量
positions.needsUpdate = true; // 上传更改;数组在首次渲染后不能增长,因此分配最大值并使用 geometry.setDrawRange(0, n)
geometry.computeVertexNormals(); // 法线必须与新的位置匹配
geometry.computeBoundingBox(); // 包围体驱动视锥体裁剪和光线投射
geometry.computeBoundingSphere();
交错缓冲区
// 一个缓冲区,每个顶点包含位置 (3 个浮点数) 和 uv (2 个浮点数)
const interleavedBuffer = new THREE.InterleavedBuffer(
new Float32Array([
-1, -1, 0, 0, 0, 1, -1, 0, 1, 0, 1, 1, 0, 1, 1, -1, 1, 0, 0, 1, // 四个顶点: xyz + uv
]),
5, // 跨距: 每个顶点的浮点数
);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.InterleavedBufferAttribute(interleavedBuffer, 3, 0));
geometry.setAttribute("uv", new THREE.InterleavedBufferAttribute(interleavedBuffer, 2, 3));
geometry.setIndex([0, 1, 2, 0, 2, 3]); // 四个顶点上的两个三角形
// BufferGeometryUtils.interleaveAttributes([position, normal, uv]) 以这种方式打包现有属性
边缘和线框
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
// 两个面的法线相差至少 thresholdAngle 度(此处为 15)的边缘,加上开放边界边缘
const edges = new THREE.EdgesGeometry(boxGeometry, 15);
const edgeLines = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff }));
// 每个三角形边,包括对角线;两个三角形共享的边只绘制一次
const wireframe = new THREE.WireframeGeometry(boxGeometry);
const wireLines = new THREE.LineSegments(wireframe, new THREE.LineBasicMaterial({ color: 0xffffff }));
scene.add(edgeLines, wireLines);
点
const count = 1000;
const positions = new Float32Array(count * 3).map(() => (Math.random() - 0.5) * 10);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
// sizeAttenuation(默认为 true)使点在透视相机下随深度缩小:一个点跨越 size * (canvas CSS height / 2) / depth CSS 像素
const material = new THREE.PointsMaterial({ size: 0.1, color: 0xffffff });
const points = new THREE.Points(geometry, material);
scene.add(points);
在 WebGPURenderer 下,WebGPU 后端将点绘制为一个像素,无论材质大小如何;请参阅 WebGPU 和 TSL 部分,了解恢复大小的实例化 Sprite。
线条
const linePoints = [
new THREE.Vector3(-1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(1, 0, 0),
];
const geometry = new THREE.BufferGeometry().setFromPoints(linePoints);
const material = new THREE.LineBasicMaterial({ color: 0xff0000 });
// Line 按顺序连接点;LineLoop 闭合回第一个点
const line = new THREE.Line(geometry, material);
const loop = new THREE.LineLoop(geometry, material);
scene.add(line, loop);
// LineSegments 绘制独立的对:顶点 0-1, 2-3, ...
const segmentsGeometry = new THREE.BufferGeometry().setFromPoints([linePoints[0], linePoints[1], linePoints[1], linePoints[2]]);
const segments = new THREE.LineSegments(segmentsGeometry, material);
scene.add(segments);
LineLoop 在 WebGPURenderer 下不受支持:它会记录错误并渲染无物。在那里闭合循环时,重复第一个点并使用 Line。
InstancedMesh
一次绘制调用渲染一个几何体的多个副本,每个副本具有自己的变换。
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshBasicMaterial(); // 乘以实例颜色;MeshNormalMaterial 忽略它们
const count = 1000;
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set((Math.random() - 0.5) * 20, (Math.random() - 0.5) * 20, (Math.random() - 0.5) * 20);
dummy.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
dummy.scale.setScalar(0.5 + Math.random());
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true; // 在网格渲染后更改时需要;之前无害
// setColorAt 在首次使用时创建 instanceColor;循环后标记它
for (let i = 0; i < count; i++) {
instancedMesh.setColorAt(i, new THREE.Color(Math.random(), Math.random(), Math.random()));
}
instancedMesh.instanceColor.needsUpdate = true;
scene.add(instancedMesh);
在运行时,像这样更新和光线投射实例:
// 这里的 mesh 是一个 InstancedMesh
const matrix = new THREE.Matrix4();
const index = 7;
mesh.getMatrixAt(index, matrix);
matrix.setPosition(0, 2, 0); // 仅移动此实例
mesh.setMatrixAt(index, matrix);
mesh.instanceMatrix.needsUpdate = true; // 在任意一批 setMatrixAt 调用后标记
mesh.computeBoundingSphere(); // 实例移动了:刷新用于视锥体裁剪和光线投射的边界
// 光线投射报告哪个实例被击中
const raycaster = new THREE.Raycaster();
const hits = raycaster.intersectObject(mesh);
if (hits.length > 0) {
const instanceId = hits[0].instanceId; // 对于非实例化对象是 undefined
}
InstancedBufferGeometry
超出变换和颜色的逐实例属性。
const count = 1000;
const geometry = new THREE.InstancedBufferGeometry();
geometry.copy(new THREE.BoxGeometry(1, 1, 1)); // 复制索引和所有属性
geometry.instanceCount = count; // 必需:copy() 不携带实例计数
const offsets = new Float32Array(count * 3).map(() => Math.random() * 10);
grometry.setAttribute("offset", new THREE.InstancedBufferAttribute(offsets, 3));
// 使用 WebGLRenderer 和 ShaderMaterial,在顶点着色器中自己声明 `in vec3 offset;`
// 并将其添加到 position(参见 threejs-shaders)。在 WebGPURenderer 下,
// 逐实例数据是一个 TSL 节点 - 请参阅 WebGPU 和 TSL 部分
...
[为了翻译而截断]