Files
skills/plugins/suaveplan-threejs/skills/threejs-lighting/SKILL.md
admin 9bf56a806b Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 01:10:00 +01:00

14 KiB

name, description
name description
threejs-lighting Three.js lighting systems adapted for React Three Fiber and Suaveplan R3F packages. Covers declarative light JSX components, shadow map configuration, environment maps with @react-three/drei, image-based lighting, light helpers, and @suaveplan/r3f-performance for light optimization and adaptive quality.

This skill covers lighting in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets React 19 + TypeScript + R3F and follows Suaveplan CLAUDE.md conventions.

The user needs to light a 3D scene: configure light types, enable shadows, use environment maps for image-based lighting, or optimize light performance.

Core Concepts

Three.js Lighting Model

Three.js provides physically-based rendering (PBR) with several light types. Lights contribute to material shading based on the Lambertian/GGX BRDF model.

R3F Lighting Approach

In R3F, lights are JSX elements added to the scene graph:

Three.js (Imperative) R3F (Declarative)
new AmbientLight(0xffffff, 0.5) <ambientLight intensity={0.5} />
scene.add(light) Place inside <Canvas> tree
light.position.set(5, 5, 5) position={[5, 5, 5]} prop
light.castShadow = true castShadow prop
light.shadow.mapSize.set(2048, 2048) shadow-mapSize={[2048, 2048]}

Suaveplan R3F Packages

Package Purpose Import
@suaveplan/r3f Core scene utilities import { useR3F } from '@suaveplan/r3f'
@suaveplan/r3f-performance Light budget monitoring, adaptive shadows import { usePerformanceMonitor } from '@suaveplan/r3f-performance'
@suaveplan/r3f-shaders Custom light-reactive materials import { useAnimatedShader } from '@suaveplan/r3f-shaders'

Light Types

AmbientLight

Uniform light from all directions. No shadows. Use for base illumination.

<ambientLight
  color="#ffffff"     // Light color
  intensity={0.4}    // Brightness (0-N, default 1)
/>

DirectionalLight

Parallel rays from a direction (like the sun). Casts shadows.

<directionalLight
  position={[10, 10, 5]}    // Direction = position - target
  intensity={1.5}
  color="#fffbe6"
  castShadow
  shadow-mapSize={[2048, 2048]}
  shadow-camera-left={-10}
  shadow-camera-right={10}
  shadow-camera-top={10}
  shadow-camera-bottom={-10}
  shadow-camera-near={0.1}
  shadow-camera-far={50}
  shadow-bias={-0.0001}
/>

PointLight

Emits light in all directions from a point (like a light bulb). Casts shadows.

<pointLight
  position={[0, 3, 0]}
  intensity={100}           // In candela (physical mode)
  distance={10}             // Maximum range (0 = infinite)
  decay={2}                 // Physical falloff (2 = realistic)
  color="#ffd700"
  castShadow
  shadow-mapSize={[1024, 1024]}
  shadow-bias={-0.001}
/>

SpotLight

Cone-shaped light (like a flashlight or stage light). Casts shadows.

<spotLight
  position={[0, 5, 0]}
  target-position={[0, 0, 0]}  // Aim point
  angle={Math.PI / 6}          // Cone angle (radians)
  penumbra={0.5}                // Soft edge (0-1)
  intensity={200}
  distance={20}
  decay={2}
  color="#ffffff"
  castShadow
  shadow-mapSize={[2048, 2048]}
  shadow-bias={-0.0001}
/>

HemisphereLight

Two-color gradient light (sky + ground). No shadows.

<hemisphereLight
  color="#87ceeb"        // Sky color
  groundColor="#8b4513"  // Ground color
  intensity={0.6}
/>

RectAreaLight

Rectangular area light (like a window or fluorescent panel). No shadow map support.

import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js';
import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';

// Must initialize uniforms before use
RectAreaLightUniformsLib.init();

<rectAreaLight
  position={[0, 3, -2]}
  rotation={[-Math.PI / 2, 0, 0]}
  width={4}
  height={2}
  intensity={5}
  color="#ffffff"
/>

Shadow Configuration

Enabling Shadows

Shadows require three things: Canvas shadow prop, light castShadow, and mesh receiveShadow/castShadow.

import { Canvas } from '@react-three/fiber';

