threejs-impl-shadows
Shadows in Three.js require THREE explicit opt-in steps. Missing ANY step results in no shadows.
Works with
Agent Skills format with YAML frontmatter. Claude Code reads it as-is.
---
name: "threejs-impl-shadows"
description: "Shadows in Three.js require THREE explicit opt-in steps. Missing ANY step results in no shadows."
license: "MIT"
---
# threejs-impl-shadows
## Quick Reference
### Three-Step Shadow Opt-In
Shadows in Three.js require THREE explicit opt-in steps. Missing ANY step results in no shadows.
```js
import * as THREE from 'three';
// Step 1: Enable shadow maps on the renderer
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Step 2: Enable shadow casting on the light
light.castShadow = true;
// Step 3: Enable per-mesh shadow behavior
mesh.castShadow = true; // this mesh casts shadows
ground.receiveShadow = true; // this mesh receives shadows
```
### Shadow Map Types
| Type | Constant | Quality | Cost | Supports `.radius` |
|------|----------|---------|------|---------------------|
| Basic | `THREE.BasicShadowMap` | Hard edges, aliased | Lowest | No |
| PCF | `THREE.PCFShadowMap` | Slightly softened | Moderate | No |
| PCF Soft | `THREE.PCFSoftShadowMap` | Soft penumbra | Higher | No (ignores it) |
| VSM | `THREE.VSMShadowMap` | Gaussian blur | Moderate | Yes |
- **Default** in r160+: `THREE.PCFShadowMap`
- **PCFSoftShadowMap**: best general-purpose choice; IGNORES the `shadow.radius` property
- **VSMShadowMap**: supports `shadow.radius` and `shadow.blurSamples` but can exhibit light bleeding on thin geometry
### Critical Warnings
**NEVER** forget any of the three opt-in steps -- missing even one produces zero shadows with no error message.
**NEVER** leave the DirectionalLight shadow camera frustum at default size -- it is almost ALWAYS wrong for your scene. ALWAYS configure it manually.
**NEVER** use more than 1-2 shadow-casting PointLights -- each PointLight shadow renders 6 cubemap faces per frame.
**ALWAYS** set `shadow.mapSize` to power-of-two values (512, 1024, 2048, 4096).
**ALWAYS** add `light.target` to the scene when repositioning a DirectionalLight or SpotLight target.
---
## Shadow Map Types -- Detailed Comparison
### BasicShadowMap
No filtering applied. Produces hard, aliased shadow edges. Use ONLY for debugging or stylized rendering where hard shadows are intentional.
### PCFShadowMap (Default)
Percentage-Closer Filtering with a fixed-size kernel. Produces slightly softened edges. Good balance of quality and performance. The `shadow.radius` property has NO effect.
### PCFSoftShadowMap
Uses a variable kernel for softer penumbra simulation. Higher quality than PCF but more expensive. The `shadow.radius` property is IGNORED -- softness is determined automatically by the filter.
### VSMShadowMap
Variance Shadow Maps use a Gaussian blur pass. Supports `shadow.radius` (blur size) and `shadow.blurSamples` (sample count). Produces smooth soft shadows but suffers from **light bleeding** artifacts where thin geometry meets shadow receivers. NEVER use VSM for scenes with many thin overlapping shadow casters.
---
## Per-Light Shadow Configuration
### DirectionalLight Shadows
Uses an **orthographic** shadow camera. The frustum MUST be manually sized to encompass the shadowed area.
```js
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7.5);
dirLight.castShadow = true;
// Shadow map resolution
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
// Orthographic frustum -- MUST size manually
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 500;
dirLight.shadow.camera.left = -50;
dirLight.shadow.camera.right = 50;
dirLight.shadow.camera.top = 50;
dirLight.shadow.camera.bottom = -50;
// Anti-acne bias
dirLight.shadow.bias = -0.0001;
dirLight.shadow.normalBias = 0.02;
scene.add(dirLight);
scene.add(dirLight.target); // REQUIRED if repositioning target
```
**Frustum sizing rule**: Make the frustum as TIGHT as possible around the area that needs shadows. A frustum that is too large wastes shadow map resolution; a frustum that is too small clips shadows.
### SpotLight Shadows
Uses a **perspective** shadow camera that auto-configures from the SpotLight `.angle` and `.distance`. Manual frustum configuration is typically unnecessary.
```js
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(0, 10, 0);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.3;
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.bias = -0.0001;
scene.add(spotLight);
scene.add(spotLight.target);
```
SpotLightShadow exposes:
- `.focus` (number, default `1`) -- adjusts shadow camera FOV relative to spotlight FOV, range `[0, 1]`
### PointLight Shadows
Uses a **cubemap** (6 perspective cameras, one per face). This is the MOST expensive shadow type -- rendering the scene 6 times per shadow-casting PointLight per frame.
```js
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
pointLight.position.set(0, 5, 0);
pointLight.castShadow = true;
// Lower resolution to offset the 6x render cost
pointLight.shadow.mapSize.width = 512;
pointLight.shadow.mapSize.height = 512;
pointLight.shadow.bias = -0.001;
pointLight.shadow.camera.near = 0.5;
pointLight.shadow.camera.far = 50;
scene.add(pointLight);
```
**Performance rule**: ALWAYS prefer SpotLight shadows over PointLight shadows. A PointLight shadow costs 6x what a SpotLight shadow costs. Use PointLight shadows ONLY when omnidirectional shadow casting is absolutely required.
---
## Shadow Debugging with CameraHelper
ALWAYS use `CameraHelper` to visualize the shadow camera frustum when configuring shadows:
```js
const shadowHelper = new THREE.CameraHelper(dirLight.shadow.camera);
scene.add(shadowHelper);
```
The helper renders the frustum as wireframe lines. If shadows are clipped, missing, or low resolution, the helper reveals whether the frustum is too small, too large, or misaligned.
**ALWAYS** remove CameraHelper in production builds.
---
## Shadow Artifact Diagnosis Flowchart
```
Shadows not visible?
├── Check renderer.shadowMap.enabled === true
├── Check light.castShadow === true
├── Check mesh.castShadow / ground.receiveShadow === true
├── Check shadow camera frustum encompasses the scene (use CameraHelper)
└── Check shadow.camera.far is large enough
Striped lines on surfaces? (Shadow Acne)
├── Increase shadow.bias (start at -0.0001, go to -0.005)
├── Increase shadow.normalBias (0.02 to 0.1) for curved surfaces
└── Increase shadow.mapSize for more depth precision
Shadows float away from objects? (Peter Panning)
├── Reduce shadow.bias (you over-corrected for acne)
├── Use shadow.normalBias instead of shadow.bias
└── Increase shadow.mapSize resolution
Shadow edges shimmer when camera moves? (Shadow Swimming)
├── Increase shadow.mapSize resolution
├── Snap shadow camera to texel-aligned positions
└── Use Cascaded Shadow Maps (CSM) for large scenes
Light leaks through thin geometry? (Light Bleeding -- VSM only)
├── Switch from VSMShadowMap to PCFSoftShadowMap
├── Increase geometry thickness
└── Reduce shadow.radius value
Shadows on transparent/alpha-tested materials incorrect?
├── Set material.alphaTest threshold
├── Assign customDepthMaterial with matching alpha map
└── For fully transparent objects: use baked shadows or ContactShadows
```
---
## Transparent Material Shadows
By default, shadows treat ALL geometry as fully opaque. For alpha-tested materials (e.g., tree leaves), assign a `customDepthMaterial`:
```js
import * as THREE from 'three';
const alphaMap = new THREE.TextureLoader().load('leaf-alpha.png');
const leafMaterial = new THREE.MeshStandardMaterial({
map: texture,
alphaMap: alphaMap,
alphaTest: 0.5,
side: THREE.DoubleSide,
});
// Custom depth material for correct shadow casting
const depthMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.RGBADepthPacking,
map: texture,
alphaMap: alphaMap,
alphaTest: 0.5,
});
leafMesh.material = leafMaterial;
leafMesh.customDepthMaterial = depthMaterial;
leafMesh.castShadow = true;
```
**NEVER** expect transparent objects (`opacity < 1` without `alphaTest`) to cast correct real-time shadows. Use baked shadows or screen-space techniques instead.
---
## Static Shadow Optimization
For scenes where lights and shadow casters do NOT move, disable automatic shadow updates:
```js
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true; // render shadows once
```
Set `renderer.shadowMap.needsUpdate = true` ONLY when something changes. This eliminates per-frame shadow map rendering entirely for static scenes.
Per-light control:
```js
light.shadow.autoUpdate = false;
light.shadow.needsUpdate = true; // update this light's shadow once
```
---
## ContactShadows Alternative (Drei / React Three Fiber)
For ground-plane shadows, Drei's `<ContactShadows>` provides high-quality soft shadows at lower cost than shadow maps:
```jsx
import { ContactShadows } from '@react-three/drei';
<ContactShadows
position={[0, 0, 0]}
opacity={0.5}
scale={10}
blur={1.5}
far={1}
/>
```
**Limitations**: Works ONLY for ground-plane shadows. Does NOT project onto arbitrary geometry. Does NOT replace shadow maps for complex scenes.
---
## Performance Budget
| Shadow Type | Cost per Frame | Max Recommended |
|-------------|---------------|-----------------|
| DirectionalLight shadow | 1 render pass | 1-2 lights |
| SpotLight shadow | 1 render pass | 2-4 lights |
| PointLight shadow | 6 render passes | 1 light max |
**Shadow map resolution impact**: Doubling `mapSize` quadruples GPU memory usage. NEVER exceed 4096x4096 on mobile; prefer 2048x2048 or lower.
**ALWAYS** profile with `renderer.info.render.calls` to verify shadow pass count does not exceed your frame budget.
---
## Reference Links
- [references/methods.md](references/methods.md) -- LightShadow API signatures and shadow map type constants
- [references/examples.md](references/examples.md) -- Complete shadow setup examples for each light type
- [references/anti-patterns.md](references/anti-patterns.md) -- Shadow artifacts, causes, and fixes
### Official Sources
- https://threejs.org/docs/#api/en/lights/shadows/LightShadow
- https://threejs.org/docs/#api/en/lights/shadows/DirectionalLightShadow
- https://threejs.org/docs/#api/en/lights/shadows/SpotLightShadow
- https://threejs.org/docs/#api/en/lights/shadows/PointLightShadow
- https://threejs.org/docs/#api/en/renderers/WebGLRenderer (shadowMap property)
- https://threejs.org/docs/#api/en/helpers/CameraHelperMore General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

