{T}

几何体操作

掌握几何体的各种操作方法,包括变换、合并、克隆、布尔运算等,可以更灵活地创建和管理 3D 模型。

概述

几何体操作是 Three.js 开发中的重要技能。通过变换、合并、克隆等操作,可以优化场景性能,创建复杂的形状,以及动态修改模型。

操作分类

code
几何体操作
├── 变换操作
│   ├── 平移 (translate)
│   ├── 旋转 (rotateX/Y/Z)
│   ├── 缩放 (scale)
│   └── 居中 (center)
├── 组合操作
│   ├── 合并 (merge)
│   ├── 克隆 (clone)
│   └── 布尔运算 (CSG)
├── 修改操作
│   ├── 切割 (clip/slice)
│   ├── 细分 (tessellate)
│   └── 简化 (simplify)
└── 提取操作
    ├── 边缘提取 (EdgesGeometry)
    └── 线框提取 (WireframeGeometry)

几何体变换

几何体变换直接修改顶点数据,与 Mesh 的变换属性不同。

变换原理

code
原始顶点数据                    变换后顶点数据
┌─────────────────┐            ┌─────────────────┐
│ 顶点0: (0, 0, 0) │  translate │ 顶点0: (2, 0, 0) │
│ 顶点1: (1, 0, 0) │  ───────> │ 顶点1: (3, 0, 0) │
│ 顶点2: (1, 1, 0) │   (2,0,0) │ 顶点2: (3, 1, 0) │
└─────────────────┘            └─────────────────┘

平移(Translate)

javascript
const geometry = new THREE.BoxGeometry(1, 1, 1);

// 平移几何体(修改顶点位置)
geometry.translate(x, y, z);

// 示例:向右移动 2 个单位
geometry.translate(2, 0, 0);

// 示例:向上移动 3 个单位
geometry.translate(0, 3, 0);

// 示例:移动到指定位置
geometry.translate(5, 2, -3);
注意

几何体变换会永久修改顶点数据。如果需要可逆变换,请使用 Mesh 的 positionrotationscale 属性。

旋转(Rotate)

javascript
// 绕 X 轴旋转(弧度)
geometry.rotateX(Math.PI / 4);  // 旋转 45 度

// 绕 Y 轴旋转
geometry.rotateY(Math.PI / 2);  // 旋转 90 度

// 绕 Z 轴旋转
geometry.rotateZ(Math.PI / 6);  // 旋转 30 度

// 示例:创建倾斜的立方体
const box = new THREE.BoxGeometry(1, 1, 1);
box.rotateX(Math.PI / 6);  // 前倾 30 度
box.rotateZ(Math.PI / 8);  // 侧倾 22.5 度

缩放(Scale)

javascript
// 整体缩放
geometry.scale(2, 2, 2);  // 各方向放大 2 倍

// 不同轴向缩放
geometry.scale(1, 2, 1);  // Y 轴放大 2 倍

// 缩小
geometry.scale(0.5, 0.5, 0.5);  // 缩小为原来的一半

// 镜像
geometry.scale(-1, 1, 1);  // X 轴镜像

居中(Center)

将几何体中心移到世界坐标原点:

javascript
const geometry = new THREE.BoxGeometry(2, 2, 2);

// 将几何体中心移到原点
geometry.center();

// 验证
geometry.computeBoundingBox();
console.log('边界框中心:', geometry.boundingBox.getCenter(new THREE.Vector3()));
// 输出: Vector3 {x: 0, y: 0, z: 0}

变换方法对比

方法操作对象是否可逆影响范围
geometry.translate()顶点数据所有使用该几何体的网格
mesh.position变换矩阵仅当前网格
geometry.rotateX/Y/Z()顶点数据所有使用该几何体的网格
mesh.rotation变换矩阵仅当前网格

几何体合并

合并几何体可以减少 draw call,显著提升渲染性能。

合并原理