export function ShadowScene() {
  return (
    <Canvas
      shadows                    // Enable shadow maps
      // Or configure shadow type:
      // shadows={{ type: PCFSoftShadowMap }}
    >
      {/* Light that casts shadows */}
      <directionalLight
        position={[5, 5, 5]}
        castShadow
        shadow-mapSize={[2048, 2048]}
        shadow-camera-far={20}
        shadow-bias={-0.0001}
      />

      {/* Object that casts shadows */}
      <mesh position={[0, 1, 0]} castShadow>
        <boxGeometry args={[1, 1, 1]} />
        <meshStandardMaterial color="tomato" />
      </mesh>

      {/* Ground that receives shadows */}
      <mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
        <planeGeometry args={[20, 20]} />
        <meshStandardMaterial color="#444" />
      </mesh>
    </Canvas>
  );
}

Shadow Map Types

import { PCFShadowMap, PCFSoftShadowMap, VSMShadowMap, BasicShadowMap } from 'three';

// BasicShadowMap: fastest, hard edges
// PCFShadowMap: default, some softness
// PCFSoftShadowMap: softer, more expensive
// VSMShadowMap: variance shadow map, supports blurring

<Canvas shadows={{ type: PCFSoftShadowMap }}>

Shadow Camera Frustum

For directional lights, the shadow camera is orthographic. Size the frustum tightly around the scene for better shadow quality:

<directionalLight
  castShadow
  shadow-camera-left={-8}
  shadow-camera-right={8}
  shadow-camera-top={8}
  shadow-camera-bottom={-8}
  shadow-camera-near={0.5}
  shadow-camera-far={30}
/>

Cascaded Shadow Maps (CSM)

For large scenes, use drei's CSM:

import { AccumulativeShadows, RandomizedLight } from '@react-three/drei';

export function SoftShadows() {
  return (
    <AccumulativeShadows
      temporal
      frames={60}
      alphaTest={0.85}
      scale={10}
      position={[0, 0.01, 0]}
    >
      <RandomizedLight
        amount={8}
        radius={2}
        ambient={0.5}
        position={[5, 5, -3]}
        bias={0.001}
      />
    </AccumulativeShadows>
  );
}

Environment Maps and IBL

Environment Component (drei)

The simplest way to add image-based lighting:

import { Environment } from '@react-three/drei';

// Preset environments
<Environment preset="sunset" />
<Environment preset="city" />
<Environment preset="forest" />
<Environment preset="studio" />

// Custom HDR
<Environment files="/textures/env.hdr" />

// As scene background
<Environment preset="sunset" background />

// Blur the background
<Environment preset="sunset" background blur={0.5} />

Available Presets

Preset Description Use Case
apartment Indoor apartment Interior scenes
city Urban skyline Outdoor urban
dawn Early morning sky Warm exterior
forest Dense forest Nature scenes
lobby Hotel lobby Indoor formal
night Night sky Dark scenes
park Park setting Outdoor casual
studio Photography studio Product shots
sunset Golden hour Dramatic exterior
warehouse Industrial space Gritty interior

Custom Environment Map

import { useRef } from 'react';
import { useThree } from '@react-three/fiber';
import { CubeCamera } from '@react-three/drei';

export function ReflectiveSphere() {
  return (
    <CubeCamera resolution={256} frames={Infinity}>
      {(texture) => (
        <mesh>
          <sphereGeometry args={[1, 64, 64]} />
          <meshStandardMaterial
            envMap={texture}
            metalness={1}
            roughness={0}
          />
        </mesh>
      )}
    </CubeCamera>
  );
}

Light Helpers

Visualize light positions and frustums during development:

import { useHelper } from '@react-three/drei';
import { useRef } from 'react';
import {
  DirectionalLightHelper,
  SpotLightHelper,
  PointLightHelper,
  CameraHelper,
} from 'three';
import type { DirectionalLight, SpotLight, PointLight } from 'three';

export function DebugLights({ debug = false }: { debug?: boolean }) {
  const dirRef = useRef<DirectionalLight>(null);
  const spotRef = useRef<SpotLight>(null);
  const pointRef = useRef<PointLight>(null);

  // Helpers only rendered when debug is true
  useHelper(debug && dirRef, DirectionalLightHelper, 1, 'red');
  useHelper(debug && spotRef, SpotLightHelper, 'green');
  useHelper(debug && pointRef, PointLightHelper, 0.5, 'blue');

  return (
    <>
      <directionalLight ref={dirRef} position={[5, 5, 5]} castShadow />
      <spotLight ref={spotRef} position={[0, 5, 0]} angle={Math.PI / 6} />
      <pointLight ref={pointRef} position={[-3, 2, 0]} intensity={50} />
    </>
  );
}

Lighting Recipes

Outdoor Scene (Sun + Sky)

import { Sky, Environment } from '@react-three/drei';

