@threlte/extras
<CSM>
The <CSM/>
component offers a streamlined convenience integration of the Cascaded Shadow Maps technique, sourced from the Three.js addon.
<script lang="ts">
import { Pane, Slider, Checkbox, Color, Point } from 'svelte-tweakpane-ui'
import { Canvas } from '@threlte/core'
import { CSM } from '@threlte/extras'
import Scene from './Scene.svelte'
let enabled = true
let lightDirection = { x: 1, y: -1, z: 1 }
let lightIntensity = Math.PI
let lightColor = '#fffceb'
</script>
<Pane
title="CSM"
position="fixed"
>
<Checkbox
label="CSM enabled"
bind:value={enabled}
/>
<!-- @TODO: breaks svelte 5 -->
<!-- <Point
bind:value={lightDirection}
label="lightDirection"
/> -->
<Slider
bind:value={lightIntensity}
label="lightIntensity"
min={0}
max={10}
/>
<Color
bind:value={lightColor}
label="lightColor"
/>
</Pane>
<div>
<Canvas>
<CSM
{enabled}
lightDirection={[lightDirection.x, lightDirection.y, lightDirection.z]}
{lightIntensity}
{lightColor}
>
<Scene />
</CSM>
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import { T } from '@threlte/core'
import { OrbitControls } from '@threlte/extras'
import { DoubleSide } from 'three'
import { DEG2RAD } from 'three/src/math/MathUtils.js'
</script>
<T.PerspectiveCamera
makeDefault
position={[45, 40, -45]}
fov={90}
>
<OrbitControls
autoRotate
autoRotateSpeed={0.1}
target.y={-10}
/>
</T.PerspectiveCamera>
<T.AmbientLight intensity={1} />
<T.Mesh
rotation.x={DEG2RAD * -90}
castShadow
receiveShadow
>
<T.PlaneGeometry args={[1000, 1000]} />
<T.MeshStandardMaterial color={'#fa992a'} />
</T.Mesh>
<T.Mesh>
<T.SphereGeometry args={[400]} />
<T.MeshBasicMaterial
color="#0057fa"
side={DoubleSide}
/>
</T.Mesh>
{#each { length: 120 } as _, x}
{@const distance = Math.abs(Math.sin(x)) * 50 + 10}
{@const height = Math.abs((30 - distance) / 2)}
{@const posX = distance * Math.cos(DEG2RAD * ((360 / 120) * x))}
{@const posY = distance * Math.sin(DEG2RAD * ((360 / 120) * x))}
<T.Mesh
castShadow
receiveShadow
position.x={posX}
position.y={height / 2}
position.z={posY}
>
<T.CapsuleGeometry args={[3, height, 12, 32]} />
<T.MeshStandardMaterial color={'#45c1ff'} />
</T.Mesh>
{/each}
Cascaded Shadow Maps (CSM) are a technique employed to render shadows with varying levels of detail based on their distance from the camera. CSMs utilize multiple shadow maps to produce higher resolution shadows closer to the camera and gradually decrease this resolution for shadows farther away. This method is especially beneficial when rendering sun-cast shadows over vast terrains, ensuring detailed and performance-optimized shadow renderings.
Usage
<script lang="ts">
import { CSM } from '@threlte/extras'
import { Vector3 } from 'three'
</script>
<CSM
enabled
args={{
lightDirection: new Vector3(1, -1, 1).normalize()
}}
>
<!-- Your scene goes here -->
</CSM>
For CSM to work, shadow maps must be enabled. Threlte does this by default. Do not disable shadowmaps by <Canvas shadows={false}>
.
Then, wrap all of the objects that you wish to use CSM on (typically your entire scene) with the <CSM>
component.
Internally, the <CSM>
component modifies the shader code of all materials inside the component.
Currently, support is limited only to THREE.MeshStandardMaterial
and THREE.MeshPhongMaterial
.
castShadow
property on any lights in a scene with <CSM>
enabled leads to crashes. CSM Parameters
The <CSM>
component exposes an optional args
prop which is used to instantiate the class CSM
. These arguments are not reactive and are only updated when CSM is enabled.
Parameters explained
Name | Type | Description |
---|---|---|
camera | THREE.Camera | Camera which is currently used for rendering, defaults to the camera set via makeDefault . |
parent | THREE.Object3D | THREE.Object3D where lights will be stored. |
cascades | number | Number of shadow cascades. |
maxFar | number | Frustum far plane distance (i.e. shadows are not visible farther this distance from camera). May be smaller than camera.far value. |
mode | "uniform" | "logarithmic" | "practical" | "custom" | Defines a split scheme (~ how the large frustum is splitted into smaller ones). Can be 'uniform' (linear), 'logarithmic' , 'practical' or 'custom' . For most cases 'practical' may be the best choice. Equations used for each scheme can be found in GPU Gems 3. Chapter 10. If mode is set to 'custom' , you’ll need to define your own customSplitsCallback |
shadowMapSize | number | Resolution of shadow maps (one per cascade). |
shadowBias | number | Serves the same purpose as THREE.LightShadow.bias , but most likely you will need to use much smaller values. |
lightDirection | THREE.Vector3 | Normalized THREE.Vector3 . Should be set by the prop lightDirection |
lightIntensity | number | Same as THREE.DirectionalLight.intensity . Should be set by the prop lightIntensity |
lightNear | number | Shadow camera frustum near plane. |
lightFar | number | Shadow camera frustum far plane. |
lightMargin | number | Defines how far the shadow camera is moved along the z-axis in cascade frustum space. The larger the value is, the more space LightShadow will be able to cover. Should be set to a higher values for larger scenes. |
customSplitsCallback | (cascades: number, cameraNear: number, cameraFar: number, breaks: number[]) => void; | A callback to compute custom cascade splits when mode is set to 'custom' . The callback should accept three number parameters: cascadeCount , nearDistance , farDistance and return an array of split distances ranging from 0 to 1, where 0 is equal to nearDistance and 1 is equal to farDistance . |
For implementation details, refer to the Three.js GitHub repository.
Custom configuration
The <CSM>
component offers a configuration callback, which is called when CSM is activated.
This callback facilitates advanced configurations, such as enabling the fade feature.
<CSM
configure={(csm) => {
// advanced CSM configuration can be handle here.
csm.fade = true
}}
>
<!-- Your scene goes here -->
</CSM>
With fade enabled, shadows subtly diminish close to maxFar
distance, presenting a more aesthetic
transition compared to an abrupt cut-off. It may be more visually pleasing, but it’s important to
consider potential performance implications. While typically minimal, the computational complexity
can escalate based on the number of cascades and lights present in the scene. Fade is disabled by
default.
Providing a fallback
You may want to use a regular <T.DirectionalLight>
when CSM is not enabled,
for instance if you are developing a game and want to support low-end devices by
disabling shadows altogehter. You can do this by using the enabled
prop and
the disabled
slot:
<CSM enabled={settings.shadows}>
<!-- Your scene goes here -->
<slot name="disabled">
<!-- Will be mounted if settings.shadows is false -->
<T.DirectionalLight castShadow={false} />
</slot>
</CSM>