code
合并前:                          合并后:
场景                              场景
├── Mesh A (draw call 1)          └── 合并后的 Mesh
├── Mesh B (draw call 2)              ├── 几何体 A 的顶点
├── Mesh C (draw call 3)              ├── 几何体 B 的顶点
...                                   └── 几何体 C 的顶点
                                    (仅 1 个 draw call)

使用 BufferGeometryUtils

javascript
import * as THREE from 'three';
import { mergeBufferGeometries } from 'three/addons/utils/BufferGeometryUtils.js';

// 创建多个几何体
const box1 = new THREE.BoxGeometry(1, 1, 1);
box1.translate(-2, 0, 0);

const box2 = new THREE.BoxGeometry(1, 1, 1);
box2.translate(0, 0, 0);

const box3 = new THREE.BoxGeometry(1, 1, 1);
box3.translate(2, 0, 0);

// 合并几何体
const mergedGeometry = mergeBufferGeometries([box1, box2, box3]);

// 创建单个网格
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const mergedMesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mergedMesh);

批量合并优化

javascript
// ❌ 不推荐:1000 个独立网格 = 1000 个 draw call
for (let i = 0; i < 1000; i++) {
  const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
  const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
  const cube = new THREE.Mesh(geometry, material);
  cube.position.set(
    Math.random() * 10 - 5,
    Math.random() * 10 - 5,
    Math.random() * 10 - 5
  );
  scene.add(cube);
}

// ✅ 推荐:合并为 1 个网格 = 1 个 draw call
const geometries = [];
for (let i = 0; i < 1000; i++) {
  const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
  geometry.translate(
    Math.random() * 10 - 5,
    Math.random() * 10 - 5,
    Math.random() * 10 - 5
  );
  geometries.push(geometry);
}

const mergedGeometry = mergeBufferGeometries(geometries);
const mergedMesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mergedMesh);

合并不同类型的几何体

javascript
import { mergeBufferGeometries } from 'three/addons/utils/BufferGeometryUtils.js';

const box = new THREE.BoxGeometry(1, 2, 1);
const sphere = new THREE.SphereGeometry(0.5, 16, 16);
sphere.translate(0, 1, 0);  // 移动球体到立方体上方

// 合并
const merged = mergeBufferGeometries([box, sphere]);

// 注意:合并的几何体必须具有相同的属性
// 例如都有 position、normal、uv 属性

按材质分组

当需要不同材质时,按材质分组合并:

javascript
// 按材质分组
const geometriesByMaterial = {
  red: [],
  green: [],
  blue: []
};

for (let i = 0; i < 100; i++) {
  const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
  geometry.translate(
    Math.random() * 10 - 5,
    Math.random() * 10 - 5,
    Math.random() * 10 - 5
  );
  
  const color = ['red', 'green', 'blue'][Math.floor(Math.random() * 3)];
  geometriesByMaterial[color].push(geometry);
}

// 为每个颜色组创建合并后的网格
const materials = {
  red: new THREE.MeshBasicMaterial({ color: 0xff0000 }),
  green: new THREE.MeshBasicMaterial({ color: 0x00ff00 }),
  blue: new THREE.MeshBasicMaterial({ color: 0x0000ff })
};

for (const [color, geometries] of Object.entries(geometriesByMaterial)) {
  if (geometries.length > 0) {
    const mergedGeometry = mergeBufferGeometries(geometries);
    const mesh = new THREE.Mesh(mergedGeometry, materials[color]);
    scene.add(mesh);
  }
}

几何体克隆

clone() 方法

创建几何体的浅拷贝:

javascript
const original = new THREE.BoxGeometry(1, 1, 1);

// 克隆几何体
const cloned = original.clone();

// 修改克隆体不影响原几何体
cloned.translate(2, 0, 0);
cloned.scale(2, 2, 2);

console.log(original.boundingBox);  // 保持不变
console.log(cloned.boundingBox);    // 已被修改

深拷贝

完全复制几何体及其所有数据:

