1. Three.js Geometry核心概念与进阶方向
Three.js中的Geometry是构建3D世界的基础元素,它定义了物体的形状和结构。与初学者常用的基础几何体(如BoxGeometry、SphereGeometry)不同,进阶几何操作需要深入理解顶点、面和UV映射等底层概念。
现代Three.js项目通常采用BufferGeometry而非传统的Geometry类,因为前者内存效率更高。BufferGeometry使用类型化数组(Typed Arrays)存储顶点数据,这对性能敏感的应用至关重要。例如,一个包含10,000个三角形的模型,在BufferGeometry中只需要一个Float32Array来存储所有顶点坐标,而传统Geometry会产生大量独立对象。
2. 自定义几何体创建实战
2.1 使用ShapeGeometry创建复杂2D形状
ShapeGeometry特别适合需要从2D路径生成3D几何体的场景。以下是一个创建齿轮形状的完整示例:
javascript复制const gearShape = new THREE.Shape();
const radius = 5;
const teethSize = 1.5;
const teethCount = 12;
gearShape.moveTo(radius + teethSize, 0);
for (let i = 0; i < teethCount * 2; i++) {
const angle = (i / teethCount) * Math.PI;
const isOuter = i % 2 === 0;
const currentRadius = isOuter ? radius + teethSize : radius - teethSize;
gearShape.lineTo(
Math.cos(angle) * currentRadius,
Math.sin(angle) * currentRadius
);
}
gearShape.lineTo(radius + teethSize, 0);
const geometry = new THREE.ShapeGeometry(gearShape, {
curveSegments: 32 // 更高的分段数使曲线更平滑
});
关键参数说明:
curveSegments:控制曲线近似精度,值越大曲线越平滑但顶点数越多extrudePath:可选的3D路径,用于将2D形状沿路径拉伸成3D物体
2.2 动态几何体修改技巧
运行时修改几何体需要特别注意性能优化。最佳实践是:
- 获取顶点数组的引用
- 批量修改顶点数据
- 标记需要更新的属性
javascript复制const positionAttribute = geometry.attributes.position;
const positions = positionAttribute.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 + Date.now() * 0.001) * 0.5;
}
positionAttribute.needsUpdate = true; // 必须标记更新
重要提示:频繁修改几何体会触发WebGL重新上传数据到GPU,对性能影响很大。对于动画场景,考虑使用顶点着色器在GPU端完成变形计算。
3. UV映射与纹理贴图高级应用
3.1 不规则几何体的UV展开
UV坐标决定了纹理如何映射到几何体表面。对于复杂形状,手动计算UV坐标是常见需求。以下是为自定义几何体添加UV坐标的典型流程:
javascript复制geometry.computeBoundingBox();
const max = geometry.boundingBox.max;
const min = geometry.boundingBox.min;
const offset = new THREE.Vector2(0 - min.x, 0 - min.y);
const range = new THREE.Vector2(max.x - min.x, max.y - min.y);
const uvAttribute = geometry.attributes.uv;
for (let i = 0; i < uvAttribute.count; i++) {
const u = (geometry.attributes.position.getX(i) + offset.x) / range.x;
const v = (geometry.attributes.position.getY(i) + offset.y) / range.y;
uvAttribute.setXY(i, u, v);
}
常见问题解决方案:
- 纹理拉伸:使用多个UV岛或更高分辨率的纹理
- 接缝问题:确保共享顶点在不同面上有相同的UV坐标
- 重复贴图:在材质设置
wrapS和wrapT为THREE.RepeatWrapping
3.2 高级贴图技术
结合法线贴图(Normal Map)和置换贴图(Displacement Map)可以显著提升几何体细节表现:
javascript复制const material = new THREE.MeshStandardMaterial({
map: textureLoader.load('diffuse.jpg'),
normalMap: textureLoader.load('normal.jpg'),
displacementMap: textureLoader.load('height.png'),
displacementScale: 0.1,
metalness: 0.5,
roughness: 0.7
});
参数优化建议:
displacementScale:根据场景单位调整,过大值会导致几何体撕裂normalScale:通常设置为new THREE.Vector2(1,1),可微调凹凸强度- 使用
aoMap时,需要第二组UV坐标(uv2)
4. 性能优化与内存管理
4.1 几何体合并技术
当场景包含大量相似几何体时,使用BufferGeometryUtils.mergeBufferGeometries可以大幅减少draw call:
javascript复制import { mergeBufferGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils';
const geometries = [];
for (let i = 0; i < 100; i++) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
geometry.translate(Math.random() * 100 - 50, 0, Math.random() * 100 - 50);
geometries.push(geometry);
}
const mergedGeometry = mergeBufferGeometries(geometries);
const mesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mesh);
注意事项:
- 合并后的几何体共享同一材质
- 无法单独控制合并后的子物体
- 适合静态场景,动态物体需要单独处理
4.2 几何体实例化
对于需要单独控制的重复几何体,使用实例化网格(InstancedMesh)是更好的选择:
javascript复制const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial();
const count = 1000;
const mesh = new THREE.InstancedMesh(geometry, material, count);
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
matrix.setPosition(
Math.random() * 100 - 50,
Math.random() * 10,
Math.random() * 100 - 50
);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh);
性能对比:
| 技术 | 内存占用 | Draw Call | CPU开销 | 适用场景 |
|---|---|---|---|---|
| 单独Mesh | 高 | 每个物体1次 | 高 | 少量独特物体 |
| 几何体合并 | 低 | 1次 | 低 | 大量静态相同物体 |
| 实例化 | 中 | 1次 | 中 | 大量相似但需独立控制的物体 |
5. 标签与标注系统实现
5.1 使用Sprite创建2D标签
Sprite非常适合创建始终面向相机的标签:
javascript复制const createLabel = (text, position) => {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 128;
const context = canvas.getContext('2d');
// 绘制背景
context.fillStyle = 'rgba(0,0,0,0.7)';
context.roundRect(10, 10, 236, 108, 10).fill();
// 绘制文字
context.font = 'Bold 48px Arial';
context.fillStyle = 'white';
context.textAlign = 'center';
context.fillText(text, 128, 74);
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(material);
sprite.scale.set(1, 0.5, 1);
sprite.position.copy(position);
return sprite;
};
const label = createLabel('重要点位', new THREE.Vector3(5, 2, 3));
scene.add(label);
5.2 3D空间中的HTML标签
对于复杂UI需求,可以使用CSS3DRenderer:
javascript复制import { CSS3DRenderer, CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer';
// 创建HTML标签
const labelDiv = document.createElement('div');
labelDiv.className = 'three-label';
labelDiv.textContent = '3D标签';
labelDiv.style.backgroundColor = 'rgba(0,150,255,0.7)';
const labelObject = new CSS3DObject(labelDiv);
labelObject.position.set(0, 3, 0);
scene.add(labelObject);
// 需要单独创建CSS3DRenderer并更新
const css3dRenderer = new CSS3DRenderer();
css3dRenderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(css3dRenderer.domElement);
// 在动画循环中
function animate() {
requestAnimationFrame(animate);
webGLRenderer.render(scene, camera);
css3dRenderer.render(scene, camera);
}
两种技术对比:
| 特性 | Sprite | CSS3D |
|---|---|---|
| 渲染方式 | WebGL | DOM |
| 性能 | 高 | 中(大量标签时性能下降) |
| 样式能力 | 有限(需Canvas绘制) | 完全CSS支持 |
| 交互能力 | 需射线检测 | 原生DOM事件 |
| 适用场景 | 简单标签、粒子效果 | 复杂UI、需要HTML交互 |
6. 模型加载与几何处理
6.1 加载外部3D模型
使用GLTFLoader加载模型时的几何处理技巧:
javascript复制import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
const loader = new GLTFLoader();
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
// 遍历模型中的所有几何体
model.traverse((node) => {
if (node.isMesh) {
// 优化几何体
node.geometry.computeVertexNormals();
// 添加自定义属性示例
node.geometry.setAttribute(
'color',
new THREE.BufferAttribute(
new Float32Array(node.geometry.attributes.position.count * 3).fill(1),
3
)
);
}
});
scene.add(model);
});
常见问题处理:
- 模型尺寸不对:加载后使用
model.scale.set(0.1, 0.1, 0.1)调整 - 材质丢失:检查纹理路径,或使用
textureLoader.setPath()设置基础路径 - 法线问题:调用
geometry.computeVertexNormals()重新计算
6.2 几何体后期处理
对加载的模型进行几何修改:
javascript复制// 创建渐变顶点颜色
const geometry = model.children[0].geometry;
const count = geometry.attributes.position.count;
const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const y = geometry.attributes.position.getY(i);
const normalizedY = (y + 1) / 2; // 假设y范围[-1,1]
colors[i * 3] = normalizedY; // R
colors[i * 3 + 1] = 1 - normalizedY; // G
colors[i * 3 + 2] = 0.5; // B
}
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
material.vertexColors = true;
高级技巧:
- 使用
geometry.merge()合并多个几何体 - 通过
geometry.toNonIndexed()转换为非索引几何体便于修改 - 使用
geometry.dispose()释放内存,特别是在动态加载/卸载场景时
