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>
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:
- Enable
shadowson<Canvas>and choose a shadow map type - Use
castShadowon lights and objects,receiveShadowon surfaces - Size shadow camera frustum tightly around the visible scene
- Set
shadow-biasto prevent shadow acne (start with-0.0001) - Use
<Environment>from drei for quick IBL and reflections - Use
useHelperto visualize light positions during development - Limit active shadow-casting lights (expensive: 1 shadow map per light per frame)
- Use
@suaveplan/r3f-performancefor adaptive shadow quality - Combine ambient/hemisphere with directional for natural-looking scenes
- Keep lighting configuration components under 250 lines