mcpField
T
threejs-graphics-optimizer

threejs-graphics-optimizer

github.com/ovachiever開発56124 回インストール48 週間前に更新
リポジトリ

threejs-graphics-optimizer について

THREE.js のグラフィックス開発者向けに、描画呼び出し、テクスチャ、レンダーループ、メモリ管理の最適化手法を示すスキルです。モバイル対応と性能計測も扱います。

主な機能

  1. 01描画呼び出しの削減とジオメトリ統合
  2. 02LOD と視錐台カリングの活用
  3. 03テクスチャとマテリアルの最適化
  4. 04モバイル向け描画設定
  5. 05リソース管理とメモリリーク防止
  6. 06FPS と GPU メモリの計測

活用シーン

  1. 01反復オブジェクトが多いシーンの最適化
  2. 02モバイル端末での描画負荷軽減
  3. 03フレームレート低下や GPU ボトルネックの調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/ovachiever/droid-tings --skill threejs-graphics-optimizer

インストール後、Claude Code で/threejs-graphics-optimizerを入力して読み込まれたか確認します。

詳細

THREE.js グラフィックス最適化

バージョン:1.0
対象:THREE.js とグラフィックスアプリケーションのパフォーマンス最適化
目的:モバイルを含むすべてのデバイスで、滑らかな 60fps のグラフィックス体験を実現する


哲学:パフォーマンス優先のグラフィックス

16ms の予算

目標:60 FPS = 1 フレームあたり 16.67ms

フレーム予算の内訳:

  • JavaScript のロジック:約 5~8ms
  • レンダリング(GPU):約 8~10ms
  • ブラウザーのオーバーヘッド:約 2ms

16ms を超えると:フレームがドロップし、カクつきが発生します。

モバイルとデスクトップの現実

デスクトップ:高性能な GPU、大容量の VRAM、高いピクセル比
モバイル:制約のある GPU、限られた VRAM、バッテリーへの配慮、サーマルスロットリング

設計思想:モバイル向けに最適化し、デスクトップ向けにはスケールアップします(逆にしないでください)。


パート 1:最適化の基本原則

1. ドローコールを最小限に抑える

問題:各オブジェクトが 1 回のドローコールになります。オブジェクトが 1000 個なら、コールも 1000 回となり、処理が遅くなります。

解決策:ジオメトリの結合

// ❌ Bad: 100 draw calls for 100 cubes
for (let i = 0; i < 100; i++) {
  const geometry = new THREE.BoxGeometry(1, 1, 1)
  const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
  const cube = new THREE.Mesh(geometry, material)
  cube.position.set(i * 2, 0, 0)
  scene.add(cube)
}

// ✅ Good: 1 draw call via InstancedMesh
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const instancedMesh = new THREE.InstancedMesh(geometry, material, 100)

for (let i = 0; i < 100; i++) {
  const matrix = new THREE.Matrix4()
  matrix.setPosition(i * 2, 0, 0)
  instancedMesh.setMatrixAt(i, matrix)
}

instancedMesh.instanceMatrix.needsUpdate = true
scene.add(instancedMesh)

使用する場面:

  • 類似したオブジェクトが多数ある場合(パーティクル、木、敵)
  • 配置が静的または準静的な場合
  • マテリアルやジオメトリを共有している場合
2. 詳細度(LOD)

オブジェクトが遠くにある場合は、より単純なジオメトリを描画します。

const lod = new THREE.LOD()

// High detail (near camera)
const highDetailGeo = new THREE.IcosahedronGeometry(1, 3) // Many faces
const highDetailMesh = new THREE.Mesh(
  highDetailGeo,
  new THREE.MeshStandardMaterial({ color: 0x00d9ff })
)
lod.addLevel(highDetailMesh, 0) // Distance 0-10

// Medium detail
const medDetailGeo = new THREE.IcosahedronGeometry(1, 1)
const medDetailMesh = new THREE.Mesh(
  medDetailGeo,
  new THREE.MeshBasicMaterial({ color: 0x00d9ff })
)
lod.addLevel(medDetailMesh, 10) // Distance 10-50

// Low detail (far from camera)
const lowDetailGeo = new THREE.IcosahedronGeometry(1, 0)
const lowDetailMesh = new THREE.Mesh(
  lowDetailGeo,
  new THREE.MeshBasicMaterial({ color: 0x00d9ff })
)
lod.addLevel(lowDetailMesh, 50) // Distance 50+