javascript
function deepCloneGeometry(geometry) {
  const clonedGeometry = new THREE.BufferGeometry();
  
  // 复制所有属性
  const attributes = geometry.attributes;
  for (const key in attributes) {
    const attribute = attributes[key];
    const clonedAttribute = new THREE.BufferAttribute(
      attribute.array.slice(),  // 复制数组数据
      attribute.itemSize,
      attribute.normalized
    );
    clonedGeometry.setAttribute(key, clonedAttribute);
  }
  
  // 复制索引
  if (geometry.index) {
    clonedGeometry.setIndex(
      new THREE.BufferAttribute(
        geometry.index.array.slice(),
        1
      )
    );
  }
  
  // 复制边界信息
  if (geometry.boundingBox) {
    clonedGeometry.boundingBox = geometry.boundingBox.clone();
  }
  if (geometry.boundingSphere) {
    clonedGeometry.boundingSphere = geometry.boundingSphere.clone();
  }
  
  // 复制其他属性
  clonedGeometry.groups = geometry.groups.map(g => ({ ...g }));
  clonedGeometry.drawRange = { ...geometry.drawRange };
  
  return clonedGeometry;
}

clone() vs 深拷贝对比

javascript
const geo1 = new THREE.BoxGeometry(1, 1, 1);
const geo2 = geo1.clone();

// clone() 共享底层数组
console.log(geo1.attributes.position.array === geo2.attributes.position.array);
// false - BufferGeometry.clone() 会复制数组

几何体布尔运算

布尔运算可以通过组合简单几何体创建复杂形状。

布尔运算类型

code
并集 (Union)      交集 (Intersect)    差集 (Subtract)
┌─────┐           ┌─────┐             ┌─────┐
│ ┌───┼───┐       │     │             │ ┌───┼───┐
│ │ A │ B │  ==>  │  A∩B│             │ │ A │   │
│ │   │   │       │     │             │ │   │   │
└─┼───┴───┘       └─────┘             └─┼───┴───┘
  └─────┘                               └─ B 被减去

使用 CSG 库

javascript
import { CSG } from 'three-csg-ts';

// 创建两个几何体
const box = new THREE.BoxGeometry(2, 2, 2);
const sphere = new THREE.SphereGeometry(1.2, 32, 32);

// 创建网格
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const boxMesh = new THREE.Mesh(box, material);
const sphereMesh = new THREE.Mesh(sphere, material);

// 更新矩阵(重要!)
boxMesh.updateMatrix();
sphereMesh.updateMatrix();

// 并集:A + B
const unionResult = CSG.union(boxMesh, sphereMesh);

// 交集:A ∩ B
const intersectResult = CSG.intersect(boxMesh, sphereMesh);

// 差集:A - B
const subtractResult = CSG.subtract(boxMesh, sphereMesh);

// 使用结果
scene.add(unionResult);

使用 three-bvh-csg(更高性能)

javascript
import { SUBTRACTION, ADDITION, INTERSECTION, Evaluator, Brush } from 'three-bvh-csg';

const box = new THREE.BoxGeometry(2, 2, 2);
const sphere = new THREE.SphereGeometry(1.2, 32, 32);

// 创建 Brush 对象
const brush1 = new Brush(box);
const brush2 = new Brush(sphere);

// 更新矩阵
brush1.updateMatrixWorld();
brush2.updateMatrixWorld();

// 创建评估器
const evaluator = new Evaluator();

// 执行运算
const result = evaluator.evaluate(brush1, brush2, SUBTRACTION);

scene.add(result);

布尔运算示例

javascript
// 创建带有圆柱孔的立方体
function createBoxWithHole() {
  const box = new THREE.BoxGeometry(2, 2, 2);
  const cylinder = new THREE.CylinderGeometry(0.5, 0.5, 3, 32);
  
  const boxMesh = new THREE.Mesh(box);
  const cylinderMesh = new THREE.Mesh(cylinder);
  
  boxMesh.updateMatrix();
  cylinderMesh.updateMatrix();
  
  // 差集:从立方体中减去圆柱
  const result = CSG.subtract(boxMesh, cylinderMesh);
  
  return result;
}