export function OutdoorLighting() {
  return (
    <>
      {/* Sun light */}
      <directionalLight
        position={[50, 50, 25]}
        intensity={2}
        color="#fff5e1"
        castShadow
        shadow-mapSize={[4096, 4096]}
        shadow-camera-left={-30}
        shadow-camera-right={30}
        shadow-camera-top={30}
        shadow-camera-bottom={-30}
      />

      {/* Fill light (sky) */}
      <hemisphereLight
        color="#87ceeb"
        groundColor="#362312"
        intensity={0.4}
      />

      {/* Sky dome */}
      <Sky
        sunPosition={[50, 50, 25]}
        turbidity={3}
        rayleigh={0.5}
      />
    </>
  );
}

Indoor Scene (Area Lights + Ambient)

export function IndoorLighting() {
  return (
    <>
      {/* Ambient base */}
      <ambientLight intensity={0.2} color="#f0e6d3" />

      {/* Ceiling lights */}
      <pointLight position={[0, 2.8, 0]} intensity={80} decay={2} distance={8} castShadow />
      <pointLight position={[3, 2.8, 0]} intensity={60} decay={2} distance={6} />

      {/* Window light */}
      <directionalLight
        position={[-5, 3, 2]}
        intensity={1.5}
        color="#b8d4e3"
        castShadow
        shadow-mapSize={[2048, 2048]}
      />
    </>
  );
}

Product Studio Lighting

Use <Environment> with nested <Lightformer> components for key, fill, and rim lighting. Add <ContactShadows> from drei for soft ground shadows without shadow maps. Configure form="rect" or form="ring", position, intensity, and scale for each lightformer.

Adaptive Lighting with @suaveplan/r3f-performance

Use usePerformanceMonitor to track FPS and switch between quality presets (high/medium/low). Each preset configures shadow map size (4096/2048/1024), env map resolution, and max light count. Scale down on onDecline, scale up on onIncline.

Light Animation

Flickering Light

import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import type { PointLight } from 'three';

export function FlickeringLight({ position }: { position: [number, number, number] }) {
  const lightRef = useRef<PointLight>(null);

  useFrame(({ clock }) => {
    if (!lightRef.current) return;
    const t = clock.elapsedTime;
    const flicker = Math.sin(t * 20) * 0.1 + Math.sin(t * 37) * 0.05 + Math.sin(t * 53) * 0.03;
    lightRef.current.intensity = 80 + flicker * 40;
  });

  return (
    <pointLight
      ref={lightRef}
      position={position}
      color="#ff9500"
      distance={8}
      decay={2}
      castShadow
    />
  );
}

Testing Lighting

Vitest Tests

import { describe, it, expect } from 'vitest';
import { DirectionalLight, PointLight } from 'three';

describe('Lighting setup', () => {
  it('creates directional light with shadow', () => {
    const light = new DirectionalLight(0xffffff, 1.5);
    light.castShadow = true;
    light.shadow.mapSize.set(2048, 2048);
    expect(light.castShadow).toBe(true);
    expect(light.shadow.mapSize.x).toBe(2048);
  });

  it('configures point light physical decay', () => {
    const light = new PointLight(0xffd700, 100, 10, 2);
    expect(light.decay).toBe(2);
    expect(light.distance).toBe(10);
  });
});

Storybook Stories

import type { Meta, StoryObj } from '@storybook/react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Environment } from '@react-three/drei';

function LightingDemo() {
  return (
    <Canvas shadows camera={{ position: [5, 5, 5] }} style={{ height: 400 }}>
      <ambientLight intensity={0.3} />
      <directionalLight position={[5, 5, 5]} castShadow />
      <mesh castShadow position={[0, 0.5, 0]}>
        <boxGeometry />
        <meshStandardMaterial color="coral" />
      </mesh>
      <mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]}>
        <planeGeometry args={[10, 10]} />
        <meshStandardMaterial color="#555" />
      </mesh>
      <OrbitControls />
    </Canvas>
  );
}

const meta: Meta = { title: 'R3F/Lighting/ShadowDemo', component: LightingDemo };
export default meta;
export const Default: StoryObj = {};

Checklist

When setting up lighting in R3F:

  1. Enable shadows on <Canvas> and choose a shadow map type
  2. Use castShadow on lights and objects, receiveShadow on surfaces
  3. Size shadow camera frustum tightly around the visible scene
  4. Set shadow-bias to prevent shadow acne (start with -0.0001)
  5. Use <Environment> from drei for quick IBL and reflections
  6. Use useHelper to visualize light positions during development
  7. Limit active shadow-casting lights (expensive: 1 shadow map per light per frame)
  8. Use @suaveplan/r3f-performance for adaptive shadow quality
  9. Combine ambient/hemisphere with directional for natural-looking scenes
  10. Keep lighting configuration components under 250 lines