Skip to content

Latest commit

 

History

History
405 lines (311 loc) · 11.5 KB

File metadata and controls

405 lines (311 loc) · 11.5 KB

Three.js Performance Guide

Performance Monitoring

To monitor CPU usage while running your Three.js application, you can utilize Chrome DevTools : PerformanceThreeJS

Monitoring Draw Calls

You can easily check the number of draw calls made by the renderer using the following code:

// Monitor draw calls
console.log(renderer.info.render.calls)

Using stats.js for Performance Metrics

For a more detailed performance analysis, you can integrate stats.js into your application. This library provides real-time performance statistics, including frame rates and rendering times. Here’s how to set it up:

import Stats from 'three/examples/jsm/libs/stats.module';

// Create a new Stats instance
const stats = new Stats();
document.body.appendChild(stats.dom);

// Animation loop to update stats
function animate() {
  stats.begin(); // Start measuring
  // Render your scene here
  stats.end(); // Stop measuring
  requestAnimationFrame(animate); // Continue the animation loop
}

// Start the animation
animate();

Lighting & Shadows Performance

Understanding the performance impact of lights and shadows is crucial for optimizing Three.js applications.

Shadow Performance Hierarchy

From best to worst performance:

  1. No Shadows (Fastest)

    • Use for non-essential objects
    • Best for mobile/low-end devices
  2. DirectionalLight with shadows

    • One additional scene render
    • Good for sun/moon effects
    • Best choice for general shadow casting
  3. SpotLight with shadows

    • One additional scene render
    • Good for focused areas like flashlights
    • More expensive than DirectionalLight
  4. PointLight with shadows (Most Expensive)

    • Six additional scene renders!
    • Extremely costly for performance
    • Use very sparingly, if at all

Understanding Draw Calls

Each shadow-casting light adds significant draw calls:

// DirectionalLight/SpotLight
drawCalls = numberOfMeshes * 1

// PointLight (6x more expensive!)
drawCalls = numberOfMeshes * 6

// Example with 5 meshes and 2 point lights:
totalDrawCalls = baseCalls + (meshes * 6 * pointLights)
// 5 base + (5 * 6 * 2) = 65 draw calls

Good draw calls for web-games : <200

Optimization Strategies

1. Light Management

// GOOD - Main shadow casting light
const mainLight = new THREE.DirectionalLight()
mainLight.castShadow = true
mainLight.position.set(50, 30, 50)
mainLight.intensity = 1.5

// GOOD - Ambient fill light for overall scene brightness
const ambientLight = new THREE.AmbientLight('#ffffff', 0.4)

// GOOD - Hemisphere light for sky/ground color variation
const hemiLight = new THREE.HemisphereLight(
  '#skyColor',   // Sky color
  '#groundColor', // Ground color
  0.5            // Intensity
)

// ACCEPTABLE - Non-shadow point light for local highlights
const accentLight = new THREE.PointLight('#ffffff', 1.0)
accentLight.castShadow = false  // Important!

// BAD - Point light with shadows (extremely expensive!)
const badLight = new THREE.PointLight('#ffffff', 1.0)
badLight.castShadow = true  // Will cause 6 additional renders!
Light Type Performance Guide

From best to worst performance:

  1. AmbientLight

    • No shadows possible
    • Extremely cheap
    • Good for base illumination
    • Use to reduce shadow darkness
  2. HemisphereLight

    • No shadows possible
    • Very performant
    • Great for outdoor scenes
    • Provides subtle color variation
  3. DirectionalLight (without shadows)

    • Good for sun/moon simulation
    • Relatively cheap
    • Consistent shadows across scene
  4. DirectionalLight (with shadows)

    • One additional render pass
    • Best choice for main shadow caster
    • Good balance of quality/performance
  5. SpotLight (without shadows)

    • Good for focused lighting
    • Moderate performance impact
    • Use for highlights/accents
  6. SpotLight (with shadows)

    • One additional render pass
    • More expensive than DirectionalLight
    • Use sparingly
  7. PointLight (without shadows)

    • Expensive but manageable
    • Good for local lighting
    • Keep radius small when possible
  8. PointLight (with shadows) ⚠️

    • EXTREMELY expensive (6 render passes!)
    • Should almost never be used
    • Consider alternatives:
      • SpotLight with shadows
      • Multiple non-shadow PointLights
      • Baked shadows/lighting
Recommended Light Setups
// Basic Outdoor Scene
const lights = {
  main: new THREE.DirectionalLight('#ffffff', 1.5),  // Sun
  hemi: new THREE.HemisphereLight('#skyblue', '#groundcolor', 0.5),
  ambient: new THREE.AmbientLight('#ffffff', 0.2)
}
lights.main.castShadow = true

// Indoor Scene
const lights = {
  main: new THREE.DirectionalLight('#ffffff', 0.5),  // Window light
  spots: [
    new THREE.SpotLight('#ffffff', 0.7),  // Key light
    new THREE.SpotLight('#ffffff', 0.3)   // Fill light
  ],
  ambient: new THREE.AmbientLight('#ffffff', 0.3)
}
lights.main.castShadow = true

// Not needed since it's by default at false, but just to show usage
lights.spots.forEach(light => light.castShadow = false)

// Night Scene
const lights = {
  moon: new THREE.DirectionalLight('#blue', 0.5),
  points: Array(5).fill(0).map(() => new THREE.PointLight('#orange', 0.4)),
  ambient: new THREE.AmbientLight('#000033', 0.2)
}
lights.moon.castShadow = true