// 创建十字形状
function createCrossShape() {
  const box1 = new THREE.BoxGeometry(0.5, 2, 0.5);
  const box2 = new THREE.BoxGeometry(2, 0.5, 0.5);
  
  const mesh1 = new THREE.Mesh(box1);
  const mesh2 = new THREE.Mesh(box2);
  
  mesh1.updateMatrix();
  mesh2.updateMatrix();
  
  // 并集:两个立方体合并
  const result = CSG.union(mesh1, mesh2);
  
  return result;
}

// 创建镂空球体
function createHollowSphere() {
  const outerSphere = new THREE.SphereGeometry(2, 32, 32);
  const innerSphere = new THREE.SphereGeometry(1.8, 32, 32);
  
  const outerMesh = new THREE.Mesh(outerSphere);
  const innerMesh = new THREE.Mesh(innerSphere);
  
  outerMesh.updateMatrix();
  innerMesh.updateMatrix();
  
  // 差集:从外部球体减去内部球体
  return CSG.subtract(outerMesh, innerMesh);
}

几何体切割

平面切割(Clipping Planes)

使用材质的剪切平面实现视觉切割:

javascript
import * as THREE from 'three';

// 创建剪切平面
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
// 平面法线 (0,1,0) 表示水平切面,常量 0 表示通过原点

// 创建几何体
const geometry = new THREE.SphereGeometry(1, 32, 32);

// 创建材质(启用剪切)
const material = new THREE.MeshStandardMaterial({
  color: 0x00ff00,
  side: THREE.DoubleSide,      // 双面渲染
  clippingPlanes: [plane],     // 设置剪切平面
  clipShadows: true            // 阴影也应用剪切
});

// 启用渲染器剪切
renderer.localClippingEnabled = true;

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 动态调整剪切平面
function animate() {
  requestAnimationFrame(animate);
  
  // 移动剪切平面
  plane.constant = Math.sin(Date.now() * 0.001) * 0.5;
  
  renderer.render(scene, camera);
}

多平面切割

javascript
// 创建多个剪切平面
const clipPlanes = [
  new THREE.Plane(new THREE.Vector3(1, 0, 0), 0),   // X 切面
  new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),   // Y 切面
  new THREE.Plane(new THREE.Vector3(0, 0, 1), 0)    // Z 切面
];

const material = new THREE.MeshStandardMaterial({
  color: 0x00ff00,
  side: THREE.DoubleSide,
  clippingPlanes: clipPlanes,
  clipIntersection: false  // false: 并集剪切,true: 交集剪切
});

renderer.localClippingEnabled = true;

切割平面可视化

javascript
import { PlaneHelper } from 'three';

// 创建平面辅助显示
const planeHelper = new THREE.PlaneHelper(plane, 2, 0xff0000);
scene.add(planeHelper);

几何体细分

TessellateModifier(细分修改器)

增加几何体的三角形数量:

javascript
import { TessellateModifier } from 'three/addons/modifiers/TessellateModifier.js';

const geometry = new THREE.BoxGeometry(1, 1, 1);

// 创建细分修改器
// 参数:最大边长,超过此长度的边会被细分
const tessellateModifier = new TessellateModifier(0.5);

// 应用细分
const tessellatedGeometry = tessellateModifier.modify(geometry);

console.log('原始顶点数:', geometry.attributes.position.count);
console.log('细分后顶点数:', tessellatedGeometry.attributes.position.count);

细分应用场景

javascript
// 用于爆炸效果:细分后每个三角形可以作为独立粒子
function createExplodableMesh(geometry) {
  const modifier = new TessellateModifier(0.2);
  const tessellated = modifier.modify(geometry);
  
  // 创建粒子系统或独立网格...
}