scene.add(lod)

// Update LOD in render loop
function animate() {
  lod.update(camera)
  renderer.render(scene, camera)
}
3. 視錐台カリング(自動)

THREE.js は、カメラの視野外にあるオブジェクトを自動的にスキップします。次の方法で支援できます。

// ❌ Bad: Unnecessarily large bounding volumes
mesh.geometry.computeBoundingSphere()
mesh.geometry.boundingSphere.radius = 1000 // Too large!

// ✅ Good: Accurate bounding volumes
mesh.geometry.computeBoundingSphere() // Uses actual geometry size
mesh.geometry.computeBoundingBox()
4. テクスチャの最適化

テクスチャサイズは重要です:

  • 4K テクスチャ(4096x4096):VRAM 64MB(非圧縮)
  • 2K テクスチャ(2048x2048):VRAM 16MB
  • 1K テクスチャ(1024x1024):VRAM 4MB

ルール:

  • 見た目を損なわない範囲で、最小サイズのテクスチャを使用する
  • 2 の累乗の寸法を使用する(512、1024、2048)
  • テクスチャを圧縮する(basis/KTX2 形式を使用)
const textureLoader = new THREE.TextureLoader()

// ❌ Bad: Loading 4K texture for small object
const texture = textureLoader.load('texture-4k.jpg')

// ✅ Good: Appropriate size for use case
const texture = textureLoader.load('texture-1k.jpg')

// ✅ Better: Set appropriate filtering
texture.minFilter = THREE.LinearFilter // No mipmaps (saves VRAM)
texture.anisotropy = renderer.capabilities.getMaxAnisotropy()

// ✅ Best: Dispose when done
function cleanup() {
  texture.dispose()
}

パート 2:モバイル固有の最適化

モバイルの検出と適応
/**
 * Detect mobile device.
 * @returns {boolean}
 */
export function isMobile() {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile/i.test(navigator.userAgent)
    || window.innerWidth < 768
}

/**
 * Get optimal pixel ratio for device.
 * @returns {number}
 */
export function getOptimalPixelRatio() {
  const mobile = isMobile()
  const deviceRatio = window.devicePixelRatio
  
  // Cap pixel ratio on mobile to save performance
  return mobile 
    ? Math.min(deviceRatio, 1.5)  // Max 1.5x on mobile
    : Math.min(deviceRatio, 2)    // Max 2x on desktop
}

// Apply to renderer
renderer.setPixelRatio(getOptimalPixelRatio())
モバイル向けパフォーマンス設定
/**
 * Configure renderer for mobile performance.
 */
function setupMobileOptimizations(renderer, scene, camera) {
  const mobile = isMobile()
  
  if (mobile) {
    // Disable expensive features
    renderer.shadowMap.enabled = false
    renderer.antialias = false
    
    // Lower pixel ratio
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5))
    
    // Simpler tone mapping
    renderer.toneMapping = THREE.NoToneMapping
    
    // Remove fog (expensive pixel shader)
    scene.fog = null
    
    // Reduce lights (expensive)
    // Keep only 1-2 lights max on mobile
    
    console.log('[Mobile] Performance optimizations applied')
  } else {
    // Desktop: enable high-quality features
    renderer.shadowMap.enabled = true
    renderer.shadowMap.type = THREE.PCFSoftShadowMap
    renderer.antialias = true
    renderer.toneMapping = THREE.ACESFilmicToneMapping
    
    console.log('[Desktop] High-quality features enabled')
  }
}
フォールバックパターン
/**
 * Create geometry with fallback for low-end devices.
 */
export function createOptimizedGeometry(options = {}) {
  const { size = 1, mobile = false } = options
  
  if (mobile) {
    // Simple geometry for mobile
    return new THREE.SphereGeometry(size, 8, 8) // Low poly
  } else {
    // Detailed geometry for desktop
    return new THREE.IcosahedronGeometry(size, 2) // High poly
  }
}

// Usage
const mobile = isMobile()
const geometry = createOptimizedGeometry({ size: 1, mobile })
const material = new THREE.MeshBasicMaterial({ color: 0x00d9ff })
const mesh = new THREE.Mesh(geometry, material)

パート 3:レンダーループの最適化

