ShadowMesh
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
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
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 = someNewGeometry
shadowMesh.geometry = mesh.geometry
This 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
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
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
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
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 offset
const 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 direction
mesh.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.