// 用于变形动画:细分后更平滑
function createMorphableMesh(geometry) {
  const modifier = new TessellateModifier(0.3);
  return modifier.modify(geometry);
}

几何体简化

SimplifyModifier(简化修改器)

减少几何体的三角形数量:

javascript
import { SimplifyModifier } from 'three/addons/modifiers/SimplifyModifier.js';

const geometry = new THREE.SphereGeometry(1, 32, 32);

console.log('原始顶点数:', geometry.attributes.position.count);

// 创建简化修改器
const simplifyModifier = new SimplifyModifier();

// 简化(减少 50% 的顶点)
const simplifiedGeometry = simplifyModifier.modify(
  geometry,
  Math.floor(geometry.attributes.position.count * 0.5)
);

console.log('简化后顶点数:', simplifiedGeometry.attributes.position.count);

const mesh = new THREE.Mesh(simplifiedGeometry, material);
scene.add(mesh);

LOD(细节层次)实现

javascript
function createLODGeometry(originalGeometry, levels) {
  const simplifyModifier = new SimplifyModifier();
  const lodGeometries = [];
  
  let currentGeometry = originalGeometry.clone();
  const vertexCount = currentGeometry.attributes.position.count;
  
  for (let i = 0; i < levels; i++) {
    const targetCount = Math.floor(vertexCount * (1 - (i + 1) / (levels + 1)));
    const simplified = simplifyModifier.modify(currentGeometry, vertexCount - targetCount);
    lodGeometries.push(simplified);
    currentGeometry = simplified.clone();
  }
  
  return lodGeometries;
}

// 使用
const sphere = new THREE.SphereGeometry(1, 64, 64);
const lodGeometries = createLODGeometry(sphere, 3);

const lod = new THREE.LOD();
lod.addLevel(new THREE.Mesh(lodGeometries[0], material), 0);
lod.addLevel(new THREE.Mesh(lodGeometries[1], material), 10);
lod.addLevel(new THREE.Mesh(lodGeometries[2], material), 20);

几何体边缘提取

边缘几何体(EdgesGeometry)

提取几何体的边缘线:

javascript
const geometry = new THREE.BoxGeometry(1, 1, 1);

// 创建边缘几何体
// 第二个参数:阈值角度(度),相邻面夹角大于此值时显示边缘
const edges = new THREE.EdgeseGeometry(geometry, 15);

// 创建线条
const line = new THREE.LineSegments(
  edges,
  new THREE.LineBasicMaterial({ color: 0xffffff })
);

scene.add(line);

阈值角度说明

code
阈值角度 = 15°

面夹角 < 15°:不显示边缘(平滑表面)
面夹角 >= 15°:显示边缘(硬边)

例如球体:相邻面夹角很小,几乎不显示边缘
例如立方体:相邻面夹角 90°,显示所有边缘

线框几何体(WireframeGeometry)

显示所有三角形的边:

javascript
const geometry = new THREE.SphereGeometry(1, 16, 16);

// 创建线框几何体(显示所有三角形边)
const wireframe = new THREE.WireframeGeometry(geometry);

// 创建线条
const line = new THREE.LineSegments(
  wireframe,
  new THREE.LineBasicMaterial({ 
    color: 0x00ff00,
    linewidth: 1  // 注意:大多数平台不支持 linewidth > 1
  })
);

scene.add(line);

EdgesGeometry vs WireframeGeometry

特性EdgesGeometryWireframeGeometry
显示内容特征边缘所有三角形边
顶点数较少较多
适用场景技术图、低多边形风格调试、分析网格结构
性能较好一般

几何体属性访问与修改

访问顶点数据

javascript
const geometry = new THREE.BoxGeometry(1, 1, 1);

// 获取顶点位置属性
const positions = geometry.attributes.position;
const array = positions.array;