// Not needed since it's by default at false, but just to show usage
lights.points.forEach(light => light.castShadow = false)  // Important!
Tips for Light Performance
  • Use ONE main shadow-casting light (usually DirectionalLight)
  • Combine AmbientLight and HemisphereLight for base illumination
  • Use non-shadow PointLights for accent lighting
  • Avoid shadow-casting SpotLights when possible
  • NEVER use shadow-casting PointLights in production
  • Consider light radius/distance for PointLights and SpotLights
  • Use light helpers during development to visualize coverage

2. Shadow Map Optimization

const directionalLight = new THREE.DirectionalLight()

// Reduce shadow map size for better performance
// Set the resolution of the shadow map texture (higher = sharper shadows but more expensive)
directionalLight.shadow.mapSize.height = 1024
directionalLight.shadow.mapSize.width = 1024

3. Selective Shadow Casting

// Only enable shadows on important objects
mainObject.traverse((child) => {
  if (child.isMesh) {
    child.castShadow = false       // Disable casting
    child.receiveShadow = true     // Still receive shadows
  }
})

Best Practices

Lighting Setup

  • Use ONE DirectionalLight for main shadows
  • Add non-shadow PointLights for ambient lighting
  • Avoid shadow-casting PointLights
  • Consider baking shadows for static scenes

Shadow Quality

  • Use smaller shadow maps (512x512) by default
  • Increase only for close-up shadows
  • Disable autoUpdate for static scenes
  • Consider shadow bias adjustment for quality/performance balance

Mesh Optimization

  • Combine small meshes where possible
  • Use instancing for repeated objects
  • Target < 200 draw calls for web
  • Group static objects

Materials

// Shadows require appropriate materials
const material = new THREE.MeshStandardMaterial({
  receiveShadow: true,
  castShadow: true
})

// BasicMaterial doesn't support shadows
const basicMaterial = new THREE.MeshBasicMaterial()  // No shadows

Manual Updates of Shadows maps

  • autoUpdate: Enables automatic updates to the shadows in the scene

    • Default is true
  • When autoUpdate is false, you need to manually update the shadows maps with needsUpdate

  • needsUpdate: Forces shadow maps to update on next render

    • Default is false
    • Required when autoUpdate = false to manually trigger shadow updates
    • Must be set to true followed by a render call to update shadows
Example : Only when a specific object moves
renderer.shadowMap.autoUpdate = false

// Update shadows only when necessary
function onObjectMove() {
  renderer.shadowMap.needsUpdate = true
}
Example : Only 1 time per second
renderer.shadowMap.autoUpdate = false

// Update shadows only once per second
function updateShadows() {
  renderer.shadowMap.needsUpdate = true
}

setInterval(updateShadows, 1000) 
// Of course, you can use requestAnimationFrame instead of setInterval with proper delta time logic

Common Pitfalls

  1. Too Many Shadow Casters

    • Every shadow-casting light multiplies render calls
    • Each mesh with castShadow = true adds to the cost
  2. Unnecessary Shadow Updates

    • Static scenes don't need autoUpdate
    • Update shadows only when the scene changes
  3. Oversized Shadow Maps

    • Large shadow maps impact memory and performance
    • Start small and increase only if needed
  4. Point Light Shadows

    • Extremely expensive (6x normal shadow cost)
    • Consider alternatives like SpotLights

Testing & Profiling

Always test on target devices:

  • Mobile performance differs significantly
  • Monitor FPS and draw calls
  • Use Chrome DevTools Performance tab
  • Test with varying numbers of objects and lights

Additional Resources

Advanced: Cascaded Shadow Maps (THREE-CSM)

THREE-CSM is a solution for rendering high-quality directional light shadows over large distances. It works by splitting the view frustum into several segments, each with its own shadow map.

Benefits

  • Better shadow quality over large distances
  • More efficient than increasing shadow map resolution
  • Good for open world scenes

Basic Setup

import { CSM } from 'three-csm'

// Create CSM instance
const csm = new CSM({
  maxFar: 1000,           // How far shadows are rendered
  cascades: 4,            // Number of shadow cascades
  shadowMapSize: 1024,    // Resolution of each cascade
  lightDirection: new THREE.Vector3(-1, -1, -1),
  camera: camera,         // Your THREE.Camera instance
  parent: scene           // Your THREE.Scene instance
})

// Update in animation loop
function animate() {
  csm.update(camera.matrix)
  renderer.render(scene, camera)
}

Performance Considerations

  • Each cascade adds one render pass
  • Balance cascade count with performance needs
  • Typically 3-4 cascades is sufficient
  • Adjust maxFar and shadowMapSize based on scene scale

When to Use CSM

  • Large outdoor environments
  • When shadow quality at distance is important
  • Games with terrain or large landscapes
  • When standard shadows show obvious cutoff points

Example Configuration for Different Scenarios

// High-end desktop setup
const csmHigh = new CSM({
  cascades: 4,
  shadowMapSize: 2048,
  maxFar: 2000,
  mode: 'practical'
})

// Mobile-friendly setup
const csmLow = new CSM({
  cascades: 2,
  shadowMapSize: 512,
  maxFar: 500,
  mode: 'uniform'
})

Note: THREE-CSM might need updates for newer Three.js versions. Check compatibility before implementation.


Example Project: Enari Engine

A previous project demonstrating good performance practices:

Also, the default scene uses baked shadows (found it on Sketchfab) You can make your own worlds with static baked shadows (Blender)