关于 threejs-geometry
threejs-geometry 是面向 three.js r186 的几何体创建与管理参考技能,涵盖内置形状、自定义 BufferGeometry、实例化、几何工具及性能注意事项,适用于 three.js 开发者。
主要功能
- 01内置几何体与路径形状
- 02自定义 BufferGeometry 与属性
- 03线条、点和线框
- 04InstancedMesh 与 BatchedMesh
- 05几何变换与形变目标
- 06合并、性能与资源释放
适用场景
- 01创建基本或路径类三维形状
- 02构建并更新自定义顶点数据
- 03用实例化绘制重复对象
- 04合并几何体并减少绘制调用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cloudai-x/ threejs-skills --skill threejs-geometry安装后在 Claude Code 中输入/threejs-geometry确认已加载。
详细介绍
Three.js 几何体
面向 three.js r186。示例使用 WebGLRenderer;WebGPU 和 TSL 部分使用 WebGPURenderer。
快速开始
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);
// A geometry is the shape; many meshes can share one geometry instance
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); // width, height, depth, widthSegments, heightSegments, depthSegments
new THREE.SphereGeometry(1, 32, 16); // radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength
new THREE.SphereGeometry(1, 32, 16, 0, Math.PI); // half sphere (phi sweep, cut through the poles)
new THREE.SphereGeometry(1, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2); // dome (cut at the equator)
new THREE.PlaneGeometry(10, 10, 1, 1); // width, height, widthSegments, heightSegments
new THREE.CircleGeometry(1, 32); // radius, segments, thetaStart, thetaLength
new THREE.CylinderGeometry(1, 1, 2, 32); // radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength
new THREE.CylinderGeometry(1, 1, 2, 6); // hexagonal prism
new THREE.ConeGeometry(1, 2, 32); // radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength
new THREE.TorusGeometry(1, 0.4, 16, 100); // radius, tube, radialSegments, tubularSegments, arc, thetaStart, thetaLength
new THREE.TorusGeometry(1, 0.4, 16, 100, Math.PI); // half arc
new THREE.TorusKnotGeometry(1, 0.4, 100, 16, 2, 3); // radius, tube, tubularSegments, radialSegments, p, q
new THREE.RingGeometry(0.5, 1, 32, 1); // innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength
高级形状
new THREE.CapsuleGeometry(0.5, 1, 4, 8, 1); // radius, height (the middle section only), capSegments, radialSegments, heightSegments
// radius, detail - each detail level subdivides every face
new THREE.IcosahedronGeometry(1, 0); // 20 faces at detail 0
new THREE.DodecahedronGeometry(1, 0);
new THREE.OctahedronGeometry(1, 0);
new THREE.TetrahedronGeometry(1, 0);
// Polyhedron - flat vertices array, indices, radius, detail
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 - a Vector2 profile rotated around the Y axis: points, segments, phiStart, phiLength
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 - a 2D Shape pushed along 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 - a circular sweep along a curve: path, tubularSegments, radius, radialSegments, closed
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";
// A .typeface.json font, from examples/fonts in the three.js repository
const font = await new FontLoader().loadAsync("fonts/helvetiker_regular.typeface.json");
const geometry = new TextGeometry("Hello", {
font: font,
size: 1, // text size in world units
depth: 0.2, // extrusion depth
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
direction: "ltr", // 'ltr', 'rtl' (right to left) or 'tb' (top to bottom)
});
geometry.center(); // recentre the glyphs on the origin
scene.add(new THREE.Mesh(geometry, new THREE.MeshNormalMaterial()));
BufferGeometry
所有几何体的基类:由顶点数据类型化数组以及可选索引组成。属性是按顶点存储 itemSize 个值的命名缓冲区;材质默认读取的属性是 position、normal 和 uv。
自定义 BufferGeometry
const geometry = new THREE.BufferGeometry();
// Four vertices, three floats each (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));
// Indices reuse vertices across the two triangles
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
// Normals drive the lighting, UVs address textures
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));
// Optional per-vertex colors, rendered with 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 for positions, normals, UVs and floating-point colors
geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(count * 3), 3));
// Uint16 indices address vertices 0 to 65534 (65535 is the primitive-restart value); setIndex([...]) with a plain array picks Uint16 or Uint32 itself
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(count), 1));
// Uint8 colors need normalized = true (third argument) so bytes reach the shader as 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); // overwrite vertex 0
positions.getX(0); // read single components with getX / getY / getZ
positions.needsUpdate = true; // upload the change; an array cannot grow after its first render, so allocate the maximum and use geometry.setDrawRange(0, n)
geometry.computeVertexNormals(); // normals must match the new positions
geometry.computeBoundingBox(); // bounding volumes drive frustum culling and raycasting
geometry.computeBoundingSphere();
交错缓冲区
// One buffer holding position (3 floats) and uv (2 floats) per vertex
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, // four vertices: xyz + uv
]),
5, // stride: floats per vertex
);
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]); // two triangles over the four vertices
// BufferGeometryUtils.interleaveAttributes([position, normal, uv]) packs existing attributes this way
边线和线框
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
// Edges where the two faces' normals differ by at least thresholdAngle (degrees; 15 here), plus open boundary edges
const edges = new THREE.EdgesGeometry(boxGeometry, 15);
const edgeLines = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff }));
// Every triangle edge, diagonals included; an edge shared by two triangles is drawn once
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 (default true) shrinks points with depth under a perspective camera: a point spans size * (canvas CSS height / 2) / depth CSS pixels
const material = new THREE.PointsMaterial({ size: 0.1, color: 0xffffff });
const points = new THREE.Points(geometry, material);
scene.add(points);
在 WebGPURenderer 下,WebGPU 后端绘制的点始终为 1 像素,不受材质大小影响;如需恢复大小,请参阅 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 connects the points in order; LineLoop closes back to the first point
const line = new THREE.Line(geometry, material);
const loop = new THREE.LineLoop(geometry, material);
scene.add(line, loop);
// LineSegments draws independent pairs: vertex 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);
WebGPURenderer 不支持 LineLoop:它会记录错误且不渲染任何内容。在该环境中,可通过重复第一个点并使用 Line 来闭合环路。
InstancedMesh
通过一次绘制调用绘制同一几何体的多个副本,每个副本都有自己的变换。
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshBasicMaterial(); // multiplies by instance colors; MeshNormalMaterial ignores them
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; // needed after changes once the mesh has rendered; harmless before
// setColorAt creates instanceColor on first use; flag it after the loop
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 here is an InstancedMesh
const matrix = new THREE.Matrix4();
const index = 7;
mesh.getMatrixAt(index, matrix);
matrix.setPosition(0, 2, 0); // move just this instance
mesh.setMatrixAt(index, matrix);
mesh.instanceMatrix.needsUpdate = true; // flag after any batch of setMatrixAt calls
mesh.computeBoundingSphere(); // instances moved: refresh the bounds used by frustum culling and raycasting
// Raycasting reports which instance was hit
const raycaster = new THREE.Raycaster();
const hits = raycaster.intersectObject(mesh);
if (hits.length > 0) {
const instanceId = hits[0].instanceId; // undefined for a non-instanced object
}
InstancedBufferGeometry
除变换和颜色外,还可设置每个实例的属性。
const count = 1000;
const geometry = new THREE.InstancedBufferGeometry();
geometry.copy(new THREE.BoxGeometry(1, 1, 1)); // copies the index and all attributes
geometry.instanceCount = count; // required: copy() carries no instance count
const offsets = new Float32Array(count * 3).map(() => Math.random() * 10);
geometry.setAttribute("offset", new THREE.InstancedBufferAttribute(offsets, 3));
// With WebGLRenderer and a ShaderMaterial, declare `in vec3 offset;` in the vertex
// shader yourself and add it to position (see threejs-shaders). Under WebGPURenderer,
// per-instance data is a TSL node - see the WebGPU and TSL section.
BatchedMesh
多个不同几何体共享同一材质,每个实例都有自己的变换。在支持 WEBGL_multi_draw 的情况下,WebGLRenderer 会通过一次多重绘制调用绘制它们;WebGPU 后端则会为每个实例单独发起一次绘制调用。
const material = new THREE.MeshNormalMaterial();
const maxInstanceCount = 1000;
const maxVertexCount = 20000;
const maxIndexCount = 20000;
const batchedMesh = new THREE.BatchedMesh(maxInstanceCount, maxVertexCount, maxIndexCount, material);
const boxGeometryId = batchedMesh.addGeometry(new THREE.BoxGeometry(1, 1, 1));
const sphereGeometryId = batchedMesh.addGeometry(new THREE.SphereGeometry(0.5, 16, 16));
// A geometry renders only through its instances; added geometries need matching attributes and index state
const boxInstanceId = batchedMesh.addInstance(boxGeometryId);
batchedMesh.setMatrixAt(boxInstanceId, new THREE.Matrix4().setPosition(-2, 0, 0));
const sphereInstanceId = batchedMesh.addInstance(sphereGeometryId);
batchedMesh.setMatrixAt(sphereInstanceId, new THREE.Matrix4().setPosition(2, 0, 0));
scene.add(batchedMesh);
几何体工具
import * as BufferGeometryUtils from "three/addons/utils/BufferGeometryUtils.js";
import * as MikkTSpace from "three/addons/libs/mikktspace.module.js";
const geo1 = new THREE.BoxGeometry(1, 1, 1);
const geo2 = new THREE.SphereGeometry(0.5, 16, 16);
// Merge static geometries; inputs need the same attributes and must all be indexed or all non-indexed (else null).
// useGroups = true adds one group per input, for a material array such as [boxMaterial, sphereMaterial].
const merged = BufferGeometryUtils.mergeGeometries([geo1, geo2], true);
// Weld duplicate vertices; also generates an index for a non-indexed geometry
const welded = BufferGeometryUtils.mergeVertices(geo2);
// Tangents for normal mapping - the core method needs an index plus position, normal and uv
welded.computeTangents();
// MikkTSpace tangents (the convention glTF normal maps use) come from the addon; this makes merged non-indexed in place
await MikkTSpace.ready;
BufferGeometryUtils.computeMikkTSpaceTangents(merged, MikkTSpace);
变换几何体
const geometry = new THREE.TorusKnotGeometry(0.5, 0.15, 100, 16);
geometry.center(); // move the vertices so the bounding-box center lands on the origin
geometry.computeBoundingBox(); // refresh after vertex changes
const size = new THREE.Vector3();
geometry.boundingBox.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z);
geometry.scale(1 / maxDim, 1 / maxDim, 1 / maxDim); // fit inside a unit cube
const clone = geometry.clone(); // clone first: these methods mutate the geometry
clone.rotateX(Math.PI / 2);
clone.translate(0, 1, 0);
若要围绕某个点旋转或缩放对象而不触及顶点数据,请设置枢轴,而不是重新居中几何体:
// Rotation and scale normally use the object origin; pivot is a point in the object's own coordinates
mesh.pivot = new THREE.Vector3(1, 0, 0); // default null
mesh.rotation.y += 0.05; // now swings around the local point (1, 0, 0)
变形目标
const geometry = new THREE.BoxGeometry(1, 1, 1, 4, 4, 4);
const morphPositions = geometry.attributes.position.array.slice();
for (let i = 0; i < morphPositions.length; i += 3) {
morphPositions[i] *= 2; // stretch X
morphPositions[i + 1] *= 0.5; // squash Y
}
geometry.morphAttributes.position = [new THREE.BufferAttribute(morphPositions, 3)];
// Build the mesh after assigning morphAttributes so it picks up the targets
const mesh = new THREE.Mesh(geometry, new THREE.MeshNormalMaterial());
mesh.morphTargetInfluences[0] = 0.5; // 50% blend between base and target
scene.add(mesh);
性能与释放
- 减少绘制调用:对静态网格使用 mergeGeometries,对重复几何体使用 InstancedMesh,对多个不同几何体使用 BatchedMesh(在支持 WEBGL_multi_draw 的 WebGLRenderer 上使用一次多重绘制调用)。
- 分段数需要在平滑度和顶点数之间权衡;球体默认的 32 乘 16 适用于大多数场景,只有在近距离查看时才需要提高分段数。
scene.remove(mesh);
mesh.dispose(); // frees renderer state kept for this object only, not children or shared resources
mesh.geometry.dispose(); // frees the GPU buffers at once, even while another mesh shares them (that mesh uploads them again)
mesh.material.dispose(); // its textures need their own texture.dispose()
要释放整个子树,请参照“常见错误”中的方法遍历它。InstancedMesh.dispose() 还会释放其变形纹理。
WebGPU 和 TSL
在 WebGPURenderer 下,几何体类、InstancedMesh 和 BatchedMesh 保持上述 API 不变。不同之处在于:WebGPU 后端绘制的 Points 始终为 1 像素,并为每个 BatchedMesh 实例单独发起一次绘制调用;LineLoop 会报错并被跳过;每个实例的着色器数据使用 TSL 节点。
import * as THREE from "three/webgpu";
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // before any other use of the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111); // the WebGPU canvas is transparent by default
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 5);
renderer.setAnimationLoop(animate);
function animate() {
renderer.render(scene, camera);
}
WebGPU 后端绘制的点始终为 1 像素,不受材质大小影响。若要绘制更大的点,可为每个位置绘制一个 Sprite:
import * as THREE from "three/webgpu";
import { instancedBufferAttribute } from "three/tsl";
const count = 1000;
const positions = new Float32Array(count * 3).map(() => (Math.random() - 0.5) * 10);
const material = new THREE.PointsNodeMaterial({
positionNode: instancedBufferAttribute(new THREE.InstancedBufferAttribute(positions, 3)),
size: 0.5, // depth-scaled like PointsMaterial; CSS pixels with sizeAttenuation: false; sizeNode for a node
});
const points = new THREE.Sprite(material);
points.count = count; // how many instances the sprite draws
points.frustumCulled = false; // culling tests one 0.7-unit sphere at the sprite origin, not the positions
scene.add(points);
自定义的每实例属性使用 TSL 节点,而非 GLSL 声明。请基于 positionLocal 构建位移,它已包含实例变换:
import * as THREE from "three/webgpu";
import { instancedBufferAttribute, positionLocal } from "three/tsl";
const count = 1000;
const jitter = new THREE.InstancedBufferAttribute(new Float32Array(count * 3).map(() => Math.random() - 0.5), 3);
const material = new THREE.MeshBasicNodeMaterial();
material.positionNode = positionLocal.add(instancedBufferAttribute(jitter).mul(0.5)); // per-instance data, no GLSL
const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(0.2, 0.2, 0.2), material, count);
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
mesh.setMatrixAt(i, matrix.setPosition(Math.random() * 8 - 4, Math.random() * 8 - 4, Math.random() * 8 - 4));
}
scene.add(mesh);
常见错误
BufferGeometryUtils.computeTangents(geometry)→geometry.computeTangents():BufferGeometryUtils 未导出 computeTangents;核心方法需要索引以及 position、normal 和 uv 属性。new THREE.BufferAttribute(new Uint8Array(rgbBytes), 3)→new THREE.BufferAttribute(new Uint8Array(rgbBytes), 3, true):未归一化的字节会以 0.0 到 255.0 的值传入着色器,导致颜色过曝。geometry.copy(new THREE.BoxGeometry(1, 1, 1));→geometry.copy(new THREE.BoxGeometry(1, 1, 1)); geometry.instanceCount = count;:InstancedBufferGeometry.copy() 会用源对象的 undefined 覆盖 instanceCount,随后 WebGLRenderer 不会绘制任何实例。batchedMesh.addGeometry(boxGeometry)→batchedMesh.addInstance(batchedMesh.addGeometry(boxGeometry)):添加到 BatchedMesh 的几何体只有通过 addInstance 创建的实例才能渲染(自 r166 起为必需)。new THREE.LineLoop(geometry, material)→new THREE.Line(new THREE.BufferGeometry().setFromPoints([...linePoints, linePoints[0]]), material):WebGPURenderer 不支持 LineLoop;它会记录错误并跳过该对象,因此要重复第一个点来闭合 Line。import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js"→import { TextGeometry } from "three/addons/geometries/TextGeometry.js":文档所述的导入映射不会解析 examples/jsm 说明符;它们映射了 three/addons/,但没有映射 three/examples/。capsule.parameters.length→capsule.parameters.height:CapsuleGeometry 参数在 r176 中从length更名为height,并新增了第五个参数heightSegments。{ font: font, size: 1, height: 0.2 }→{ font: font, size: 1, depth: 0.2 }:TextGeometry 的挤出选项在 r163 中从height更名为depth;旧的 height 会被忽略,文本会按默认的 50 个单位挤出。new THREE.Points(cloud, new THREE.PointsMaterial({ size: 10 }))→new THREE.Sprite(new THREE.PointsNodeMaterial({ positionNode: instancedBufferAttribute(attr) })):无论 size 值是多少,WebGPU 后端绘制的 Points 都只有一个像素大,因此较大的点需要使用实例化 Sprite 形式。new THREE.ShaderMaterial({ vertexShader: offsetVertexShader })→material.positionNode = positionLocal.add(instancedBufferAttribute(offsetAttribute)):WebGPURenderer 没有 ShaderMaterial 的节点版本;它会记录材质不兼容并回退到普通的 NodeMaterial,因此每个实例的数据应使用 TSL 节点。material.positionNode = positionGeometry.add(offset)→material.positionNode = positionLocal.add(offset):positionLocal 已包含蒙皮、形态目标、批处理和实例化;positionGeometry 是原始属性。mesh.morphTargetInfluences[0] = 0.5→mesh.updateMorphTargets(); mesh.morphTargetInfluences[0] = 0.5;:只有网格在构造时检测到的形态属性才会有对应的影响值,因此向现有网格添加目标后要刷新这些值。gltf.scene.dispose()→gltf.scene.traverse((child) => { child.geometry?.dispose(); for (const material of [child.material ?? []].flat()) { for (const value of Object.values(material)) if (value?.isTexture) value.dispose(); material.dispose(); } child.dispose(); }):Object3D.dispose() 只会释放该对象自身的渲染器状态;它不会遍历子对象,也不会释放几何体、材质或纹理。new THREE.BoxBufferGeometry(1, 1, 1)→new THREE.BoxGeometry(1, 1, 1):内置形状的*BufferGeometry别名已不存在;所有内置几何体本身已经是 BufferGeometry。BufferGeometryUtils.mergeBufferGeometries([geo1, geo2])→BufferGeometryUtils.mergeGeometries([geo1, geo2]):旧名称未导出,因此调用会抛出 TypeError。geometry.setAttribute("uv2", geometry.attributes.uv)→material.aoMap = aoTexture;:aoMap 默认读取uv(通道 0);只有单独烘焙的展开贴图才使用uv1,并设置aoTexture.channel = 1。new THREE.LineBasicMaterial({ linewidth: 4 })→new Line2(new LineGeometry().setPositions(xyzArray), new LineMaterial({ linewidth: 4 })):无论 linewidth 设置为何值,WebGL 和 WebGPU 绘制的线图元都只有一个像素宽;three/addons/lines/ 中的 Line2、LineGeometry 和 LineMaterial 可在 WebGLRenderer 上绘制粗线,而在 WebGPURenderer 下,three/addons/lines/webgpu/Line2.js 中的 Line2 使用 three/webgpu 中的 Line2NodeMaterial。
另请参阅
threejs-fundamentals- 场景设置、渲染循环、Object3D 变换和资源释放。threejs-materials- 此处构建的网格所用的材质类型和设置。threejs-shaders- 用于 WebGLRenderer 的 ShaderMaterial 顶点和片段代码。threejs-animation- 随时间为变形目标影响值和变换添加动画。threejs-loaders- 带有自带几何体的 glTF 和其他格式。threejs-performance- 根据实测绘制调用,在合并、实例化、批处理和 LOD 之间进行选择。