効率的なアニメーションループ
class SceneManager {
  constructor() {
    this.clock = new THREE.Clock()
    this.animationId = null
    this.lastFrameTime = 0
    this.fps = 60
    this.frameInterval = 1000 / this.fps
  }
  
  /**
   * Main render loop with delta time.
   */
  animate() {
    this.animationId = requestAnimationFrame(() => this.animate())
    
    const now = performance.now()
    const delta = now - this.lastFrameTime
    
    // Throttle to target FPS if needed
    if (delta < this.frameInterval) return
    
    this.lastFrameTime = now - (delta % this.frameInterval)
    
    // Update logic with delta
    const deltaSeconds = this.clock.getDelta()
    this.update(deltaSeconds)
    
    // Render
    this.renderer.render(this.scene, this.camera)
  }
  
  /**
   * Update scene objects.
   * @param {number} delta - Time since last frame (seconds)
   */
  update(delta) {
    // Update animations, physics, etc.
    this.animatedObjects.forEach(obj => {
      if (obj.update) obj.update(delta)
    })
  }
  
  /**
   * Cleanup and stop animation.
   */
  dispose() {
    if (this.animationId) {
      cancelAnimationFrame(this.animationId)
    }
  }
}
条件付きレンダリング
/**
 * Only render when something changed (for static scenes).
 */
class ConditionalRenderer {
  constructor(renderer, scene, camera) {
    this.renderer = renderer
    this.scene = scene
    this.camera = camera
    this.needsRender = true
  }
  
  /**
   * Mark scene as needing re-render.
   */
  invalidate() {
    this.needsRender = true
  }
  
  /**
   * Render only if needed.
   */
  render() {
    if (this.needsRender) {
      this.renderer.render(this.scene, this.camera)
      this.needsRender = false
    }
  }
  
  /**
   * Use with controls.
   */
  connectControls(controls) {
    controls.addEventListener('change', () => this.invalidate())
  }
}

// Usage
const conditionalRenderer = new ConditionalRenderer(renderer, scene, camera)
conditionalRenderer.connectControls(controls)

function animate() {
  requestAnimationFrame(animate)
  controls.update()
  conditionalRenderer.render() // Only renders if camera moved
}

パート 4:メモリ管理

破棄パターン
/**
 * Properly dispose THREE.js resources.
 */
export function disposeObject(object) {
  if (!object) return
  
  // Traverse and dispose children
  object.traverse((child) => {
    // Dispose geometry
    if (child.geometry) {
      child.geometry.dispose()
    }
    
    // Dispose materials
    if (child.material) {
      if (Array.isArray(child.material)) {
        child.material.forEach(material => disposeMaterial(material))
      } else {
        disposeMaterial(child.material)
      }
    }
    
    // Dispose textures
    if (child.texture) {
      child.texture.dispose()
    }
  })
  
  // Remove from parent
  if (object.parent) {
    object.parent.remove(object)
  }
}

/**
 * Dispose material and its textures.
 */
function disposeMaterial(material) {
  material.dispose()
  
  // Dispose textures
  Object.keys(material).forEach(key => {
    const value = material[key]
    if (value && typeof value === 'object' && 'minFilter' in value) {
      value.dispose() // It's a texture
    }
  })
}
メモリリークの防止
class SafeSceneManager {
  constructor() {
    this.scene = new THREE.Scene()
    this.renderer = new THREE.WebGLRenderer()
    this.objects = new Set()
  }
  
  /**
   * Add object and track it.
   */
  add(object) {
    this.scene.add(object)
    this.objects.add(object)
  }
  
  /**
   * Remove and dispose object.
   */
  remove(object) {
    this.scene.remove(object)
    this.objects.delete(object)
    disposeObject(object)
  }
  
  /**
   * Cleanup all resources.
   */
  dispose() {
    // Dispose all tracked objects
    this.objects.forEach(obj => disposeObject(obj))
    this.objects.clear()
    
    // Dispose renderer
    this.renderer.dispose()
    
    // Clear scene
    this.scene.clear()
  }
}

パート 5:マテリアルの最適化

マテリアルの共有
// ❌ Bad: New material for each object
for (let i = 0; i < 100; i++) {
  const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
  const mesh = new THREE.Mesh(geometry, material)
  scene.add(mesh)
}

// ✅ Good: Share single material
const sharedMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 })

