Skip to content

hexazen-cover

HexaZen - EP09:WebGPU 與效能優化

最後一章,來聊聊那些讓 HexaZen 跑得更順暢的技術細節。

從渲染引擎的選擇到鏡頭限制,這些看似微小的優化加在一起,就是「流暢」和「卡頓」的差別。੭ ˙ᗜ˙ )੭

WebGPU 優先,WebGL 降級

Babylon.js 支援 WebGPU,效能比 WebGL 好不少。我們優先使用 WebGPU,不支援的裝置自動降級。

content\aquarium\hexazen\composables\use-babylon-scene.ts

typescript
async createEngine({ canvas }) {
  const webGPUSupported = await WebGPUEngine.IsSupportedAsync
  if (webGPUSupported) {
    const engine = new WebGPUEngine(canvas, {
      antialias: true,
      stencil: true,
    })
    await engine.initAsync()
    return engine
  }

  return new Engine(canvas, true, {
    antialias: true,
    alpha: false,
    stencil: true,
  })
},

WebGPUEngine.IsSupportedAsync 是非同步檢測,因為瀏覽器需要時間確認 GPU 支援狀態。

場景初始化

useBabylonScene composable 封裝了完整的場景生命週期:

typescript
export function useBabylonScene(param?: UseBabylonSceneParam) {
  const canvasRef = ref<HTMLCanvasElement>()
  const engine = shallowRef<BabylonEngine>()
  const scene = shallowRef<Scene>()
  const camera = shallowRef<ArcRotateCamera>()

  onMounted(async () => {
    if (!canvasRef.value)
      return

    engine.value = await createEngine({
      canvas: canvasRef.value,
    })
    // 適配高 DPI 螢幕
    engine.value.setHardwareScalingLevel(1 / (window?.devicePixelRatio ?? 1))

    scene.value = createScene({
      canvas: canvasRef.value,
      engine: engine.value,
    })
    camera.value = createCamera({
      canvas: canvasRef.value,
      engine: engine.value,
      scene: scene.value,
    })

    window.addEventListener('resize', handleResize)

    engine.value.runRenderLoop(() => {
      scene.value?.render()
    })

    await init({ canvas, engine, scene, camera })
    setupSmartCameraLimits(engine, camera, scene)
  })

  onBeforeUnmount(() => {
    engine.value?.dispose()
    scene.value?.dispose()
    window.removeEventListener('resize', handleResize)
  })

  return { canvasRef, engine, scene, camera }
}

setHardwareScalingLevel 根據 devicePixelRatio 調整渲染解析度,Retina 螢幕上會更清晰。

預設光照與霧氣

typescript
createScene({ engine }) {
  const scene = new Scene(engine)
  scene.createDefaultLight()

  const defaultLight = scene.lights.at(-1)
  if (defaultLight instanceof HemisphericLight) {
    defaultLight.diffuse = new Color3(1.0, 0.98, 0.95)
    defaultLight.direction = new Vector3(0.5, 1, 0)
    defaultLight.groundColor = new Color3(0.64, 0.56, 0.78)
  }

  scene.clearColor = new Color4(0.97, 0.97, 0.96, 1)

  scene.fogMode = Scene.FOGMODE_LINEAR
  scene.fogColor = new Color3(0.55, 0.6, 0.65)
  scene.fogStart = 10
  scene.fogEnd = 100

  return scene
},

暖色光源搭配偏紫的地面反射光,讓積木看起來有柔和的手繪感。

智慧鏡頭限制

鏡頭需要根據裝置螢幕比例自動調整:

typescript
function setupSmartCameraLimits(
  engine: BabylonEngine,
  camera: ArcRotateCamera,
  scene: Scene,
) {
  const width = engine.getRenderWidth()
  const height = engine.getRenderHeight()

  // 直式螢幕使用垂直 FOV,橫式使用水平 FOV
  if (width < height) {
    camera.fovMode = Camera.FOVMODE_VERTICAL_FIXED
  }
  else {
    camera.fovMode = Camera.FOVMODE_HORIZONTAL_FIXED
  }

  const finalRadius = 4
  const safeDistance = finalRadius / Math.sin(camera.fov / 2)

  camera.lowerRadiusLimit = safeDistance * 0.2
  camera.upperRadiusLimit = safeDistance * 1
  camera.minZ = finalRadius * 0.1
}

