{T}

自定义几何体

当内置几何体无法满足需求时,可以通过 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];
    // ... 复制其他顶点
  }
  
  // 创建新几何体...
}

最佳实践

  1. 使用索引:复用顶点减少内存占用,特别是平滑表面
  2. 合理分段:根据需求平衡质量和性能
  3. 及时计算法线:确保光照效果正确
  4. 动态更新优化:使用 DynamicDrawUsage 和部分更新
  5. 内存管理:不再使用时调用 dispose()
  6. 预分配数组:避免频繁的数组扩容操作
  7. 验证数据:创建后检查边界框确认数据正确

相关链接