for (let i = 0; i < 100; i++) {
  const mesh = new THREE.Mesh(geometry, sharedMaterial)
  scene.add(mesh)
}
より軽量なマテリアルの種類

パフォーマンスの順(速いものから遅いもの):

  1. MeshBasicMaterial - ライティングなし、フラットシェーディング
  2. MeshLambertMaterial - シンプルな拡散光ライティング
  3. MeshPhongMaterial - 鏡面ハイライト
  4. MeshStandardMaterial - PBR(高負荷)
  5. MeshPhysicalMaterial - 高度な PBR(非常に高負荷)
// Mobile: Use cheaper materials
const material = isMobile()
  ? new THREE.MeshBasicMaterial({ color: 0x00d9ff })
  : new THREE.MeshStandardMaterial({ 
      color: 0x00d9ff,
      roughness: 0.5,
      metalness: 0.1
    })
ブレンディングモード
// Additive blending for glows (cheaper than transparent)
material.blending = THREE.AdditiveBlending
material.transparent = true
material.depthWrite = false // Don't write to depth buffer

パート 6:ポストプロセスの最適化

選択的なポストプロセス
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'

function setupPostProcessing(renderer, scene, camera, mobile) {
  const composer = new EffectComposer(renderer)
  
  // Always add render pass
  composer.addPass(new RenderPass(scene, camera))
  
  // Bloom only on desktop
  if (!mobile) {
    const bloomPass = new UnrealBloomPass(
      new THREE.Vector2(window.innerWidth, window.innerHeight),
      1.5,  // strength
      0.4,  // radius
      0.85  // threshold
    )
    composer.addPass(bloomPass)
  }
  
  return composer
}

パート 7:グラフィックス全般のベストプラクティス

1. オブジェクトプーリング
/**
 * Object pool to reuse objects instead of creating/destroying.
 */
class ObjectPool {
  constructor(createFn, resetFn) {
    this.pool = []
    this.createFn = createFn
    this.resetFn = resetFn
  }
  
  /**
   * Get object from pool or create new one.
   */
  acquire() {
    if (this.pool.length > 0) {
      return this.pool.pop()
    }
    return this.createFn()
  }
  
  /**
   * Return object to pool.
   */
  release(obj) {
    this.resetFn(obj)
    this.pool.push(obj)
  }
}

// Usage: Particle pool
const particlePool = new ObjectPool(
  // Create function
  () => {
    const geometry = new THREE.SphereGeometry(0.1)
    const material = new THREE.MeshBasicMaterial({ color: 0xffffff })
    return new THREE.Mesh(geometry, material)
  },
  // Reset function
  (particle) => {
    particle.position.set(0, 0, 0)
    particle.visible = false
  }
)

// Spawn particle
const particle = particlePool.acquire()
particle.position.set(Math.random(), Math.random(), Math.random())
particle.visible = true
scene.add(particle)

// Later: Return to pool
scene.remove(particle)
particlePool.release(particle)
2. 可視性カリング
/**
 * Hide objects far from camera.
 */
function updateVisibility(camera, objects, maxDistance = 50) {
  const cameraPos = camera.position
  
  objects.forEach(obj => {
    const distance = obj.position.distanceTo(cameraPos)
    obj.visible = distance < maxDistance
  })
}
3. 遅延読み込み
/**
 * Load textures on demand.
 */
class LazyTextureLoader {
  constructor() {
    this.loader = new THREE.TextureLoader()
    this.cache = new Map()
  }
  
  async load(url) {
    // Check cache
    if (this.cache.has(url)) {
      return this.cache.get(url)
    }
    
    // Load texture
    return new Promise((resolve, reject) => {
      this.loader.load(
        url,
        (texture) => {
          this.cache.set(url, texture)
          resolve(texture)
        },
        undefined,
        reject
      )
    })
  }
}

パート 8:パフォーマンスの監視

FPS カウンター
/**
 * Simple FPS monitor.
 */
class FPSMonitor {
  constructor() {
    this.frames = 0
    this.lastTime = performance.now()
    this.fps = 60
  }
  
  update() {
    this.frames++
    const now = performance.now()
    
    if (now >= this.lastTime + 1000) {
      this.fps = Math.round((this.frames * 1000) / (now - this.lastTime))
      this.frames = 0
      this.lastTime = now
      
      // Warn if FPS drops
      if (this.fps < 30) {
        console.warn(`Low FPS: ${this.fps}`)
      }
    }
  }
  
