threlte logo
@threlte/extras

<SoftShadows>

<SoftShadows> replaces Three.js’s default hard shadows with Percentage-Closer Soft Shadows (PCSS): shadows that are sharp where they touch their caster and soften with distance. Mount it alongside the rest of your scene — adding or removing it after the first render recompiles every shadow-receiving material.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import { Checkbox, Pane, Slider } from 'svelte-tweakpane-ui'
  import Scene from './Scene.svelte'

  let enabled = $state(true)
  let size = $state(60)
  let focus = $state(0)
  let samples = $state(16)
</script>

<Pane
  title="SoftShadows"
  position="fixed"
>
  <Checkbox
    bind:value={enabled}
    label="enabled"
  />
  <Slider
    bind:value={size}
    label="size"
    min={1}
    max={100}
    step={1}
  />
  <Slider
    bind:value={focus}
    label="focus"
    min={0}
    max={2}
    step={0.01}
  />
  <Slider
    bind:value={samples}
    label="samples"
    min={1}
    max={32}
    step={1}
  />
</Pane>

<div>
  <Canvas>
    <Scene
      {enabled}
      {size}
      {focus}
      {samples}
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T } from '@threlte/core'
  import { Environment, OrbitControls, SoftShadows } from '@threlte/extras'
  import Suzanne from './Suzanne.svelte'

  interface Props {
    enabled: boolean
    size: number
    focus: number
    samples: number
  }

  let { enabled, size, focus, samples }: Props = $props()
</script>

<T.PerspectiveCamera
  makeDefault
  position={[0, 8, 15]}
  fov={36}
>
  <OrbitControls
    enableZoom={false}
    enableDamping
  />
</T.PerspectiveCamera>

<Suzanne />

<T.DirectionalLight
  position={[5, 8, 4]}
  castShadow
  shadow.mapSize.width={1024}
  shadow.mapSize.height={1024}
  shadow.bias={0.0001}
/>

{#if enabled}
  <SoftShadows
    {size}
    {focus}
    {samples}
  />
{/if}

<Environment url="/textures/equirectangular/hdr/mpumalanga_veld_puresky_1k.hdr" />
<!--
Auto-generated by: https://github.com/threlte/threlte/tree/main/packages/gltf
Command: npx @threlte/gltf@1.0.1 ./Suzanne.glb -t -s
-->
<script lang="ts">
  import { T, useTask } from '@threlte/core'
  import { Float, useGltf } from '@threlte/extras'
  import type * as THREE from 'three'

  type GLTFResult = {
    nodes: {
      Suzanne: THREE.Mesh
      Icosphere: THREE.Mesh
      Cylinder: THREE.Mesh
      Floor: THREE.Mesh
    }
    materials: {
      Mat: THREE.MeshStandardMaterial
    }
  }

  let rotation = $state(0)

  useTask((dt) => {
    rotation += dt
  })

  const gltf = useGltf<GLTFResult>('/models/Suzanne.glb')
</script>

<T.Group dispose={false}>
  {#await gltf then gltf}
    <Float
      floatIntensity={10}
      speed={2}
      floatingRange={[0.15, 0.4]}
    >
      <T.Mesh
        castShadow
        receiveShadow
        geometry={gltf.nodes.Suzanne.geometry}
        material={gltf.materials.Mat}
        rotation.x={-0.62}
        rotation.y={rotation}
      />
    </Float>

    <Float
      floatIntensity={8}
      seed={1}
      speed={3}
      floatingRange={[0.2, 0.6]}
      position={[2.2, 0, -0.5]}
    >
      <T.Mesh
        castShadow
        receiveShadow
        geometry={gltf.nodes.Icosphere.geometry}
        material={gltf.materials.Mat}
        rotation.x={-0.62}
        rotation.y={0.09 + rotation}
        rotation.z={1.4 + rotation / 2}
      />
    </Float>

    <Float
      floatIntensity={6}
      seed={2}
      speed={4}
      floatingRange={[0.2, 0.5]}
      position={[-2.4, 0, 0.2]}
    >
      <T.Mesh
        castShadow
        receiveShadow
        geometry={gltf.nodes.Cylinder.geometry}
        material={gltf.materials.Mat}
      />
    </Float>

    <T.Mesh
      receiveShadow
      geometry={gltf.nodes.Floor.geometry}
      material={gltf.materials.Mat}
      position={[0, -0.1, 0]}
    />
  {/await}
</T.Group>

Component Signature

Props

name
type
required
default
description

focus
number
no
0
Depth focus, use it to shift the focal point (where the shadow is the sharpest)

samples
number
no
10
Number of samples (more samples less noise but more expensive)

size
number
no
25
Size of the light source – the larger the softer the light