手機直式螢幕和電腦橫式螢幕的 FOV 模式不同,這樣積木在任何裝置上都能完整顯示在畫面中。

鏡頭約束

typescript
createCamera({ scene, canvas }) {
  const camera = new ArcRotateCamera(
    'camera', 0, Math.PI / 3 * 2, 5,
    new Vector3(0, 0, 0), scene,
  )
  camera.attachControl(canvas, true)

  // 禁止平移,只能旋轉和縮放
  camera.panningSensibility = 0
  camera.wheelDeltaPercentage = 0.01

  // 限制仰角,永遠從上方俯瞰
  camera.lowerBetaLimit = Math.PI / 3
  camera.upperBetaLimit = Math.PI / 3

  return camera
},

禁止平移(panningSensibility = 0)和固定仰角(beta 鎖定在 60°),讓使用者只能水平旋轉和縮放,保持最佳觀賞角度。

模型材質優化

載入 GLB 模型時,強制使用三線性過濾避免紋理閃爍:

typescript
model.meshes.forEach((mesh) => {
  if (mesh.material instanceof PBRMaterial) {
    mesh.material.metallic = 0
    mesh.material.roughness = 0.4

    const texture = mesh.material.albedoTexture
    if (texture instanceof Texture) {
      texture.updateSamplingMode(Texture.TRILINEAR_SAMPLINGMODE)
    }
  }
})

TRILINEAR_SAMPLINGMODE 讓紋理邊緣被柔化,旋轉時不會有像素閃爍。

一開始看到的時候真的是快被閃瞎眼。(◞‸◟ )

陰影生成

typescript
function createShadowGenerator(scene: Scene) {
  const light = new DirectionalLight('dir01', new Vector3(-3, -5, -2), scene)
  light.intensity = 0.8

  const shadowGenerator = new ShadowGenerator(2048, light)
  shadowGenerator.bias = 0.000001
  shadowGenerator.normalBias = 0.0001
  shadowGenerator.forceBackFacesOnly = true
  return shadowGenerator
}

2048×2048 的陰影貼圖確保陰影邊緣銳利。forceBackFacesOnly = true 防止自身陰影產生的瑕疵(shadow acne)。

整體架構回顧

經過 9 篇文章,HexaZen 的架構如下:

hexazen/
├── hexazen.vue              # 應用根元件(狀態管理、UI 佈局)
├── main-scene.vue           # 3D 場景(互動、粒子、後製)
├── composables/
│   ├── use-babylon-scene    # 引擎初始化與生命週期
│   ├── use-thumbnail-gen    # 多執行緒縮圖生成
│   ├── use-simple-i18n      # 輕量國際化
│   └── use-font-loader      # 字體動態載入
└── domains/
    ├── block/               # 積木定義、建立、縮圖
    ├── hex-grid/            # 六角格數學
    ├── soundscape/          # 聲景規則、播放引擎、混音器
    └── share/codec/         # URL 編碼/解碼

總結 🐟

以上程式碼可以在此取得

  • WebGPU 優先、WebGL 自動降級
  • 高 DPI 螢幕適配與智慧鏡頭限制
  • 三線性過濾消除紋理閃爍
  • 2048×2048 高品質陰影

感謝看到這裡的你!希望這系列文章能給你一些靈感。ヾ(◍'౪`◍)ノ゙

如果你也想體驗看看,歡迎到 HexaZen 玩玩看,打造屬於你自己的音景吧!

TIP

因為篇幅問題,某些部分簡單帶過。

若想看到更詳細的解釋,請不吝留言或寫信給我喔!(*´∀`)~♥