跳到主要内容

性能优化

性能优化是 3D 应用开发中不可忽视的环节。良好的性能可以确保流畅的用户体验,特别是在移动设备或低端硬件上。本章将介绍 Three.js 中的各种性能优化技术和最佳实践。

性能分析

测量帧率

使用 stats.js 或手动计算来监控帧率:

// 使用 stats.js
import Stats from 'three/addons/libs/stats.module.js';

const stats = new Stats();
document.body.appendChild(stats.dom);

function animate() {
stats.begin();
// 渲染代码
stats.end();
requestAnimationFrame(animate);
}

// 手动计算帧率
let frameCount = 0;
let lastTime = performance.now();

function animate() {
frameCount++;
const now = performance.now();

if (now >= lastTime + 1000) {
const fps = Math.round((frameCount * 1000) / (now - lastTime));
console.log(`FPS: ${fps}`);
frameCount = 0;
lastTime = now;
}

requestAnimationFrame(animate);
}

渲染器信息

查看渲染器的统计信息:

console.log(renderer.info);
// {
// memory: { geometries: 1, textures: 2 },
// render: { calls: 3, triangles: 1000, points: 0, lines: 0 }
// }

renderer.info.render.calls 表示每帧的绘制调用次数,这是重要的性能指标。

几何体优化

减少顶点数量

顶点数量直接影响 GPU 的处理负担:

// 使用适当的分段数
const geometry = new THREE.SphereGeometry(1, 16, 16); // 而不是 64, 64

// 根据物体大小调整细节
function createSphere(radius) {
const segments = Math.max(8, Math.floor(radius * 8));
return new THREE.SphereGeometry(radius, segments, segments);
}

合并几何体

将多个静态物体合并为一个:

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

const geometries = [];
for (let i = 0; i < 100; i++) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
geometry.translate(
Math.random() * 100 - 50,
Math.random() * 100 - 50,
Math.random() * 100 - 50
);
geometries.push(geometry);
}

const mergedGeometry = mergeGeometries(geometries);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mesh);

合并后只需一次绘制调用,大大减少 CPU 与 GPU 的通信。

实例化渲染

对于大量相同几何体,使用实例化渲染:

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const count = 10000;

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() * 100 - 50,
Math.random() * 100 - 50
);
mesh.setMatrixAt(i, matrix);
}

mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);

实例化渲染比合并几何体更灵活,可以单独更新每个实例。

材质优化

选择合适的材质

材质复杂度影响渲染性能:

// 最快:无光照
const basicMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

// 较快:仅漫反射
const lambertMaterial = new THREE.MeshLambertMaterial({ color: 0x00ff00 });

// 中等:有高光
const phongMaterial = new THREE.MeshPhongMaterial({ color: 0x00ff00 });

// 较慢:PBR
const standardMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00 });

// 最慢:高级 PBR
const physicalMaterial = new THREE.MeshPhysicalMaterial({ color: 0x00ff00 });

根据场景需求选择最简单的材质。

共享材质

多个物体共享同一个材质实例:

// 好的做法:共享材质
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
for (let i = 0; i < 100; i++) {
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}

纹理优化

纹理尺寸

纹理尺寸应该是 2 的幂次方(如 512, 1024, 2048):

// 推荐的尺寸
const sizes = [64, 128, 256, 512, 1024, 2048, 4096];

// 避免非二次幂尺寸
// 虽然现代 GPU 支持,但性能会受影响

各向异性过滤

const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
texture.anisotropy = Math.min(4, maxAnisotropy); // 不要超过 4

纹理压缩

使用压缩纹理格式减少内存占用:

import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';

const ktx2Loader = new KTX2Loader(manager);
ktx2Loader.setTranscoderPath('jsm/libs/basis/');
ktx2Loader.detectSupport(renderer);

ktx2Loader.load('texture.ktx2', (texture) => {
material.map = texture;
});

光源和阴影优化

减少光源数量

// 少量高效的光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
scene.add(directionalLight);

点光源和聚光灯的开销比方向光大得多,应尽量少用。

阴影优化

const light = new THREE.DirectionalLight(0xffffff, 1);
light.castShadow = true;

// 限制阴影贴图尺寸
light.shadow.mapSize.width = 1024; // 而不是 4096
light.shadow.mapSize.height = 1024;

// 限制阴影范围
light.shadow.camera.near = 0.5;
light.shadow.camera.far = 50;
light.shadow.camera.left = -10;
light.shadow.camera.right = 10;
light.shadow.camera.top = 10;
light.shadow.camera.bottom = -10;

// 使用更简单的阴影类型
renderer.shadowMap.type = THREE.BasicShadowMap; // 或 PCFShadowMap

渲染优化

按需渲染

只在场景变化时渲染:

let needsRender = true;

controls.addEventListener('change', () => { needsRender = true; });
window.addEventListener('resize', () => { needsRender = true; });

function animate() {
requestAnimationFrame(animate);
controls.update();

if (needsRender) {
renderer.render(scene, camera);
needsRender = false;
}
}

降低分辨率

// 限制像素比
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

