threejs-graphics-optimizer

安装量: 75
排名: #10444

安装

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

THREE.js Graphics Optimizer

Version: 1.0 Focus: Performance optimization for THREE.js and graphics applications Purpose: Build smooth 60fps graphics experiences across all devices including mobile

Philosophy: Performance-First Graphics The 16ms Budget

Target: 60 FPS = 16.67ms per frame

Frame budget breakdown:

JavaScript logic: ~5-8ms Rendering (GPU): ~8-10ms Browser overhead: ~2ms

If you exceed 16ms: Frames drop, stuttering occurs.

Mobile vs Desktop Reality

Desktop: Powerful GPU, lots of VRAM, high pixel ratios Mobile: Constrained GPU, limited VRAM, battery concerns, thermal throttling

Design philosophy: Optimize for mobile, scale up for desktop (not vice versa).

Part 1: Core Optimization Principles 1. Minimize Draw Calls

The Problem: Each object = one draw call. 1000 objects = 1000 calls = slow.

Solution: Geometry Merging

// ❌ 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)

When to use:

Many similar objects (particles, trees, enemies) Static or semi-static positioning Shared material/geometry 2. Level of Detail (LOD)

Render simpler geometry when objects are far away:

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

  1. Frustum Culling (Automatic)

THREE.js automatically skips objects outside camera view. Help it:

// ❌ 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()

  1. Texture Optimization

Texture size matters:

4K texture (4096x4096): 64MB VRAM (uncompressed) 2K texture (2048x2048): 16MB VRAM 1K texture (1024x1024): 4MB VRAM

Rules:

Use smallest textures that look good Power-of-two dimensions (512, 1024, 2048) Compress textures (use basis/KTX2 format) 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() }

Part 2: Mobile-Specific Optimization Mobile Detection & Adaptation /* * 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())

Mobile Performance Settings /* * 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')

} }

Fallback Pattern /* * 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)

Part 3: Render Loop Optimization Efficient Animation Loop 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) } } }

Conditional Rendering /* * 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 }

Part 4: Memory Management Dispose Pattern /* * 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 } }) }

Memory Leak Prevention 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()

} }

Part 5: Material Optimization Material Sharing // ❌ 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) }

Cheaper Material Types

Performance ranking (fastest to slowest):

MeshBasicMaterial - No lighting, flat shading
MeshLambertMaterial - Simple diffuse lighting
MeshPhongMaterial - Specular highlights
MeshStandardMaterial - PBR (expensive)
MeshPhysicalMaterial - Advanced PBR (very expensive)
// Mobile: Use cheaper materials
const material = isMobile()
? new THREE.MeshBasicMaterial({ color: 0x00d9ff })
new THREE.MeshStandardMaterial({ color: 0x00d9ff, roughness: 0.5, metalness: 0.1 })

Blending Modes // Additive blending for glows (cheaper than transparent) material.blending = THREE.AdditiveBlending material.transparent = true material.depthWrite = false // Don't write to depth buffer

Part 6: Post-Processing Optimization Selective Post-Processing 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 }

Part 7: General Graphics Best Practices 1. Object Pooling /* * 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)

  1. Visibility Culling /**
  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 }) }

  1. Lazy Loading /**
  2. 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
  )
})

} }

Part 8: Performance Monitoring FPS Counter /* * 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 Memory Monitoring /* * 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)

Critical Optimization Checklist Before Optimizing Profile first (Chrome DevTools Performance tab) Identify bottleneck (CPU or GPU?) Set target FPS (usually 60fps = 16ms/frame) Geometry Use InstancedMesh for repeated objects Implement LOD for distant objects Merge static geometries Use BufferGeometry (not Geometry) Dispose unused geometries Textures Use smallest texture size needed Power-of-two dimensions Compress textures (basis/KTX2) Set minFilter = LinearFilter if no mipmaps Dispose unused textures Materials Share materials across objects Use cheaper material types on mobile Limit transparent objects Use additive blending for glows Dispose unused materials Lighting Limit lights (1-2 on mobile, 3-5 on desktop) Disable shadows on mobile Use baked lighting where possible Prefer directional/point over spot lights Rendering Cap pixel ratio (1.5x mobile, 2x desktop) Disable antialiasing on mobile Use conditional rendering for static scenes Implement frustum culling Limit post-processing on mobile Mobile-Specific Detect mobile devices Reduce geometry complexity Disable expensive features Lower pixel ratio Test on real devices (not just desktop browser) Common Performance Killers Too many draw calls → Use InstancedMesh High-resolution textures → Resize to 1K or 2K Too many lights → Limit to 2-3 Transparent objects → Use sparingly, render last Post-processing on mobile → Disable or simplify Memory leaks → Always dispose geometries/materials/textures Unnecessary re-renders → Use conditional rendering High pixel ratio on mobile → Cap at 1.5x Performance Testing Workflow 1. Test on Target Devices // 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() })

  1. Profile with Chrome DevTools Open DevTools → Performance tab Record 5-10 seconds of rendering Look for: Long frames (>16ms) GPU bottlenecks Memory leaks
  2. A/B Test Optimizations // Feature flag for testing const ENABLE_SHADOWS = !isMobile() const ENABLE_BLOOM = !isMobile() const MAX_PARTICLE_COUNT = isMobile() ? 100 : 500

Resources THREE.js Docs: https://threejs.org/docs/ THREE.js Performance Tips: https://discoverthreejs.com/tips-and-tricks/ WebGL Fundamentals: https://webglfundamentals.org/ GPU Performance: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices

返回排行榜