  getFPS() {
    return this.fps
  }
}

// Usage
const fpsMonitor = new FPSMonitor()

function animate() {
  requestAnimationFrame(animate)
  fpsMonitor.update()
  renderer.render(scene, camera)
}
GPU メモリの監視
/**
 * Monitor GPU memory usage.
 */
function logMemoryUsage(renderer) {
  const info = renderer.info
  
  console.log('GPU Memory:', {
    geometries: info.memory.geometries,
    textures: info.memory.textures,
    programs: info.programs.length,
    drawCalls: info.render.calls,
    triangles: info.render.triangles
  })
}

// Call periodically
setInterval(() => logMemoryUsage(renderer), 5000)

重要な最適化チェックリスト

最適化の前に
  • まずプロファイリングを行う(Chrome DevTools の Performance タブ)
  • ボトルネックを特定する(CPU と GPU のどちらか?)
  • 目標 FPS を設定する(通常は 60fps = 1 フレームあたり 16ms)
ジオメトリ
  • 繰り返し配置するオブジェクトには InstancedMesh を使用する
  • 遠くにあるオブジェクトには LOD を実装する
  • 静的なジオメトリを結合する
  • BufferGeometry を使用する(Geometry ではなく)
  • 未使用のジオメトリを破棄する
テクスチャ
  • 必要最小限のテクスチャサイズを使用する
  • 2 の累乗の寸法を使用する
  • テクスチャを圧縮する(basis/KTX2)
  • ミップマップを使用しない場合は、minFilter = LinearFilter に設定する
  • 未使用のテクスチャを破棄する
マテリアル
  • 複数のオブジェクト間でマテリアルを共有する
  • モバイルでは、より軽量なマテリアルの種類を使用する
  • 透明なオブジェクトを制限する
  • グローには加算ブレンディングを使用する
  • 未使用のマテリアルを破棄する
ライティング
  • ライトの数を制限する(モバイルでは 1~2 個、デスクトップでは 3~5 個)
  • モバイルではシャドウを無効にする
  • 可能な場合はベイク済みライティングを使用する
  • スポットライトよりもディレクショナルライトやポイントライトを優先する
レンダリング
  • ピクセル比を制限する(モバイルでは 1.5 倍、デスクトップでは 2 倍)
  • モバイルではアンチエイリアスを無効にする
  • 静的なシーンには条件付きレンダリングを使用する
  • 視錐台カリングを実装する
  • モバイルではポストプロセスを制限する
モバイル固有
  • モバイルデバイスを検出する
  • ジオメトリの複雑さを軽減する
  • 負荷の高い機能を無効にする
  • ピクセル比を下げる
  • 実機でテストする(デスクトップブラウザーだけでテストしない)

よくあるパフォーマンス低下の原因

  1. ドローコールが多すぎる → InstancedMesh を使用する
  2. テクスチャの解像度が高すぎる → 1K または 2K にリサイズする
  3. ライトが多すぎる → 2~3 個に制限する
  4. 透明なオブジェクト → 控えめに使用し、最後にレンダリングする
  5. モバイルでのポストプロセス → 無効にするか、簡素化する
  6. メモリリーク → ジオメトリ、マテリアル、テクスチャは必ず破棄する
  7. 不要な再レンダリング → 条件付きレンダリングを使用する
  8. モバイルでのピクセル比が高すぎる → 1.5 倍に制限する

パフォーマンステストのワークフロー

1. 対象デバイスでテストする
// Detect and log device info
console.log('Device Info:', {
  userAgent: navigator.userAgent,
  pixelRatio: window.devicePixelRatio,
  screen: `${window.screen.width}x${window.screen.height}`,
  gpu: renderer.capabilities.getMaxAnisotropy()
})
2. Chrome DevTools でプロファイリングする
  1. DevTools を開き、Performance タブを選択する
  2. 5~10 秒間のレンダリングを記録する
  3. 次の項目を確認する:
    • 長いフレーム(>16ms)
    • GPU のボトルネック
    • メモリリーク
3. 最適化を A/B テストする
// Feature flag for testing
const ENABLE_SHADOWS = !isMobile()
const ENABLE_BLOOM = !isMobile()
const MAX_PARTICLE_COUNT = isMobile() ? 100 : 500

リソース

コメント