模型优化
优化 3D 模型的加载和渲染性能,对于创建流畅的 Web 3D 应用至关重要。
概述
模型优化包括文件大小优化、渲染性能优化、内存管理等多个方面。合理的优化能显著提升用户体验,特别是在移动设备和低端硬件上。
优化策略概览
性能瓶颈分析
code
┌─────────────────────────────────────────────────────────────┐
│ 3D 应用性能瓶颈 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 加载阶段 │ → │ 渲染阶段 │ → │ 内存阶段 │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ • 文件大小 │ │ • Draw Calls│ │ • 几何体内存│ │
│ │ • 网络带宽 │ │ • 三角形数 │ │ • 纹理内存 │ │
│ │ • 解析速度 │ │ • 着色器复杂│ │ • 材质实例 │ │
│ │ │ │ • 状态切换 │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ DRACO 压缩 │ │ LOD 策略 │ │ 资源释放 │ │
│ │ 纹理压缩 │ │ 实例化 │ │ 缓存管理 │ │
│ │ 异步加载 │ │ 合并几何体 │ │ 按需加载 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘优化目标
| 指标 | 目标值 | 说明 |
|---|---|---|
| 文件大小 | < 5MB | 单个模型建议大小 |
| 加载时间 | < 3s | 初始可见时间 |
| 三角形数 | < 100K | 单个模型建议上限 |
| Draw Calls | < 100 | 每帧渲染调用数 |
| 纹理内存 | < 100MB | 总纹理显存占用 |
| FPS | > 30 | 最低可接受帧率 |
模型压缩
DRACO 压缩
DRACO 是 Google 开发的 3D 几何体压缩库,可显著减小模型文件大小。
javascript
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const loader = new GLTFLoader();
// 配置 DRACO 解码器
const dracoLoader = new DRACOLoader();
// 使用 CDN
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
// 或使用本地文件(从 three/examples/jsm/libs/draco/ 复制)
// dracoLoader.setDecoderPath('/draco/');
// 使用 WASM 版本(更快)
dracoLoader.setDecoderConfig({ type: 'wasm' });
loader.setDRACOLoader(dracoLoader);
// 加载压缩模型
loader.load('model-draco.glb', (gltf) => {
scene.add(gltf.scene);
});DRACO 压缩工具
bash
# 方式1:使用 gltf-pipeline
npm install -g gltf-pipeline
# 压缩 GLTF 模型
gltf-pipeline -i model.gltf -o model-draco.gltf -d
# 压缩 GLB 模型
gltf-pipeline -i model.glb -o model-draco.glb -d
# 方式2:使用 gltf-transform
npm install -g gltf-transform
# 压缩模型
gltf-transform draco model.glb model-draco.glb
# 方式3:使用 Blender
# 导出时勾选 "Draco compression" 选项压缩效果对比
| 模型类型 | 原始大小 | DRACO 压缩后 | 压缩率 |
|---|---|---|---|
| 简单模型 | 1MB | 200KB | 80% |
| 中等模型 | 5MB | 1MB | 80% |
| 复杂模型 | 20MB | 4MB | 80% |
| 骨骼动画 | 10MB | 3MB | 70% |
纹理压缩
javascript
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('/basis/');
ktx2Loader.detectSupport(renderer);
const loader = new GLTFLoader();
loader.setKTX2Loader(ktx2Loader);
loader.load('model-compressed.glb', (gltf) => {
scene.add(gltf.scene);
});几何体优化
减少顶点数量
javascript
import { SimplifyModifier } from 'three/addons/modifiers/SimplifyModifier.js';
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
model.traverse((child) => {
if (child.isMesh) {
const geometry = child.geometry;
console.log('原始顶点数:', geometry.attributes.position.count);
// 简化几何体(移除 50% 顶点)
const modifier = new SimplifyModifier();
const simplified = modifier.modify(
geometry,
Math.floor(geometry.attributes.position.count * 0.5)
);
console.log('简化后顶点数:', simplified.attributes.position.count);
child.geometry = simplified;
}
});
scene.add(model);
});合并几何体
减少 Draw Calls 的有效方法。
javascript
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
// 合并多个几何体
function mergeModelGeometries(model) {
const geometries = [];
const materials = [];
const matrix = new THREE.Matrix4();
model.traverse((child) => {
if (child.isMesh) {
// 保存世界变换
child.updateMatrixWorld();
matrix.copy(child.matrixWorld);
// 克隆几何体并应用变换
const geometry = child.geometry.clone();
geometry.applyMatrix4(matrix);
geometries.push(geometry);
materials.push(child.material);
}
});
// 合并几何体(相同材质时效果最好)
const mergedGeometry = mergeGeometries(geometries);
return new THREE.Mesh(mergedGeometry, materials[0]);
}
// 使用
loader.load('model.glb', (gltf) => {
const mergedMesh = mergeModelGeometries(gltf.scene);
scene.add(mergedMesh);
});批处理(Instancing)
对相同几何体使用实例化渲染。
javascript
import { InstancedMesh } from 'three';
// 创建实例化网格
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const count = 1000;
const instancedMesh = new InstancedMesh(geometry, material, count);
// 设置每个实例的变换
const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
const rotation = new THREE.Euler();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3(1, 1, 1);
for (let i = 0; i < count; i++) {
position.set(
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20
);
rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI
);
quaternion.setFromEuler(rotation);
matrix.compose(position, quaternion, scale);
instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
scene.add(instancedMesh);
// 只产生 1 个 Draw Call,而非 1000 个!索引优化
javascript
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
model.traverse((child) => {
if (child.isMesh) {
const geometry = child.geometry;
// 确保使用索引几何体
if (!geometry.index) {
// 将非索引转换为索引几何体
const indexed = geometry.toIndexedGeometry();
child.geometry = indexed;
}
console.log('顶点数:', geometry.attributes.position.count);
console.log('索引数:', geometry.index ? geometry.index.count : '无索引');
}
});
scene.add(model);
});LOD(细节层次)
LOD 根据观察距离自动切换不同精度的模型。
创建 LOD 对象
javascript
// 创建 LOD 对象
const lod = new THREE.LOD();
// 加载不同精度的模型
async function loadLODModels() {
const highDetail = await loadModel('high.glb');
const mediumDetail = await loadModel('medium.glb');
const lowDetail = await loadModel('low.glb');
// 添加不同细节级别
// 参数:模型对象, 切换距离
lod.addLevel(highDetail, 0); // 0-10 米使用高精度
lod.addLevel(mediumDetail, 10); // 10-30 米使用中精度
lod.addLevel(lowDetail, 30); // 30+ 米使用低精度
// 可选:最远距离不显示
// lod.addLevel(new THREE.Object3D(), 100);
scene.add(lod);
}
// 更新 LOD(在动画循环中)
function animate() {
requestAnimationFrame(animate);
// LOD 需要根据相机位置更新
lod.update(camera);
renderer.render(scene, camera);
}自动生成 LOD
javascript
import { SimplifyModifier } from 'three/addons/modifiers/SimplifyModifier.js';
function generateLODFromModel(geometry, material, levels) {
const lod = new THREE.LOD();
const modifier = new SimplifyModifier();
levels.forEach((level) => {
// 计算目标顶点数
const targetCount = Math.floor(
geometry.attributes.position.count * level.ratio
);
// 简化几何体
const simplified = modifier.modify(geometry, targetCount);
// 创建网格
const mesh = new THREE.Mesh(simplified, material.clone());
// 添加到 LOD
lod.addLevel(mesh, level.distance);
});
return lod;
}
// 使用示例
const levels = [
{ distance: 0, ratio: 1.0 }, // 原始
{ distance: 20, ratio: 0.5 }, // 50% 顶点
{ distance: 50, ratio: 0.25 }, // 25% 顶点
{ distance: 100, ratio: 0.1 } // 10% 顶点
];
const lod = generateLODFromModel(geometry, material, levels);
scene.add(lod);LOD 最佳实践
javascript
// 1. 距离阈值设置
const lodDistances = {
high: 0, // 高精度:近距离
medium: 20, // 中精度:中等距离
low: 50, // 低精度:远距离
culled: 100 // 超出后剔除
};
// 2. 渐进式加载
class ProgressiveLOD {
constructor() {
this.lods = new Map();
this.loadPriority = ['low', 'medium', 'high'];
}
async loadProgressive(basePath, id) {
const lod = new THREE.LOD();
// 先加载低精度
const lowModel = await this.loadModel(`${basePath}_low.glb`);
lod.addLevel(lowModel, lodDistances.medium);
// 添加占位
lod.addLevel(new THREE.Object3D(), lodDistances.culled);
this.lods.set(id, { lod, basePath, loadedLevels: ['low'] });
// 后台加载更高精度
this.loadHigherLevels(id);
return lod;
}
async loadHigherLevels(id) {
const data = this.lods.get(id);
if (!data) return;
// 加载中精度
const mediumModel = await this.loadModel(`${data.basePath}_medium.glb`);
data.lod.addLevel(mediumModel, lodDistances.low);
// 加载高精度
const highModel = await this.loadModel(`${data.basePath}_high.glb`);
data.lod.addLevel(highModel, lodDistances.high);
}
}纹理优化
纹理尺寸
javascript
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
model.traverse((child) => {
if (child.isMesh && child.material) {
const material = child.material;
// 检查所有贴图
const textureTypes = ['map', 'normalMap', 'roughnessMap',
'metalnessMap', 'aoMap', 'emissiveMap'];
textureTypes.forEach(type => {
if (material[type]) {
const texture = material[type];
const image = texture.image;
if (image) {
console.log(`${type}: ${image.width}x${image.height}`);
// 检查是否为 2 的幂次方
const isPowerOfTwo = (n) => (n & (n - 1)) === 0;
if (!isPowerOfTwo(image.width) || !isPowerOfTwo(image.height)) {
console.warn(`纹理 ${type} 尺寸不是 2 的幂次方`);
}
}
// 设置各向异性过滤
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
}
});
}
});
scene.add(model);
});纹理 LOD
根据距离动态切换纹理分辨率。
javascript
// 预加载不同分辨率的纹理
const textureLODs = {
high: textureLoader.load('texture_2048.jpg'),
medium: textureLoader.load('texture_1024.jpg'),
low: textureLoader.load('texture_512.jpg')
};
// 根据距离切换纹理
function updateTextureLOD(mesh, camera) {
const distance = camera.position.distanceTo(mesh.position);
let texture;
if (distance < 10) {
texture = textureLODs.high;
} else if (distance < 30) {
texture = textureLODs.medium;
} else {
texture = textureLODs.low;
}
if (mesh.material.map !== texture) {
mesh.material.map = texture;
mesh.material.needsUpdate = true;
}
}纹理压缩
javascript
// 使用 KTX2 压缩纹理
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('/basis/');
ktx2Loader.detectSupport(renderer);
// 加载压缩纹理
const compressedTexture = ktx2Loader.load('texture.ktx2');
compressedTexture.colorSpace = THREE.SRGBColorSpace;
// 应用到材质
material.map = compressedTexture;材质优化
简化材质
javascript
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
model.traverse((child) => {
if (child.isMesh) {
// 将复杂材质替换为简单材质
if (child.material.isMeshStandardMaterial) {
const oldMaterial = child.material;
child.material = new THREE.MeshLambertMaterial({
map: oldMaterial.map, // 保留基础贴图
color: oldMaterial.color, // 保留颜色
emissive: oldMaterial.emissive, // 保留自发光
emissiveMap: oldMaterial.emissiveMap
});
// 释放旧材质
oldMaterial.dispose();
}
}
});
scene.add(model);
});共享材质
javascript
// 创建共享材质实例
const sharedMaterials = {
metal: new THREE.MeshStandardMaterial({
color: 0x888888,
metalness: 1.0,
roughness: 0.3
}),
wood: new THREE.MeshStandardMaterial({
color: 0x8B4513,
metalness: 0.0,
roughness: 0.8
}),
plastic: new THREE.MeshStandardMaterial({
color: 0xff0000,
metalness: 0.0,
roughness: 0.5
})
};
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
model.traverse((child) => {
if (child.isMesh) {
// 根据名称或 userData 分配材质
const materialType = child.userData.materialType || 'metal';
child.material = sharedMaterials[materialType];
}
});
scene.add(model);
});加载优化
延迟加载
只加载可见区域的模型。
javascript
// 视锥体剔除加载
async function loadVisibleModels(models, camera) {
const frustum = new THREE.Frustum();
const projScreenMatrix = new THREE.Matrix4();
projScreenMatrix.multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
frustum.setFromProjectionMatrix(projScreenMatrix);
const loadPromises = [];
for (const modelData of models) {
// 检查是否在视锥内
const boundingSphere = new THREE.Sphere(
modelData.position,
modelData.radius
);
if (frustum.intersectsSphere(boundingSphere)) {
loadPromises.push(loadAndPlaceModel(modelData));
}
}
return Promise.all(loadPromises);
}
async function loadAndPlaceModel(modelData) {
const model = await loadModel(modelData.url);
model.position.copy(modelData.position);
scene.add(model);
return model;
}分块加载
对于大型场景,按区块加载和卸载。
javascript
class ChunkLoader {
constructor(chunkSize, renderDistance) {
this.chunkSize = chunkSize;
this.renderDistance = renderDistance;
this.loadedChunks = new Map();
this.loader = new GLTFLoader();
}
update(camera) {
// 计算相机所在的区块坐标
const chunkX = Math.floor(camera.position.x / this.chunkSize);
const chunkZ = Math.floor(camera.position.z / this.chunkSize);
// 加载周围区块
for (let x = chunkX - this.renderDistance; x <= chunkX + this.renderDistance; x++) {
for (let z = chunkZ - this.renderDistance; z <= chunkZ + this.renderDistance; z++) {
const key = `${x},${z}`;
if (!this.loadedChunks.has(key)) {
this.loadChunk(x, z);
}
}
}
// 卸载远距离区块
this.unloadDistantChunks(chunkX, chunkZ);
}
async loadChunk(x, z) {
const key = `${x},${z}`;
const url = `chunks/${x}_${z}.glb`;
try {
const gltf = await new Promise((resolve, reject) => {
this.loader.load(url, resolve, undefined, reject);
});
gltf.scene.position.set(
x * this.chunkSize,
0,
z * this.chunkSize
);
scene.add(gltf.scene);
this.loadedChunks.set(key, gltf.scene);
} catch (error) {
console.warn(`区块 ${key} 加载失败:`, error);
}
}
unloadDistantChunks(centerX, centerZ) {
const maxDistance = this.renderDistance + 1;
for (const [key, model] of this.loadedChunks) {
const [x, z] = key.split(',').map(Number);
const distance = Math.max(
Math.abs(x - centerX),
Math.abs(z - centerZ)
);
if (distance > maxDistance) {
scene.remove(model);
disposeModel(model);
this.loadedChunks.delete(key);
}
}
}
}
// 使用
const chunkLoader = new ChunkLoader(16, 2); // 16x16 区块,渲染 2 格范围
function animate() {
requestAnimationFrame(animate);
chunkLoader.update(camera);
renderer.render(scene, camera);
}内存管理
资源释放
正确释放模型资源防止内存泄漏。
javascript
// 完整的资源释放函数
function disposeModel(model) {
model.traverse((child) => {
// 释放几何体
if (child.geometry) {
child.geometry.dispose();
}
// 释放材质
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(material => disposeMaterial(material));
} else {
disposeMaterial(child.material);
}
}
// 释放骨架
if (child.skeleton) {
child.skeleton.dispose();
}
});
}
function disposeMaterial(material) {
// 释放所有贴图
const textureProperties = [
'map', 'normalMap', 'roughnessMap', 'metalnessMap',
'aoMap', 'emissiveMap', 'alphaMap', 'bumpMap',
'displacementMap', 'envMap', 'lightMap', 'specularMap'
];
textureProperties.forEach(prop => {
if (material[prop] && typeof material[prop].dispose === 'function') {
material[prop].dispose();
}
});
// 释放材质本身
material.dispose();
}
// 使用示例
const model = await loadModel('model.glb');
scene.add(model);
// 不再需要时
scene.remove(model);
disposeModel(model);资源监控
javascript
class ResourceMonitor {
constructor() {
this.resources = new Map();
}
track(id, resource) {
this.resources.set(id, {
resource,
timestamp: Date.now(),
type: this.getResourceType(resource)
});
}
untrack(id) {
this.resources.delete(id);
}
getResourceType(resource) {
if (resource.isMesh) return 'mesh';
if (resource.isBufferGeometry) return 'geometry';
if (resource.isMaterial) return 'material';
if (resource.isTexture) return 'texture';
return 'unknown';
}
estimateMemory(resource) {
let memory = 0;
if (resource.isBufferGeometry) {
const geo = resource;
for (const name in geo.attributes) {
memory += geo.attributes[name].array.byteLength;
}
if (geo.index) {
memory += geo.index.array.byteLength;
}
} else if (resource.isTexture) {
const img = resource.image;
if (img) {
memory += img.width * img.height * 4;
}
} else if (resource.isMesh) {
if (resource.geometry) {
memory += this.estimateMemory(resource.geometry);
}
if (resource.material) {
const materials = Array.isArray(resource.material)
? resource.material
: [resource.material];
materials.forEach(mat => {
['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'aoMap']
.forEach(prop => {
if (mat[prop]) {
memory += this.estimateMemory(mat[prop]);
}
});
});
}
}
return memory;
}
getStats() {
const stats = {
count: this.resources.size,
byType: {},
totalMemory: 0
};
this.resources.forEach(({ resource, type }) => {
stats.byType[type] = (stats.byType[type] || 0) + 1;
stats.totalMemory += this.estimateMemory(resource);
});
return stats;
}
printReport() {
const stats = this.getStats();
console.log('=== 资源使用报告 ===');
console.log(`资源总数: ${stats.count}`);
console.log(`估算内存: ${(stats.totalMemory / 1024 / 1024).toFixed(2)} MB`);
console.log('按类型:', stats.byType);
// 使用 renderer.info 获取实际渲染信息
console.log('渲染器信息:', {
geometries: renderer.info.memory.geometries,
textures: renderer.info.memory.textures,
programs: renderer.info.programs?.length
});
}
}
// 使用
const monitor = new ResourceMonitor();
loader.load('model.glb', (gltf) => {
scene.add(gltf.scene);
monitor.track('model1', gltf.scene);
});
// 定期检查
setInterval(() => monitor.printReport(), 5000);性能监控
使用 Stats.js
javascript
import Stats from 'three/addons/libs/stats.module.js';
const stats = new Stats();
stats.showPanel(0); // 0: fps, 1: ms, 2: mb
document.body.appendChild(stats.dom);
function animate() {
stats.begin();
// 渲染逻辑
renderer.render(scene, camera);
stats.end();
requestAnimationFrame(animate);
}自定义性能监控
javascript
class PerformanceMonitor {
constructor() {
this.fps = 0;
this.frameTime = 0;
this.drawCalls = 0;
this.triangles = 0;
this.frames = 0;
this.lastTime = performance.now();
}
update() {
this.frames++;
const currentTime = performance.now();
if (currentTime - this.lastTime >= 1000) {
this.fps = Math.round(this.frames * 1000 / (currentTime - this.lastTime));
this.frames = 0;
this.lastTime = currentTime;
// 从渲染器获取信息
this.drawCalls = renderer.info.render.calls;
this.triangles = renderer.info.render.triangles;
this.display();
}
}
display() {
console.log({
FPS: this.fps,
DrawCalls: this.drawCalls,
Triangles: this.triangles
});
}
}
const perfMonitor = new PerformanceMonitor();
function animate() {
requestAnimationFrame(animate);
// 渲染逻辑
renderer.render(scene, camera);
perfMonitor.update();
}常见问题
Q: 模型加载后很卡顿?
A: 检查以下几点:
javascript
// 1. 检查三角形数量
let triangles = 0;
model.traverse((child) => {
if (child.isMesh && child.geometry) {
const geo = child.geometry;
triangles += geo.index ? geo.index.count / 3 : geo.attributes.position.count / 3;
}
});
console.log('总三角形数:', triangles);
// 2. 检查 Draw Calls
console.log('Draw Calls:', renderer.info.render.calls);
// 3. 检查纹理内存
let textureCount = 0;
model.traverse((child) => {
if (child.isMesh && child.material) {
const mats = Array.isArray(child.material) ? child.material : [child.material];
mats.forEach(mat => {
['map', 'normalMap', 'roughnessMap'].forEach(prop => {
if (mat[prop]) textureCount++;
});
});
}
});
console.log('纹理数量:', textureCount);Q: 如何判断模型是否需要 LOD?
A: 根据使用场景判断:
javascript
function shouldUseLOD(model, camera, threshold = 50000) {
// 计算三角形数
let triangles = 0;
model.traverse((child) => {
if (child.isMesh && child.geometry) {
const geo = child.geometry;
triangles += geo.index ? geo.index.count / 3 : geo.attributes.position.count / 3;
}
});
// 如果超过阈值且场景中有多个此模型,建议使用 LOD
if (triangles > threshold) {
console.log(`模型三角形数 ${triangles} 超过阈值 ${threshold},建议使用 LOD`);
return true;
}
return false;
}Q: DRACO 解压很慢?
A: 优化方案:
javascript
// 1. 使用 WASM 版本(更快)
dracoLoader.setDecoderConfig({ type: 'wasm' });
// 2. 预加载解码器
dracoLoader.setDecoderPath('/draco/');
await dracoLoader.preload();
// 3. 减少压缩级别(导出时设置)
// gltf-pipeline -i model.glb -o output.glb -d --draco.compressionLevel=1Q: 内存占用持续增长?
A: 检查资源释放:
javascript
// 定期检查内存
setInterval(() => {
const info = renderer.info;
console.log('几何体:', info.memory.geometries);
console.log('纹理:', info.memory.textures);
}, 5000);
// 确保正确释放
function safeRemove(object) {
scene.remove(object);
disposeModel(object);
object = null;
}Q: 如何优化大型场景?
A: 综合使用多种策略:
javascript
// 1. 使用 LOD
const lod = new THREE.LOD();
lod.addLevel(highModel, 0);
lod.addLevel(mediumModel, 20);
lod.addLevel(lowModel, 50);
// 2. 使用实例化
const instancedMesh = new InstancedMesh(geometry, material, count);
// 3. 按需加载
chunkLoader.update(camera);
// 4. 合并静态几何体
const mergedGeometry = mergeGeometries(geometries);
// 5. 使用遮挡剔除
renderer.setOcclusionQueryEnabled(true);最佳实践
- 使用 GLTF + DRACO:最佳压缩和性能平衡
- 合理 LOD:根据距离调整细节级别
- 合并几何体:减少 Draw Calls
- 共享材质:减少材质实例
- 及时释放:避免内存泄漏
- 监控性能:定期检查渲染指标
- 渐进加载:先显示低精度,后台加载高精度
- 按需加载:只加载可见区域