Shadow Mesh
ShadowMesh is a threejs addon that is used as very performant alternative to shadow mapping. It uses the renderer’s stencil buffer and a locally computed matrix.
<script lang="ts"> import Scene from './Scene.svelte' import { Canvas } from '@threlte/core' import { Pane, Slider } from 'svelte-tweakpane-ui' import { WebGLRenderer } from 'three'
let w = $state(0.01)</script>
<Canvas createRenderer={(canvas) => { return new WebGLRenderer({ antialias: true, canvas, stencil: true }) }}> <Scene {w} /></Canvas>
<Pane title="shadow mesh" position="fixed"> <Slider bind:value={w} label="light position.w" min={0.1} max={0.9} /></Pane><script lang="ts"> import { DirectionalLight, Mesh, MeshNormalMaterial, PerspectiveCamera, Plane, TorusKnotGeometry, Vector3, Vector4 } from 'three' import { ShadowMesh } from 'three/examples/jsm/objects/ShadowMesh.js' import { T, useTask } from '@threlte/core' import { OrbitControls } from '@threlte/extras'
const planeY = 0 const planeOffset = 0.01 const planeConstant = planeY + planeOffset const yHat = new Vector3(0, 1, 0) const plane = new Plane(yHat, planeConstant)
let { w = 0.01 } = $props()
const mesh = new Mesh(new TorusKnotGeometry(), new MeshNormalMaterial()) mesh.translateY(2)
const translationAxis = new Vector3()
// only used to create a DirectionalLightHelper // notice that it's not added to the scene at the bottom but the shadow is still visible const light = new DirectionalLight() light.translateOnAxis(translationAxis.set(1, 1, -1).normalize(), 5) const lightPosition4D = new Vector4(...light.position)
$effect(() => { lightPosition4D.w = w })
const shadowMesh = new ShadowMesh(mesh)
const floor = new Mesh() const floorSize = 15 floor.lookAt(plane.normal)
const camera = new PerspectiveCamera() camera.translateOnAxis(translationAxis.set(1, 1, 1).normalize(), 20) camera.lookAt(floor.position)
useTask((dt) => { mesh.rotateY(dt) shadowMesh.update(plane, lightPosition4D) })</script>
<T is={camera} makeDefault> <OrbitControls enableDamping maxPolarAngle={(2 / 5) * Math.PI} /></T>
<T is={floor}> <T.PlaneGeometry args={[floorSize, floorSize]} /> <T.MeshBasicMaterial color="#ccccaa" /></T>
<T is={mesh} />
<T is={shadowMesh} />
<T is={light} attach={false} target={mesh}/>
<T.DirectionalLightHelper args={[light]} />Limitations
Section titled “Limitations”ShadowMesh has a few limitations, notably
- shadows must be cast onto a flat plane
- it does not support soft shadows
- you must use the renderer’s stencil buffer
Updating Mesh Geometry
Section titled “Updating Mesh Geometry”If you update the shadow-casting mesh to a new instance, you must create an entirely new ShadowMesh instance or assign the shadow mesh’s geometry to the new geometry.
// mesh.geometry = someNewGeometryshadowMesh.geometry = mesh.geometryThis is because the geometry of the shadow mesh is pulled from the mesh passed into its constructor at instantiation and does not update if the reference mesh’s geometry is updated.
Enabling the Stencil Buffer
Section titled “Enabling the Stencil Buffer”When using the ShadowMesh, you must enable the stencil buffer of the renderer. This can be done with the createRenderer prop of the <Canvas/> component
<Canvas createRenderer={(canvas) => { return new WebGLRenderer({ antialias: true, canvas, stencil: true }) }}> <Scene /></Canvas>Light Source
Section titled “Light Source”Because light sources are assumed to be pointed towards the plane, creating a light instance is unnecessary (unless you’re using materials that require lighting). Instead, all you need is the position of the light source.
const lightPosition4D = new Vector4(5, 5, 5, 0.1)
// later
shadowMesh.update(plane, lightPosition4D)W Component
Section titled “W Component”The w component of the light position vector controls the spread of the shadow. Values closer to 0 are suited for directional lights whereas values closer to 1 are for point lights.
Offsetting and Orienting the Plane
Section titled “Offsetting and Orienting the Plane”The plane should be offset from the ground or floor by a small amount. This is done to avoid z-fighting. An easy way to do this is to create the plane first and then orient the floor mesh to look in the direction of the plane’s normal.
const yHat = new Vector3(0, 1, 0)
// make a plane that faces straight up with a slight offsetconst plane = new Plane(yHat, 0.01)const planeGeometry = new PlaneGeometry()
const mesh = new Mesh(planeGeometry)
// orient the mesh to face in the plane's normal directionmesh.lookAt(plane.normal)Unfortunately, there’s not an easy way to get the normal of a plane geometry and involves accessing the geometry’s buffer attributes.