threlte logo
@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

Scene.svelte
<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.

Setting 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

NameTypeDescription
cameraTHREE.CameraCamera which is currently used for rendering, defaults to the camera set via makeDefault.
parentTHREE.Object3DTHREE.Object3D where lights will be stored.
cascadesnumberNumber of shadow cascades.
maxFarnumberFrustum 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
shadowMapSizenumberResolution of shadow maps (one per cascade).
shadowBiasnumberServes the same purpose as THREE.LightShadow.bias, but most likely you will need to use much smaller values.
lightDirectionTHREE.Vector3Normalized THREE.Vector3. Should be set by the prop lightDirection
lightIntensitynumberSame as THREE.DirectionalLight.intensity. Should be set by the prop lightIntensity
lightNearnumberShadow camera frustum near plane.
lightFarnumberShadow camera frustum far plane.
lightMarginnumberDefines 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.

Scene.svelte
<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:

Scene.svelte
<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>

Component Signature

Props

name
type
required
default

args
CSMParameters
no

camera
THREE.Camera | undefined
no

configure
(csm: CSM) => void
no

enabled
boolean
no

lightColor
THREE.ColorRepresentation
no

lightDirection
THREE.Vector3Tuple
no
[1, -1, 1]

lightIntensity
number
no