// 或降低渲染分辨率
const scale = 0.75;
renderer.setSize(
window.innerWidth * scale,
window.innerHeight * scale,
false
);
renderer.domElement.style.width = '100%';
renderer.domElement.style.height = '100%';

LOD(细节层次)

根据距离使用不同精度的模型:

const lod = new THREE.LOD();

lod.addLevel(highDetailMesh, 0); // 0-50 单位
lod.addLevel(mediumDetailMesh, 50); // 50-100 单位
lod.addLevel(lowDetailMesh, 100); // 100+ 单位

scene.add(lod);

function animate() {
lod.update(camera);
}

内存管理

释放资源

不再使用的资源需要手动释放:

// 释放几何体
geometry.dispose();

// 释放材质
material.dispose();

// 释放纹理
texture.dispose();

// 完整释放物体
function disposeObject(object) {
object.traverse((child) => {
if (child.geometry) child.geometry.dispose();
if (child.material) {
const materials = Array.isArray(child.material) ? child.material : [child.material];
materials.forEach((material) => {
Object.values(material).forEach((value) => {
if (value && typeof value.dispose === 'function') {
value.dispose();
}
});
material.dispose();
});
}
});
}

// 使用
scene.remove(mesh);
disposeObject(mesh);

避免内存泄漏

// 移除事件监听器
function onResize() {}
window.addEventListener('resize', onResize);
// 不再需要时
window.removeEventListener('resize', onResize);

// 清理渲染目标
renderTarget.dispose();

// 清理后处理
composer.dispose();

移动端优化

设备检测

const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

// 根据设备调整设置
const pixelRatio = isMobile ? 1 : Math.min(window.devicePixelRatio, 2);
const shadowMapSize = isMobile ? 512 : 2048;
const antialias = !isMobile;

renderer.setPixelRatio(pixelRatio);
renderer.shadowMap.enabled = !isMobile;

WebGL 扩展检测

const gl = renderer.getContext();
const hasInstancing = gl.getExtension('ANGLE_instanced_arrays');
const hasFloatTextures = gl.getExtension('OES_texture_float');

触摸事件优化

// 防止默认触摸行为
renderer.domElement.addEventListener('touchstart', (e) => {
e.preventDefault();
}, { passive: false });

// 节流触摸事件
let lastTouchTime = 0;
renderer.domElement.addEventListener('touchmove', (e) => {
const now = Date.now();
if (now - lastTouchTime < 16) return;
lastTouchTime = now;
// 处理触摸
});

性能检查清单

几何体

  • 顶点数量是否合理?
  • 是否可以合并静态几何体?
  • 是否可以使用实例化渲染?
  • 是否需要使用 LOD?

材质和纹理

  • 是否使用了最简单的材质?
  • 纹理尺寸是否合理?
  • 是否共享了材质和纹理?

光源和阴影

  • 光源数量是否过多?
  • 阴影贴图尺寸是否合适?
  • 是否限制了阴影范围?

渲染

  • 是否启用了按需渲染?
  • 分辨率是否合适?
  • 后期处理效果是否过多?

内存

  • 不再使用的资源是否释放?
  • 是否存在内存泄漏?

完整示例

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 性能配置
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const config = {
pixelRatio: isMobile ? 1 : Math.min(window.devicePixelRatio, 2),
shadowMapSize: isMobile ? 512 : 2048,
antialias: !isMobile
};

// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);

const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.set(10, 10, 10);

const renderer = new THREE.WebGLRenderer({ antialias: config.antialias });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(config.pixelRatio);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 使用实例化渲染
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material = new THREE.MeshLambertMaterial({ color: 0x00ff88 });
const count = 1000;

const mesh = new THREE.InstancedMesh(geometry, material, count);
mesh.castShadow = true;
mesh.receiveShadow = true;

const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
matrix.setPosition(
Math.random() * 20 - 10,
Math.random() * 20 - 10,
Math.random() * 20 - 10
);
mesh.setMatrixAt(i, matrix);
}

scene.add(mesh);

// 地面
const groundGeometry = new THREE.PlaneGeometry(50, 50);
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x333344 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);

// 光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(10, 20, 10);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = config.shadowMapSize;
directionalLight.shadow.mapSize.height = config.shadowMapSize;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -20;
directionalLight.shadow.camera.right = 20;
directionalLight.shadow.camera.top = 20;
directionalLight.shadow.camera.bottom = -20;
scene.add(directionalLight);

// 按需渲染
let needsRender = true;
controls.addEventListener('change', () => { needsRender = true; });

function animate() {
requestAnimationFrame(animate);
controls.update();

if (needsRender) {
renderer.render(scene, camera);
needsRender = false;
}
}

animate();

// 响应窗口变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
needsRender = true;
});

小结

性能优化是 3D 应用开发的关键环节:

  • 分析工具帮助定位性能瓶颈
  • 几何体优化包括减少顶点、合并、实例化
  • 材质优化选择合适的材质类型,共享材质实例
  • 渲染优化按需渲染、降低分辨率、使用 LOD
  • 内存管理及时释放不再使用的资源
  • 移动端优化根据设备能力调整渲染策略

优化是一个持续的过程,需要在视觉效果和性能之间找到平衡。