
Threejs Impl Shadows
- 20 installs
- 11 repo stars
- Updated July 8, 2026
- openaec-foundation/three.js-claude-skill-package
Helps with ai & agent building tasks.
About
threejs-impl-shadows is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- threejs-impl-shadows
- AI & Agent Building
- AI-coding skill
Threejs Impl Shadows by the numbers
- 20 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #10,459 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/three.js-claude-skill-package --skill threejs-impl-shadowsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 20 |
|---|---|
| repo stars | ★ 11 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/three.js-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
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.
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 shadowsShadow 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.radiusproperty - VSMShadowMap: supports
shadow.radiusandshadow.blurSamplesbut 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.
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 targetFrustum 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.
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, default1) -- 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.
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:
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:
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:
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true; // render shadows onceSet renderer.shadowMap.needsUpdate = true ONLY when something changes. This eliminates per-frame shadow map rendering entirely for static scenes.
Per-light control:
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:
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 -- LightShadow API signatures and shadow map type constants
- references/examples.md -- Complete shadow setup examples for each light type
- 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/CameraHelper
threejs-impl-shadows -- Anti-Patterns
Anti-Pattern 1: Missing Shadow Opt-In Step
Wrong: Enabling shadows on the light but forgetting the renderer or mesh flags.
// BAD -- shadows silently fail with no error
const light = new THREE.DirectionalLight(0xffffff, 1);
light.castShadow = true;
scene.add(light);
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// No renderer.shadowMap.enabled
// No cube.castShadow
// No ground.receiveShadow
// Result: zero shadows, zero errorsCorrect: ALWAYS set all three levels -- renderer, light, and mesh.
renderer.shadowMap.enabled = true;
light.castShadow = true;
cube.castShadow = true;
ground.receiveShadow = true;Why: Three.js shadows use a three-level opt-in system. Each level independently gates shadow behavior. Missing any one level silently produces no shadows.
---
Anti-Pattern 2: Default DirectionalLight Shadow Camera Frustum
Wrong: Leaving the shadow camera frustum at default values.
// BAD -- default frustum is tiny (-5 to 5)
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.castShadow = true;
scene.add(dirLight);
// Shadow camera frustum: left=-5, right=5, top=5, bottom=-5
// Result: most of the scene has no shadows or extremely pixelated shadowsCorrect: ALWAYS size the frustum to match your scene.
dirLight.shadow.camera.left = -30;
dirLight.shadow.camera.right = 30;
dirLight.shadow.camera.top = 30;
dirLight.shadow.camera.bottom = -30;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 100;Why: The default orthographic frustum is -5 to 5 on each axis. For any scene larger than a 10-unit cube, shadows are either clipped or spread across too few texels. ALWAYS use CameraHelper to verify.
---
Anti-Pattern 3: Excessive PointLight Shadow Usage
Wrong: Multiple shadow-casting PointLights in a scene.
// BAD -- 3 PointLight shadows = 18 shadow render passes per frame
for (let i = 0; i < 3; i++) {
const pl = new THREE.PointLight(0xffffff, 1, 50);
pl.castShadow = true;
pl.shadow.mapSize.set(1024, 1024);
scene.add(pl);
}
// Result: 18 shadow maps per frame (6 per PointLight), severe frame dropsCorrect: Replace PointLights with SpotLights where possible.
// GOOD -- SpotLight costs 1 render pass per light
const spotLight = new THREE.SpotLight(0xffffff, 1, 50, Math.PI / 3);
spotLight.castShadow = true;
scene.add(spotLight);Why: Each PointLight shadow renders a 6-face cubemap. Three shadow-casting PointLights at 1024x1024 means 18 render passes per frame. NEVER use more than 1 shadow-casting PointLight; prefer SpotLights at 1/6th the cost.
---
Anti-Pattern 4: Over-Correcting Bias (Peter Panning)
Wrong: Setting shadow bias too high to fix acne, causing shadows to detach.
// BAD -- excessive bias causes peter panning
dirLight.shadow.bias = -0.05; // way too large
// Result: shadows visibly float away from objectsCorrect: Use minimal bias combined with normalBias.
dirLight.shadow.bias = -0.0001; // small depth bias
dirLight.shadow.normalBias = 0.02; // normal-based offset for curved surfacesWhy: bias shifts the entire shadow depth comparison. Large values push shadows away from surfaces entirely. normalBias offsets along the surface normal, which fixes acne on curved geometry without causing peter panning. ALWAYS try normalBias before increasing bias.
---
Anti-Pattern 5: Non-Power-of-Two Shadow Map Size
Wrong: Using arbitrary shadow map dimensions.
// BAD -- non-power-of-two dimensions
dirLight.shadow.mapSize.width = 1000;
dirLight.shadow.mapSize.height = 1000;
// Result: GPU may pad to next power-of-two anyway, wasting memory and causing artifactsCorrect: ALWAYS use power-of-two values.
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;Why: WebGL texture hardware is optimized for power-of-two dimensions (256, 512, 1024, 2048, 4096). Non-power-of-two sizes may be silently padded, causing wasted memory and potential filtering artifacts.
---
Anti-Pattern 6: Forgetting to Add Light Target to Scene
Wrong: Moving a DirectionalLight target without adding it to the scene.
// BAD -- target position change has no effect
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(10, 10, 10);
dirLight.target.position.set(5, 0, 5); // target NOT in scene
scene.add(dirLight);
// Result: light still points at (0, 0, 0), shadow frustum is misalignedCorrect: ALWAYS add the target to the scene.
scene.add(dirLight);
scene.add(dirLight.target);
dirLight.target.position.set(5, 0, 5); // NOW takes effectWhy: Object3D.position updates only take effect when the object is part of the scene graph so its world matrix gets computed. Without scene.add(dirLight.target), the target's world position is never updated and the light direction remains default.
---
Anti-Pattern 7: Shadow on Transparent Objects Without customDepthMaterial
Wrong: Expecting transparent or alpha-tested materials to cast correct shadows automatically.
// BAD -- shadow ignores alpha channel entirely
const leafMaterial = new THREE.MeshStandardMaterial({
map: leafTexture,
alphaMap: alphaTexture,
alphaTest: 0.5,
transparent: true,
});
leafMesh.material = leafMaterial;
leafMesh.castShadow = true;
// Result: shadow is a solid rectangle ignoring the alpha cutoutCorrect: Assign a matching customDepthMaterial.
leafMesh.customDepthMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.RGBADepthPacking,
map: leafTexture,
alphaMap: alphaTexture,
alphaTest: 0.5,
});Why: The shadow depth pass uses a separate material (default MeshDepthMaterial) that knows nothing about your visible material's alpha settings. You MUST provide a customDepthMaterial with matching alphaMap and alphaTest values.
---
Anti-Pattern 8: Changing Shadow Map Type at Runtime
Wrong: Switching renderer.shadowMap.type after shadows have been created.
// BAD -- switching type after initial render
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// ... render a few frames ...
renderer.shadowMap.type = THREE.VSMShadowMap;
// Result: corrupted shadow maps, visual glitchesCorrect: Set shadow map type BEFORE any rendering occurs, or dispose all shadow maps first.
// Set BEFORE first render
renderer.shadowMap.type = THREE.VSMShadowMap;
renderer.shadowMap.enabled = true;Why: Shadow map textures are allocated with internal format and filtering settings specific to the shadow map type. Changing the type does not re-allocate existing shadow maps. ALWAYS set the type before the first render or manually call .dispose() on all light shadows before switching.
---
Anti-Pattern 9: Shadow Map Resolution Too Large on Mobile
Wrong: Using desktop-grade shadow maps on mobile devices.
// BAD -- 4096x4096 on mobile kills performance
dirLight.shadow.mapSize.width = 4096;
dirLight.shadow.mapSize.height = 4096;
// Result: GPU memory exhaustion, frame rate collapse on mobileCorrect: Scale shadow map size to device capability.
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
const shadowSize = isMobile ? 1024 : 2048;
dirLight.shadow.mapSize.width = shadowSize;
dirLight.shadow.mapSize.height = shadowSize;Why: A 4096x4096 shadow map consumes 64MB of GPU memory (single channel, 32-bit float). Mobile GPUs have limited VRAM and fill rate. NEVER exceed 2048x2048 on mobile; prefer 1024x1024.
---
Anti-Pattern 10: Not Using Static Shadow Optimization
Wrong: Re-rendering shadow maps every frame when nothing moves.
// BAD -- shadow maps re-render every frame even though scene is static
renderer.shadowMap.enabled = true;
// autoUpdate defaults to true -- shadow maps rebuild every frameCorrect: Disable auto-update for static scenes.
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true; // render once
// Later, if something moves:
function onSceneChanged() {
renderer.shadowMap.needsUpdate = true;
}Why: Shadow map rendering is one of the most expensive operations in a Three.js scene. For static scenes (architectural visualization, product displays), shadow maps NEVER change. Disabling autoUpdate eliminates per-frame shadow passes entirely.
threejs-impl-shadows -- Examples
Example 1: Basic DirectionalLight Shadow Setup
The most common shadow scenario: a single directional light casting shadows onto a ground plane.
import * as THREE from 'three';
// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// Step 1: Enable shadows on the renderer
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Step 2: Create and configure light with shadows
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7.5);
dirLight.castShadow = true;
// Configure shadow map resolution
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
// Configure orthographic frustum -- sized to scene
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
dirLight.shadow.camera.top = 10;
dirLight.shadow.camera.bottom = -10;
// Bias tuning
dirLight.shadow.bias = -0.0001;
dirLight.shadow.normalBias = 0.02;
scene.add(dirLight);
scene.add(dirLight.target);
// Step 3: Create objects with shadow flags
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0x00ff00 })
);
cube.position.y = 0.5;
cube.castShadow = true;
scene.add(cube);
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshStandardMaterial({ color: 0x808080 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Add ambient light for fill
scene.add(new THREE.AmbientLight(0x404040, 0.5));
// Debug helper -- REMOVE in production
const shadowHelper = new THREE.CameraHelper(dirLight.shadow.camera);
scene.add(shadowHelper);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();---
Example 2: SpotLight Shadow with Soft Penumbra
SpotLight shadows auto-configure the perspective frustum from the light's cone angle.
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
// SpotLight with shadows
const spotLight = new THREE.SpotLight(0xffffff, 50);
spotLight.position.set(0, 8, 4);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.3;
spotLight.decay = 2;
spotLight.distance = 30;
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.bias = -0.0002;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 30;
scene.add(spotLight);
scene.add(spotLight.target);
// Shadow-casting sphere
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 32, 32),
new THREE.MeshStandardMaterial({ color: 0xff4444 })
);
sphere.position.set(0, 1, 0);
sphere.castShadow = true;
scene.add(sphere);
// Ground plane receiving shadows
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(15, 15),
new THREE.MeshStandardMaterial({ color: 0xcccccc })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);---
Example 3: PointLight Shadow (Cubemap -- Expensive)
PointLight shadows render 6 cubemap faces. Use sparingly.
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
// PointLight -- use smaller mapSize to offset 6x cost
const pointLight = new THREE.PointLight(0xffaa33, 50, 20);
pointLight.position.set(0, 3, 0);
pointLight.castShadow = true;
pointLight.shadow.mapSize.width = 512;
pointLight.shadow.mapSize.height = 512;
pointLight.shadow.camera.near = 0.1;
pointLight.shadow.camera.far = 20;
pointLight.shadow.bias = -0.002;
scene.add(pointLight);
// Multiple objects casting shadows in all directions
const positions = [
[-2, 0.5, -2], [2, 0.5, -2], [-2, 0.5, 2], [2, 0.5, 2]
];
positions.forEach(([x, y, z]) => {
const box = new THREE.Mesh(
new THREE.BoxGeometry(0.8, 1, 0.8),
new THREE.MeshStandardMaterial({ color: 0x4488ff })
);
box.position.set(x, y, z);
box.castShadow = true;
box.receiveShadow = true;
scene.add(box);
});
// Room walls and floor receiving shadows
const floorGeo = new THREE.PlaneGeometry(10, 10);
const wallMat = new THREE.MeshStandardMaterial({ color: 0x999999, side: THREE.DoubleSide });
const floor = new THREE.Mesh(floorGeo, wallMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
const ceiling = new THREE.Mesh(floorGeo, wallMat);
ceiling.rotation.x = Math.PI / 2;
ceiling.position.y = 6;
ceiling.receiveShadow = true;
scene.add(ceiling);---
Example 4: Alpha-Tested Shadow (Tree Leaves)
Custom depth material enables correct shadows for alpha-tested geometry.
import * as THREE from 'three';
const loader = new THREE.TextureLoader();
const leafTexture = loader.load('leaf-diffuse.png');
const leafAlpha = loader.load('leaf-alpha.png');
// Visible material with alpha test
const leafMaterial = new THREE.MeshStandardMaterial({
map: leafTexture,
alphaMap: leafAlpha,
alphaTest: 0.5,
side: THREE.DoubleSide,
});
// Custom depth material -- MUST match alphaTest and alphaMap
const leafDepthMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.RGBADepthPacking,
map: leafTexture,
alphaMap: leafAlpha,
alphaTest: 0.5,
});
const leafMesh = new THREE.Mesh(
new THREE.PlaneGeometry(2, 2),
leafMaterial
);
leafMesh.castShadow = true;
leafMesh.customDepthMaterial = leafDepthMaterial;
scene.add(leafMesh);---
Example 5: Static Shadow Optimization
For scenes where lights and casters are stationary, render shadows once and stop.
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Disable automatic shadow updates
renderer.shadowMap.autoUpdate = false;
const scene = new THREE.Scene();
// ... set up lights, shadow casters, receivers ...
// Render shadows once after scene is fully loaded
renderer.shadowMap.needsUpdate = true;
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
// When something moves later, trigger a single shadow update:
function onObjectMoved() {
renderer.shadowMap.needsUpdate = true;
}---
Example 6: VSM Soft Shadows with Configurable Blur
VSM shadow maps support radius and blurSamples for artistic control.
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.VSMShadowMap;
const scene = new THREE.Scene();
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 5);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.left = -15;
dirLight.shadow.camera.right = 15;
dirLight.shadow.camera.top = 15;
dirLight.shadow.camera.bottom = -15;
// VSM-specific: blur radius and sample count
dirLight.shadow.radius = 4; // larger = softer
dirLight.shadow.blurSamples = 16; // more = smoother blur
scene.add(dirLight);Warning: VSM can produce light bleeding artifacts. If shadows leak through thin geometry, switch to PCFSoftShadowMap.
threejs-impl-shadows -- Methods Reference
LightShadow (Base Class)
All shadow-capable lights (DirectionalLight, SpotLight, PointLight) expose a .shadow property inheriting from LightShadow.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
.autoUpdate | boolean | true | If true, shadow map re-renders every frame |
.bias | number | 0 | Depth offset to reduce shadow acne; typical range -0.005 to 0.001 |
.normalBias | number | 0 | Offset along surface normal; reduces acne on curved surfaces |
.mapSize | Vector2 | (512, 512) | Shadow map resolution; MUST be power-of-two |
.radius | number | 1 | Blur radius for VSMShadowMap; IGNORED by PCF and PCFSoft |
.blurSamples | number | 8 | Number of blur samples; ONLY used by VSMShadowMap |
.intensity | number | 1 | Shadow darkness; 0 = invisible, 1 = fully opaque |
.map | `WebGLRenderTarget \ | null` | null |
.camera | Camera | varies | Virtual camera used to render the shadow map |
.needsUpdate | boolean | false | Set true to force re-render when autoUpdate is false |
Methods
| Method | Signature | Description |
|---|---|---|
.clone() | (): LightShadow | Returns a deep copy |
.copy(source) | (source: LightShadow): this | Copies properties from source |
.dispose() | (): void | Releases GPU resources (shadow map texture) |
.getFrustum() | (): Frustum | Returns the shadow camera frustum |
.updateMatrices(light) | (light: Light): void | Recalculates shadow camera matrices |
.toJSON() | (): Object | Serializes to JSON |
---
DirectionalLightShadow
Extends LightShadow. Uses an OrthographicCamera.
Shadow Camera Properties (OrthographicCamera)
| Property | Type | Default | Description |
|---|---|---|---|
.camera.left | number | -5 | Left frustum boundary |
.camera.right | number | 5 | Right frustum boundary |
.camera.top | number | 5 | Top frustum boundary |
.camera.bottom | number | -5 | Bottom frustum boundary |
.camera.near | number | 0.5 | Near clipping plane |
.camera.far | number | 500 | Far clipping plane |
Type Check
| Property | Value |
|---|---|
.isDirectionalLightShadow | true (readonly) |
---
SpotLightShadow
Extends LightShadow. Uses a PerspectiveCamera auto-configured from the SpotLight.
Additional Properties
| Property | Type | Default | Description |
|---|---|---|---|
.focus | number | 1 | Shadow camera FOV relative to spotlight FOV; range [0, 1] |
.aspect | number | 1 | Shadow texture aspect ratio |
Type Check
| Property | Value |
|---|---|
.isSpotLightShadow | true (readonly) |
Note: The shadow camera perspective is auto-calculated from spotLight.angle and spotLight.distance. Manual frustum configuration is RARELY needed.
---
PointLightShadow
Extends LightShadow. Uses 6 PerspectiveCamera instances (cubemap faces).
Type Check
| Property | Value |
|---|---|
.isPointLightShadow | true (readonly) |
Note: The shadow camera .near and .far can be configured. All 6 face cameras share the same near/far. FOV is fixed at 90 degrees per face.
---
Shadow Map Type Constants
| Constant | Value | Description |
|---|---|---|
THREE.BasicShadowMap | 0 | No filtering; hard aliased edges |
THREE.PCFShadowMap | 1 | Percentage-Closer Filtering; fixed kernel |
THREE.PCFSoftShadowMap | 2 | Variable kernel PCF; softer edges |
THREE.VSMShadowMap | 3 | Variance Shadow Maps; Gaussian blur |
Set via: renderer.shadowMap.type = THREE.PCFSoftShadowMap;
ALWAYS set the shadow map type BEFORE creating any shadow maps. Changing the type at runtime requires disposing all existing shadow maps.
---
WebGLRenderer Shadow Properties
| Property | Type | Default | Description |
|---|---|---|---|
renderer.shadowMap.enabled | boolean | false | Master shadow toggle |
renderer.shadowMap.autoUpdate | boolean | true | Auto re-render shadows each frame |
renderer.shadowMap.needsUpdate | boolean | false | Force single shadow update |
renderer.shadowMap.type | number | THREE.PCFShadowMap | Shadow map filtering algorithm |
---
Object3D Shadow Properties
| Property | Type | Default | Description |
|---|---|---|---|
.castShadow | boolean | false | Object casts shadows onto other objects |
.receiveShadow | boolean | false | Object receives shadows from other objects |
.customDepthMaterial | `Material \ | null` | null |
.customDistanceMaterial | `Material \ | null` | null |
---
CameraHelper
Visualizes any camera's frustum, including shadow cameras.
Constructor
new THREE.CameraHelper(camera: Camera)Properties
| Property | Type | Description |
|---|---|---|
.camera | Camera | The camera being visualized |
.pointMap | Object | Maps frustum point names to indices |
Methods
| Method | Signature | Description |
|---|---|---|
.update() | (): void | Refreshes the helper geometry; call after camera changes |
.dispose() | (): void | Releases resources |
Usage for Shadow Debugging
const helper = new THREE.CameraHelper(light.shadow.camera);
scene.add(helper);
// ALWAYS call update after modifying shadow camera properties
light.shadow.camera.left = -30;
light.shadow.camera.updateProjectionMatrix();
helper.update();