1. 遮挡剔除与视锥体剔除

在 Three.js 中,遮挡剔除(Occlusion Culling) 和 视锥体剔除(Frustum Culling) 是两种重要的性能优化技术,用于减少渲染的物体数量,提升渲染效率。

1.1. 视锥体剔除(Frustum Culling)

1.1.1. 基本概念

视锥体剔除自动移除相机视野之外的物体。Three.js 默认启用此功能。

javascript

// 检查物体是否在视锥体内
const frustum = new THREE.Frustum()
const cameraViewProjectionMatrix = new THREE.Matrix4()
cameraViewProjectionMatrix.multiplyMatrices(
  camera.projectionMatrix,
  camera.matrixWorldInverse
)
frustum.setFromProjectionMatrix(cameraViewProjectionMatrix)

const mesh = new THREE.Mesh(geometry, material)
const inFrustum = frustum.intersectsObject(mesh)

1.1.2. 控制视锥体剔除

javascript

// 禁用单个物体的视锥体剔除
mesh.frustumCulled = false

// 扩展视锥体检测边界(防止物体在边界闪烁)
mesh.geometry.computeBoundingSphere()
mesh.geometry.boundingSphere.radius *= 1.5 // 扩大边界球

1.1.3. 自定义视锥体检测

javascript

class CustomFrustumCulling {
  constructor(camera) {
    this.frustum = new THREE.Frustum()
    this.camera = camera
  }

  update() {
    this.frustum.setFromProjectionMatrix(
      new THREE.Matrix4().multiplyMatrices(
        this.camera.projectionMatrix,
        this.camera.matrixWorldInverse
      )
    )
  }

  isVisible(object) {
    const sphere = object.geometry.boundingSphere
    if (!sphere) return true

    const worldSphere = sphere.clone()
    worldSphere.applyMatrix4(object.matrixWorld)
    return this.frustum.intersectsSphere(worldSphere)
  }
}

1.2. 遮挡剔除(Occlusion Culling)

Three.js 本身不提供完整的遮挡剔除系统,但可以通过以下方式实现:

1.2.1. GPU 遮挡查询(实验性)

javascript

import { WEBGL } from 'three/examples/jsm/WebGL.js'

if (WEBGL.getWebGL2Extensions().occlusion_query) {
  // 使用 occlusion query
  const occlusionQuery = renderer.getContext().createQuery()
}

1.2.2. 基于深度的简化遮挡

javascript

class SimpleOcclusionCulling {
  constructor(camera, scene) {
    this.camera = camera
    this.scene = scene
    this.depthMaterial = new THREE.MeshDepthMaterial()
    this.renderTarget = new THREE.WebGLRenderTarget(512, 512)
  }

  checkOcclusion(objects) {
    // 渲染深度图
    const originalMaterial = objects.map(obj => obj.material)
    objects.forEach(obj => obj.material = this.depthMaterial)

    renderer.setRenderTarget(this.renderTarget)
    renderer.render(this.scene, this.camera)
    renderer.setRenderTarget(null)

    // 恢复材质
    objects.forEach((obj, i) => obj.material = originalMaterial[i])

    // 这里可以读取深度缓冲区进行遮挡判断
  }
}

1.2.3. 使用 BVH 进行 CPU 遮挡检测

javascript

import { Octree } from 'three/examples/jsm/math/Octree.js'

class BVHOcclusionCuller {
  constructor() {
    this.octree = new Octree()
    this.visibleObjects = new Set()
  }

  buildOctree(objects) {
    objects.forEach(obj => {
      if (obj.geometry) {
        this.octree.add(obj)
      }
    })
  }

  update(camera) {
    this.visibleObjects.clear()

    // 从相机位置发射射线检测可见性
    const raycaster = new THREE.Raycaster()
    const cameraPos = camera.position

    // 采样多个方向
    for (let i = 0; i < 100; i++) {
      const direction = new THREE.Vector3(
        Math.random() * 2 - 1,
        Math.random() * 2 - 1,
        Math.random() * 2 - 1
      ).normalize()

      raycaster.set(cameraPos, direction)
      const intersects = this.octree.raycast(raycaster)

      if (intersects.length > 0) {
        this.visibleObjects.add(intersects[0].object)
      }
    }
  }
}

1.3. 优化技巧与最佳实践

1.3.1. LOD(细节层次)

javascript

const lod = new THREE.LOD()

// 添加不同细节级别的模型
lod.addLevel(highDetailMesh, 0)
lod.addLevel(mediumDetailMesh, 50)
lod.addLevel(lowDetailMesh, 100)

scene.add(lod)

1.3.2. 按需渲染

javascript

let needsRender = true

function checkVisibility() {
  needsRender = false

  scene.children.forEach(object => {
    if (object.visible && camera.frustum.intersectsObject(object)) {
      needsRender = true
    }
  })
}

function animate() {
  checkVisibility()

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

  requestAnimationFrame(animate)
}

1.3.3. 空间分区

javascript

class SpatialGrid {
  constructor(cellSize) {
    this.cellSize = cellSize
    this.grid = new Map()
  }

  addObject(object) {
    const key = this.getGridKey(object.position)
    if (!this.grid.has(key)) {
      this.grid.set(key, [])
    }
    this.grid.get(key).push(object)
  }

  getVisibleObjects(cameraFrustum) {
    const visible = []

    this.grid.forEach((objects, key) => {
      // 检查网格单元是否在视锥体内
      if (this.isCellVisible(key, cameraFrustum)) {
        objects.forEach(obj => {
          if (cameraFrustum.intersectsObject(obj)) {
            visible.push(obj)
          }
        })
      }
    })

    return visible
  }
}

1.3.4. 实例化渲染优化

javascript

// 对大量相同物体使用 InstancedMesh
const instanceCount = 1000
const instancedMesh = new THREE.InstancedMesh(geometry, material, instanceCount)

// 设置每个实例的位置
const matrix = new THREE.Matrix4()
for (let i = 0; i < instanceCount; i++) {
  matrix.setPosition(
    Math.random() * 100 - 50,
    Math.random() * 100 - 50,
    Math.random() * 100 - 50
  )
  instancedMesh.setMatrixAt(i, matrix)
}

1.4. 调试与可视化

javascript

// 显示视锥体
const frustumHelper = new THREE.CameraHelper(camera)
scene.add(frustumHelper)

// 显示边界框
const bboxHelper = new THREE.BoxHelper(mesh, 0xffff00)
scene.add(bboxHelper)

// 显示边界球
const sphereHelper = new THREE.SphereHelper(mesh, 0x00ffff)
scene.add(sphereHelper)

1.5. 注意事项

  1. 权衡开销:复杂的剔除算法可能有 CPU 开销

  2. 动态物体:移动物体需要每帧更新剔除状态

  3. 透明度处理:透明物体需要特殊处理(按深度排序)

  4. WebGL 限制:某些高级剔除技术需要 WebGL 2.0

1.6. 推荐的工作流程

  1. 首先使用内置视锥体剔除

  2. 对静态场景使用烘焙遮挡信息

  3. 对大量物体使用空间分区

  4. 根据距离使用 LOD

  5. 考虑使用 InstancedMesh 减少绘制调用

这些技术可以显著提升 Three.js 应用的性能,特别是在处理复杂场景时。根据具体应用场景选择合适的优化策略。