{T}

模型优化

优化 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 压缩后压缩率
简单模型1MB200KB80%
中等模型5MB1MB80%
复杂模型20MB4MB80%
骨骼动画10MB3MB70%

纹理压缩

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=1

Q: 内存占用持续增长?

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);

最佳实践

  1. 使用 GLTF + DRACO:最佳压缩和性能平衡
  2. 合理 LOD:根据距离调整细节级别
  3. 合并几何体:减少 Draw Calls
  4. 共享材质:减少材质实例
  5. 及时释放:避免内存泄漏
  6. 监控性能:定期检查渲染指标
  7. 渐进加载:先显示低精度,后台加载高精度
  8. 按需加载:只加载可见区域

相关链接