几何体操作
掌握几何体的各种操作方法,包括变换、合并、克隆、布尔运算等,可以更灵活地创建和管理 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 的 position、rotation、scale 属性。
旋转(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
| 特性 | EdgesGeometry | WireframeGeometry |
|---|---|---|
| 显示内容 | 特征边缘 | 所有三角形边 |
| 顶点数 | 较少 | 较多 |
| 适用场景 | 技术图、低多边形风格 | 调试、分析网格结构 |
| 性能 | 较好 | 一般 |
几何体属性访问与修改
访问顶点数据
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: 检查以下几点:
- 确保调用了
updateMatrix() - 几何体顶点数不要过多
- 尝试增加几何体的分段数
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);
}最佳实践
- 合理使用合并:静态场景合并几何体,动态场景使用 InstancedMesh 或保持独立
- 及时释放内存:不再使用的几何体调用
dispose() - 避免过度操作:频繁的几何体操作影响性能
- 使用索引缓冲区:减少顶点数据量
- 预处理几何体:提前计算边界、法线等属性
- 选择合适的布尔运算库:
three-bvh-csg比three-csg-ts性能更好 - 注意变换顺序:先缩放、后旋转、再平移