自定义几何体
当内置几何体无法满足需求时,可以通过 BufferGeometry 创建自定义几何体,完全控制顶点数据、法线、UV 坐标等属性。
概述
BufferGeometry 是 Three.js 中几何体的基础类,使用类型化数组(TypedArray)存储顶点数据,具有更高的性能和灵活性。自定义几何体需要手动设置顶点位置、法线、UV 等属性。
数据结构
BufferGeometry 使用扁平化的类型化数组存储顶点数据:
code
顶点位置数组 (Float32Array)
┌─────────────────────────────────────────────────────────────┐
│ 顶点0 │ 顶点1 │ 顶点2 │ ... │ 顶点n │
│ x y z │ x y z │ x y z │ │ x y z │
└─────────────────────────────────────────────────────────────┘
索引: 0 1 2 3 4 5 6 7 8 3n 3n+1 3n+2
UV 坐标数组 (Float32Array)
┌───────────────────────────────────────┐
│ 顶点0 │ 顶点1 │ ... │ 顶点n │
│ u v │ u v │ │ u v │
└───────────────────────────────────────┘
索引: 0 1 2 3 2n 2n+1
索引缓冲区 (Uint16Array/Uint32Array)
┌─────────────────────────────────────────┐
│ 三角形0 │ 三角形1 │ ... │
│ idx0 idx1 idx2│ idx3 idx4 idx5│ │
└─────────────────────────────────────────┘BufferGeometry 基础
创建基本几何体
javascript
import * as THREE from 'three';
// 创建几何体实例
const geometry = new THREE.BufferGeometry();
// 定义顶点位置(三角形)
const vertices = new Float32Array([
0, 1, 0, // 顶点 0
-1, -1, 0, // 顶点 1
1, -1, 0 // 顶点 2
]);
// 设置顶点位置属性
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// 创建网格
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);BufferAttribute 详解
BufferAttribute 是存储顶点属性的核心类:
javascript
// 创建 BufferAttribute
const attribute = new THREE.BufferAttribute(
array: TypedArray, // 类型化数组
itemSize: number // 每个顶点的数据数量(位置为 3,UV 为 2)
);
// 示例:创建位置属性
const positions = new Float32Array([
0, 0, 0, // 顶点 0: (x, y, z)
1, 0, 0, // 顶点 1
1, 1, 0, // 顶点 2
0, 1, 0 // 顶点 3
]);
const positionAttribute = new THREE.BufferAttribute(positions, 3);
// 获取数据
const x = positionAttribute.getX(0); // 获取第 0 个顶点的 x 坐标
const y = positionAttribute.getY(0); // 获取第 0 个顶点的 y 坐标
const z = positionAttribute.getZ(0); // 获取第 0 个顶点的 z 坐标
// 设置数据
positionAttribute.setX(0, 2); // 设置第 0 个顶点的 x 坐标为 2
positionAttribute.setXYZ(0, 2, 3, 4); // 设置第 0 个顶点的坐标
// 更新数据后需要标记为需要更新
positionAttribute.needsUpdate = true;BufferAttribute 常用方法:
| 方法 | 说明 |
|---|---|
getX(index) | 获取指定顶点的 x 分量 |
getY(index) | 获取指定顶点的 y 分量 |
getZ(index) | 获取指定顶点的 z 分量 |
getW(index) | 获取指定顶点的 w 分量 |
setX(index, value) | 设置指定顶点的 x 分量 |
setY(index, value) | 设置指定顶点的 y 分量 |
setZ(index, value) | 设置指定顶点的 z 分量 |
setW(index, value) | 设置指定顶点的 w 分量 |
setXY(index, x, y) | 设置指定顶点的 x、y 分量 |
setXYZ(index, x, y, z) | 设置指定顶点的 x、y、z 分量 |
setXYZW(index, x, y, z, w) | 设置指定顶点的全部四个分量 |
顶点属性
顶点位置(Position)
顶点位置是最基本的属性,定义几何体的形状:
javascript
// 定义立方体的顶点(6 个面,每面 2 个三角形,共 36 个顶点)
// 注意:这是非索引模式,每个三角形独立定义顶点
const vertices = new Float32Array([
// 前面(Z+)
-1, -1, 1, 1, -1, 1, 1, 1, 1, // 三角形 1
-1, -1, 1, 1, 1, 1, -1, 1, 1, // 三角形 2
// 后面(Z-)
-1, -1, -1, -1, 1, -1, 1, 1, -1,
-1, -1, -1, 1, 1, -1, 1, -1, -1,
// 顶面(Y+)
-1, 1, -1, -1, 1, 1, 1, 1, 1,
-1, 1, -1, 1, 1, 1, 1, 1, -1,
// 底面(Y-)
-1, -1, -1, 1, -1, -1, 1, -1, 1,
-1, -1, -1, 1, -1, 1, -1, -1, 1,
// 右面(X+)
1, -1, -1, 1, 1, -1, 1, 1, 1,
1, -1, -1, 1, 1, 1, 1, -1, 1,
// 左面(X-)
-1, -1, -1, -1, -1, 1, -1, 1, 1,
-1, -1, -1, -1, 1, 1, -1, 1, -1
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));顶点法线(Normal)
法线决定了光照效果,每个顶点都需要法线信息:
javascript
// 手动设置法线
const normals = new Float32Array([
// 前面法线(指向 +Z)
0, 0, 1, 0, 0, 1, 0, 0, 1,
0, 0, 1, 0, 0, 1, 0, 0, 1,
// 后面法线(指向 -Z)
0, 0, -1, 0, 0, -1, 0, 0, -1,
0, 0, -1, 0, 0, -1, 0, 0, -1,
// ... 其他面的法线
]);
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
// 或者自动计算法线(适用于平滑表面)
geometry.computeVertexNormals();法线计算说明
computeVertexNormals():基于相邻面的法线计算平均法线,产生平滑效果- 手动设置法线:可实现硬边效果或自定义光照行为
UV 坐标
UV 坐标用于纹理映射,范围 [0, 1],左下角为原点:
javascript
// 设置 UV 坐标(每个顶点对应一个 UV 坐标)
const uvs = new Float32Array([
// 前面 UV(对应前面的 6 个顶点)
0, 0, 1, 0, 1, 1, // 三角形 1
0, 0, 1, 1, 0, 1, // 三角形 2
// 后面 UV
0, 0, 1, 0, 1, 1,
0, 0, 1, 1, 0, 1,
// ... 其他面的 UV
]);
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
// 多组 UV 坐标(如光照贴图使用第二组 UV)
const uv2 = new Float32Array([...]);
geometry.setAttribute('uv2', new THREE.BufferAttribute(uv2, 2));UV 坐标示意图:
code
纹理坐标系统:
V
│
1 ┌─────────────┐
│ │
│ │
│ │
0 └─────────────┴─ U
0 1
映射示例:
┌─────────────┐
│ (0,1) (1,1) │ 顶点 UV 坐标
│ │
│ (0,0) (1,0) │
└─────────────┘顶点颜色
顶点颜色可以实现渐变或多彩效果:
javascript
// 设置顶点颜色(RGB 格式,范围 0-1)
const colors = new Float32Array([
// 前面颜色
1, 0, 0, // 红色
0, 1, 0, // 绿色
0, 0, 1, // 蓝色
1, 0, 0,
0, 0, 1,
1, 1, 0, // 黄色
// ... 其他面的颜色
]);
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// 在材质中启用顶点颜色
const material = new THREE.MeshBasicMaterial({
vertexColors: true // 启用顶点颜色
});顶点索引(Index)
使用索引可以复用顶点,显著减少数据量:
javascript
// 定义顶点(8 个顶点定义立方体的 8 个角)
const vertices = new Float32Array([
-1, -1, -1, // 0: 左下后
1, -1, -1, // 1: 右下后
1, 1, -1, // 2: 右上后
-1, 1, -1, // 3: 左上后
-1, -1, 1, // 4: 左下前
1, -1, 1, // 5: 右下前
1, 1, 1, // 6: 右上前
-1, 1, 1 // 7: 左上前
]);
// 定义索引(每个面 2 个三角形,共 12 个三角形)
const indices = new Uint16Array([
0, 1, 2, 0, 2, 3, // 后面
4, 6, 5, 4, 7, 6, // 前面
0, 4, 5, 0, 5, 1, // 底面
2, 6, 7, 2, 7, 3, // 顶面
0, 3, 7, 0, 7, 4, // 左面
1, 5, 6, 1, 6, 2 // 右面
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
// 注意:使用索引时,法线和 UV 需要按顶点数设置
// 如果需要硬边效果,应该使用非索引模式索引与非索引对比:
| 特性 | 索引模式 | 非索引模式 |
|---|---|---|
| 顶点数据量 | 少 | 多 |
| 内存占用 | 低 | 高 |
| 硬边效果 | 需要拆分顶点 | 天然支持 |
| 适用场景 | 平滑表面 | 硬边表面 |
创建自定义几何体示例
创建三角形
最简单的自定义几何体:
javascript
function createTriangle() {
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
0, 1, 0, // 顶部顶点
-1, -1, 0, // 左下顶点
1, -1, 0 // 右下顶点
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.computeVertexNormals(); // 自动计算法线
return geometry;
}
// 使用
const triangleGeometry = createTriangle();
const triangleMesh = new THREE.Mesh(triangleGeometry, material);
scene.add(triangleMesh);创建平面网格
可自定义分段数的平面:
javascript
function createPlaneGrid(width, height, segmentsX, segmentsY) {
const geometry = new THREE.BufferGeometry();
const vertices = [];
const indices = [];
const normals = [];
const uvs = [];
// 生成顶点
for (let y = 0; y <= segmentsY; y++) {
for (let x = 0; x <= segmentsX; x++) {
const px = (x / segmentsX) * width - width / 2;
const py = (y / segmentsY) * height - height / 2;
vertices.push(px, 0, py);
normals.push(0, 1, 0); // 朝上的法线
uvs.push(x / segmentsX, y / segmentsY);
}
}
// 生成索引
for (let y = 0; y < segmentsY; y++) {
for (let x = 0; x < segmentsX; x++) {
const a = y * (segmentsX + 1) + x;
const b = a + 1;
const c = a + segmentsX + 1;
const d = c + 1;
indices.push(a, c, b); // 三角形 1
indices.push(b, c, d); // 三角形 2
}
}
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2));
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));
return geometry;
}
// 使用
const planeGeometry = createPlaneGrid(10, 10, 20, 20);
const planeMesh = new THREE.Mesh(planeGeometry, material);
scene.add(planeMesh);创建球体
使用参数化方程创建球体:
javascript
function createCustomSphere(radius, widthSegments, heightSegments) {
const geometry = new THREE.BufferGeometry();
const vertices = [];
const normals = [];
const uvs = [];
const indices = [];
// 生成顶点
for (let y = 0; y <= heightSegments; y++) {
const v = y / heightSegments;
const theta = v * Math.PI; // 0 到 π
for (let x = 0; x <= widthSegments; x++) {
const u = x / widthSegments;
const phi = u * Math.PI * 2; // 0 到 2π
// 球面坐标转笛卡尔坐标
const px = -radius * Math.cos(phi) * Math.sin(theta);
const py = radius * Math.cos(theta);
const pz = radius * Math.sin(phi) * Math.sin(theta);
vertices.push(px, py, pz);
// 法线(从球心指向表面)
normals.push(px / radius, py / radius, pz / radius);
// UV 坐标
uvs.push(u, 1 - v);
}
}
// 生成索引
for (let y = 0; y < heightSegments; y++) {
for (let x = 0; x < widthSegments; x++) {
const a = y * (widthSegments + 1) + x;
const b = a + 1;
const c = a + widthSegments + 1;
const d = c + 1;
indices.push(a, c, b);
indices.push(b, c, d);
}
}
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2));
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));
return geometry;
}创建圆环
javascript
function createCustomTorus(radius, tube, radialSegments, tubularSegments) {
const geometry = new THREE.BufferGeometry();
const vertices = [];
const normals = [];
const uvs = [];
const indices = [];
// 生成顶点
for (let j = 0; j <= radialSegments; j++) {
for (let i = 0; i <= tubularSegments; i++) {
const u = i / tubularSegments * Math.PI * 2;
const v = j / radialSegments * Math.PI * 2;
// 圆环参数方程
const px = (radius + tube * Math.cos(v)) * Math.cos(u);
const py = tube * Math.sin(v);
const pz = (radius + tube * Math.cos(v)) * Math.sin(u);
vertices.push(px, py, pz);
// 计算法线
const cx = radius * Math.cos(u);
const cz = radius * Math.sin(u);
const nx = px - cx;
const ny = py;
const nz = pz - cz;
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
normals.push(nx / len, ny / len, nz / len);
uvs.push(i / tubularSegments, j / radialSegments);
}
}
// 生成索引
for (let j = 1; j <= radialSegments; j++) {
for (let i = 1; i <= tubularSegments; i++) {
const a = (tubularSegments + 1) * j + i - 1;
const b = (tubularSegments + 1) * (j - 1) + i - 1;
const c = (tubularSegments + 1) * (j - 1) + i;
const d = (tubularSegments + 1) * j + i;
indices.push(a, b, d);
indices.push(b, c, d);
}
}
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2));
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));
return geometry;
}创建参数化曲面
通过自定义函数创建任意曲面:
javascript
function createParametricSurface(func, segments) {
const geometry = new THREE.BufferGeometry();
const vertices = [];
const normals = [];
const uvs = [];
const indices = [];
// 生成顶点
for (let i = 0; i <= segments; i++) {
for (let j = 0; j <= segments; j++) {
const u = i / segments;
const v = j / segments;
const point = func(u, v);
vertices.push(point.x, point.y, point.z);
uvs.push(u, v);
}
}
// 使用数值微分计算法线
for (let i = 0; i <= segments; i++) {
for (let j = 0; j <= segments; j++) {
const u = i / segments;
const v = j / segments;
const delta = 0.001;
const p1 = func(u - delta, v);
const p2 = func(u + delta, v);
const p3 = func(u, v - delta);
const p4 = func(u, v + delta);
// 计算切向量
const tangentU = new THREE.Vector3(
(p2.x - p1.x) / (2 * delta),
(p2.y - p1.y) / (2 * delta),
(p2.z - p1.z) / (2 * delta)
);
const tangentV = new THREE.Vector3(
(p4.x - p3.x) / (2 * delta),
(p4.y - p3.y) / (2 * delta),
(p4.z - p3.z) / (2 * delta)
);
// 法线 = 切向量叉积
const normal = new THREE.Vector3().crossVectors(tangentU, tangentV).normalize();
normals.push(normal.x, normal.y, normal.z);
}
}
// 生成索引
for (let i = 0; i < segments; i++) {
for (let j = 0; j < segments; j++) {
const a = i * (segments + 1) + j;
const b = a + 1;
const c = a + segments + 1;
const d = c + 1;
indices.push(a, c, b);
indices.push(b, c, d);
}
}
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2));
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));
return geometry;
}
// 示例:创建波浪曲面
const waveGeometry = createParametricSurface((u, v) => {
const x = (u - 0.5) * 4;
const z = (v - 0.5) * 4;
const y = Math.sin(x * 2) * Math.cos(z * 2) * 0.5;
return new THREE.Vector3(x, y, z);
}, 50);
// 示例:创建马鞍面
const saddleGeometry = createParametricSurface((u, v) => {
const x = (u - 0.5) * 4;
const z = (v - 0.5) * 4;
const y = x * x - z * z;
return new THREE.Vector3(x, y * 0.1, z);
}, 50);动态更新几何体
更新顶点位置
动态修改顶点可以实现变形效果:
javascript
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
0, 1, 0,
-1, -1, 0,
1, -1, 0
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// 动态更新顶点
function updateVertices(time) {
const positions = geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
// 根据时间和位置计算新的 y 值
positions[i + 1] += Math.sin(time + i) * 0.01;
}
// 标记需要更新
geometry.attributes.position.needsUpdate = true;
}
// 在动画循环中调用
function animate() {
requestAnimationFrame(animate);
updateVertices(Date.now() * 0.001);
renderer.render(scene, camera);
}创建动态地形
javascript
function createTerrain(width, height, segments) {
const geometry = new THREE.BufferGeometry();
const vertices = [];
const indices = [];
// 生成顶点
for (let y = 0; y <= segments; y++) {
for (let x = 0; x <= segments; x++) {
const px = (x / segments) * width - width / 2;
const pz = (y / segments) * height - height / 2;
const py = 0; // 初始高度为 0
vertices.push(px, py, pz);
}
}
// 生成索引
for (let y = 0; y < segments; y++) {
for (let x = 0; x < segments; x++) {
const a = y * (segments + 1) + x;
const b = a + 1;
const c = a + segments + 1;
const d = c + 1;
indices.push(a, c, b);
indices.push(b, c, d);
}
}
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));
geometry.computeVertexNormals();
return geometry;
}
// 更新地形高度
function updateTerrainHeight(geometry, time) {
const positions = geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const z = positions[i + 2];
// 使用简化的噪声函数生成高度
positions[i + 1] = Math.sin(x * 0.5 + time) * Math.cos(z * 0.5 + time) * 0.5;
}
geometry.attributes.position.needsUpdate = true;
geometry.computeVertexNormals(); // 更新法线
}几何体合并
合并多个几何体可以减少 draw call,提升性能:
javascript
function mergeGeometries(geometries) {
const positions = [];
const normals = [];
const uvs = [];
const indices = [];
let indexOffset = 0;
for (const geometry of geometries) {
// 获取属性
const position = geometry.attributes.position.array;
const normal = geometry.attributes.normal?.array;
const uv = geometry.attributes.uv?.array;
// 添加顶点数据
positions.push(...position);
if (normal) normals.push(...normal);
if (uv) uvs.push(...uv);
// 处理索引
if (geometry.index) {
const index = geometry.index.array;
for (let i = 0; i < index.length; i++) {
indices.push(index[i] + indexOffset);
}
} else {
const vertexCount = position.length / 3;
for (let i = 0; i < vertexCount; i++) {
indices.push(i + indexOffset);
}
}
indexOffset += position.length / 3;
}
// 创建合并后的几何体
const mergedGeometry = new THREE.BufferGeometry();
mergedGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
if (normals.length > 0) {
mergedGeometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
}
if (uvs.length > 0) {
mergedGeometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2));
}
mergedGeometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));
return mergedGeometry;
}
// 使用示例
const box = new THREE.BoxGeometry(1, 1, 1);
const sphere = new THREE.SphereGeometry(0.5, 16, 16);
sphere.translate(2, 0, 0);
const merged = mergeGeometries([box, sphere]);
const mergedMesh = new THREE.Mesh(merged, material);
scene.add(mergedMesh);推荐使用 BufferGeometryUtils
Three.js 提供了 BufferGeometryUtils.mergeBufferGeometries() 工具函数,建议优先使用:
javascript
import { mergeBufferGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
const merged = mergeBufferGeometries([geo1, geo2, geo3]);性能优化
使用类型化数组
预分配内存可以提高性能:
javascript
// 预分配内存
const vertexCount = 1000;
const positions = new Float32Array(vertexCount * 3);
const normals = new Float32Array(vertexCount * 3);
const uvs = new Float32Array(vertexCount * 2);
// 填充数据
for (let i = 0; i < vertexCount; i++) {
const i3 = i * 3;
const i2 = i * 2;
positions[i3] = Math.random() * 10;
positions[i3 + 1] = Math.random() * 10;
positions[i3 + 2] = Math.random() * 10;
normals[i3] = 0;
normals[i3 + 1] = 1;
normals[i3 + 2] = 0;
uvs[i2] = Math.random();
uvs[i2 + 1] = Math.random();
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));动态缓冲区
对于频繁更新的几何体,使用动态绘制模式:
javascript
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(1000 * 3);
const positionAttribute = new THREE.BufferAttribute(positions, 3);
positionAttribute.setUsage(THREE.DynamicDrawUsage); // 标记为动态
geometry.setAttribute('position', positionAttribute);
// 更新时只更新需要的部分
function updatePosition(index, x, y, z) {
positionAttribute.setXYZ(index, x, y, z);
positionAttribute.needsUpdate = true;
}BufferUsage 类型:
| 常量 | 说明 | 适用场景 |
|---|---|---|
StaticDrawUsage | 一次设置,多次使用 | 静态几何体 |
DynamicDrawUsage | 多次修改,多次使用 | 动态几何体 |
StreamDrawUsage | 每帧都更新 | 粒子系统 |
常见问题解答
Q: 为什么我的自定义几何体是黑色的?
A: 可能缺少法线数据。法线用于光照计算,如果没有法线,几何体在光照下会显示不正确:
javascript
// 解决方法:计算法线
geometry.computeVertexNormals();
// 或者手动设置法线
const normals = new Float32Array([...]);
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));Q: 如何调试几何体的顶点数据?
A: 可以遍历并打印顶点信息:
javascript
const positions = geometry.attributes.position;
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i);
const y = positions.getY(i);
const z = positions.getZ(i);
console.log(`顶点 ${i}: (${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)})`);
}
// 检查边界
geometry.computeBoundingBox();
console.log('边界框:', geometry.boundingBox);Q: 索引缓冲区的数据类型如何选择?
A: 根据顶点数量选择:
javascript
// 顶点数 <= 65535:使用 Uint16Array
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));
// 顶点数 > 65535:使用 Uint32Array
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));Q: 为什么使用索引后纹理映射不正确?
A: 索引模式下,共享顶点共享所有属性(包括 UV)。如果需要不同 UV,应该使用非索引模式或拆分顶点:
javascript
// 立方体的角落顶点在三个面上需要不同的 UV
// 此时应该使用非索引模式,每个面独立定义顶点Q: 如何创建双面渲染的几何体?
A: 在材质中设置 side 属性,或者在几何体中复制一份翻转法线的面:
javascript
// 方法一:材质设置(推荐)
const material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide
});
// 方法二:几何体层面创建双面
function createDoubleSidedGeometry(geometry) {
const positions = geometry.attributes.position.array;
const doublePositions = new Float32Array(positions.length * 2);
// 正面
doublePositions.set(positions, 0);
// 反面(翻转三角形顺序)
for (let i = 0; i < positions.length; i += 9) {
doublePositions[positions.length + i] = positions[i];
doublePositions[positions.length + i + 1] = positions[i + 2];
doublePositions[positions.length + i + 2] = positions[i + 1];
// ... 复制其他顶点
}
// 创建新几何体...
}最佳实践
- 使用索引:复用顶点减少内存占用,特别是平滑表面
- 合理分段:根据需求平衡质量和性能
- 及时计算法线:确保光照效果正确
- 动态更新优化:使用
DynamicDrawUsage和部分更新 - 内存管理:不再使用时调用
dispose() - 预分配数组:避免频繁的数组扩容操作
- 验证数据:创建后检查边界框确认数据正确