// 遍历所有顶点
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)})`);
}

// 修改顶点位置
positions.setXYZ(0, 2, 2, 2);
positions.needsUpdate = true;  // 必须标记更新

修改法线

javascript
const geometry = new THREE.SphereGeometry(1, 32, 32);

// 重新计算顶点法线(平滑)
geometry.computeVertexNormals();

// 手动翻转法线
const normals = geometry.attributes.normal;
for (let i = 0; i < normals.count; i++) {
  const nx = normals.getX(i);
  const ny = normals.getY(i);
  const nz = normals.getZ(i);
  
  normals.setXYZ(i, -nx, -ny, -nz);
}
normals.needsUpdate = true;

修改 UV 坐标

javascript
const geometry = new THREE.PlaneGeometry(2, 2);

// 获取 UV 属性
const uvs = geometry.attributes.uv;

// 修改 UV 坐标(实现纹理动画)
for (let i = 0; i < uvs.count; i++) {
  const u = uvs.getX(i);
  const v = uvs.getY(i);
  
  // 缩放 UV(纹理平铺)
  uvs.setXY(i, u * 2, v * 2);
}
uvs.needsUpdate = true;

实用工具函数

计算几何体表面积

javascript
function calculateSurfaceArea(geometry) {
  let area = 0;
  const positions = geometry.attributes.position;
  const indices = geometry.index ? geometry.index.array : null;
  
  const vA = new THREE.Vector3();
  const vB = new THREE.Vector3();
  const vC = new THREE.Vector3();
  const cb = new THREE.Vector3();
  const ab = new THREE.Vector3();
  
  const triangleCount = indices ? indices.length / 3 : positions.count / 3;
  
  for (let i = 0; i < triangleCount; i++) {
    const i3 = i * 3;
    
    if (indices) {
      vA.fromBufferAttribute(positions, indices[i3]);
      vB.fromBufferAttribute(positions, indices[i3 + 1]);
      vC.fromBufferAttribute(positions, indices[i3 + 2]);
    } else {
      vA.fromBufferAttribute(positions, i3);
      vB.fromBufferAttribute(positions, i3 + 1);
      vC.fromBufferAttribute(positions, i3 + 2);
    }
    
    // 三角形面积 = |AB × AC| / 2
    cb.subVectors(vC, vB);
    ab.subVectors(vA, vB);
    cb.cross(ab);
    
    area += cb.length() / 2;
  }
  
  return area;
}

// 使用
const sphere = new THREE.SphereGeometry(1, 32, 32);
console.log('表面积:', calculateSurfaceArea(sphere));
// 理论值: 4πr² ≈ 12.57

计算几何体体积

javascript
function calculateVolume(geometry) {
  let volume = 0;
  const positions = geometry.attributes.position;
  const indices = geometry.index ? geometry.index.array : null;
  
  const vA = new THREE.Vector3();
  const vB = new THREE.Vector3();
  const vC = new THREE.Vector3();
  
  const triangleCount = indices ? indices.length / 3 : positions.count / 3;
  
  for (let i = 0; i < triangleCount; i++) {
    const i3 = i * 3;
    
    if (indices) {
      vA.fromBufferAttribute(positions, indices[i3]);
      vB.fromBufferAttribute(positions, indices[i3 + 1]);
      vC.fromBufferAttribute(positions, indices[i3 + 2]);
    } else {
      vA.fromBufferAttribute(positions, i3);
      vB.fromBufferAttribute(positions, i3 + 1);
      vC.fromBufferAttribute(positions, i3 + 2);
    }
    
    // 使用有向体积公式
    // V = (vA · (vB × vC)) / 6
    volume += vA.x * (vB.y * vC.z - vC.y * vB.z);
    volume += vB.x * (vC.y * vA.z - vA.y * vC.z);
    volume += vC.x * (vA.y * vB.z - vB.y * vA.z);
  }
  
  return Math.abs(volume) / 6;
}

// 使用
const sphere = new THREE.SphereGeometry(1, 32, 32);
console.log('体积:', calculateVolume(sphere));
// 理论值: 4/3πr³ ≈ 4.19

翻转几何体法线

javascript
function flipNormals(geometry) {
  const normals = geometry.attributes.normal;
  
  if (!normals) {
    console.warn('几何体没有法线属性');
    return geometry;
  }
  
  for (let i = 0; i < normals.count; i++) {
    normals.setXYZ(
      i,
      -normals.getX(i),
      -normals.getY(i),
      -normals.getZ(i)
    );
  }
  
  normals.needsUpdate = true;
  return geometry;
}

性能优化

几何体合并的性能对比

javascript
// 性能测试
console.time('1000 个独立网格');
for (let i = 0; i < 1000; i++) {
  const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.set(Math.random() * 10, Math.random() * 10, Math.random() * 10);
  scene.add(mesh);
}
console.timeEnd('1000 个独立网格');

console.time('合并后的几何体');
const geometries = [];
for (let i = 0; i < 1000; i++) {
  const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
  geometry.translate(
    Math.random() * 10,
    Math.random() * 10,
    Math.random() * 10
  );
  geometries.push(geometry);
}
const merged = mergeBufferGeometries(geometries);
const mergedMesh = new THREE.Mesh(merged, material);
scene.add(mergedMesh);
console.timeEnd('合并后的几何体');

合并适用场景

场景是否适合合并
静态场景(建筑、地形)✅ 适合
大量相同形状的物体✅ 适合
需要单独操作的物体❌ 不适合
动态移动的物体❌ 不适合
不同材质的物体⚠️ 按材质分组后合并

常见问题解答

Q: 几何体变换后边界框没有更新?

A: 变换后需要重新计算边界:

javascript
geometry.translate(2, 0, 0);
geometry.computeBoundingBox();
geometry.computeBoundingSphere();

Q: 合并几何体后纹理映射错误?

A: 确保所有几何体都有 UV 属性,且属性结构相同:

javascript
// 检查几何体属性
const geo1 = new THREE.BoxGeometry(1, 1, 1);
const geo2 = new THREE.SphereGeometry(1, 32, 32);

console.log('Box UV:', geo1.attributes.uv);  // 存在
console.log('Sphere UV:', geo2.attributes.uv);  // 存在

// 可以合并
const merged = mergeBufferGeometries([geo1, geo2]);

Q: 布尔运算结果不正确?

A: 检查以下几点:

  1. 确保调用了 updateMatrix()
  2. 几何体顶点数不要过多
  3. 尝试增加几何体的分段数
javascript
const box = new THREE.BoxGeometry(2, 2, 2);
const sphere = new THREE.SphereGeometry(1.2, 32, 32);  // 足够的分段

const boxMesh = new THREE.Mesh(box);
const sphereMesh = new THREE.Mesh(sphere);

boxMesh.updateMatrix();  // 必须!
sphereMesh.updateMatrix();  // 必须!

const result = CSG.subtract(boxMesh, sphereMesh);

Q: 如何选择合并还是实例化?

A: 根据场景选择:

javascript
// 合并:适合静态、不需要单独操作的场景
// - 减少内存(共享几何体)
// - 减少 draw call
// - 无法单独操作

// 实例化(InstancedMesh):适合需要单独变换的场景
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, 1000);

// 可以单独设置每个实例的变换
const matrix = new THREE.Matrix4();
for (let i = 0; i < 1000; i++) {
  matrix.setPosition(Math.random() * 10, Math.random() * 10, Math.random() * 10);
  instancedMesh.setMatrixAt(i, matrix);
}

最佳实践

  1. 合理使用合并:静态场景合并几何体,动态场景使用 InstancedMesh 或保持独立
  2. 及时释放内存:不再使用的几何体调用 dispose()
  3. 避免过度操作:频繁的几何体操作影响性能
  4. 使用索引缓冲区:减少顶点数据量
  5. 预处理几何体:提前计算边界、法线等属性
  6. 选择合适的布尔运算库three-bvh-csgthree-csg-ts 性能更好
  7. 注意变换顺序:先缩放、后旋转、再平移

相关链接