threlte logo
@threlte/extras

<ShadowAlpha>

This component is a port of drei’s <ShadowAlpha> component.

By default in Three.js, shadows are always fully opaque regardless of the material’s opacity or alpha map. <ShadowAlpha> fixes this by making a mesh’s shadow respect its material’s opacity and alphaMap.

Place it as a child of the mesh whose shadow you want to control:

<T.Mesh castShadow>
  <T.BoxGeometry />
  <T.MeshStandardMaterial
    transparent
    opacity={0.5}
  />
  <ShadowAlpha />
</T.Mesh>

The shadow will now be 50% transparent, matching the material’s opacity.

Alpha Maps

If the parent material has an alphaMap (e.g. a leaf texture with cutouts), the shadow will automatically match those cutouts:

<T.Mesh castShadow>
  <T.PlaneGeometry />
  <T.MeshStandardMaterial
    transparent
    map={leafTexture}
    alphaMap={leafAlpha}
  />
  <ShadowAlpha />
</T.Mesh>

You can override the alpha map or disable it:

<!-- Use a custom alpha map -->
<ShadowAlpha alphaMap={customTexture} />

<!-- Disable alpha map, only use opacity -->
<ShadowAlpha alphaMap={false} />

How It Works

<ShadowAlpha> uses Bayer dithering in custom depth and distance materials. Fragments below the opacity threshold are discarded from the shadow map, creating a pattern that approximates transparency. This is a screen-space technique, so the dither pattern may be visible at close range.

This component is a workaround for a long-standing Three.js limitation. It will be deprecated when Three.js adds native support for shadows from transparent objects.

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

  let shadowOpacity = $state(0.5)
  let meshOpacity = $state(0.5)
  let overrideOpacity = $state(false)
</script>

<Pane
  title="ShadowAlpha"
  position="fixed"
>
  <Slider
    bind:value={meshOpacity}
    label="material opacity"
    min={0}
    max={1}
    step={0.01}
  />
  <Checkbox
    bind:value={overrideOpacity}
    label="override shadow opacity"
  />
  <Slider
    bind:value={shadowOpacity}
    label="shadow opacity"
    min={0}
    max={1}
    step={0.01}
    disabled={!overrideOpacity}
  />
</Pane>

<div>
  <Canvas>
    <Scene
      {meshOpacity}
      shadowOpacity={overrideOpacity ? shadowOpacity : undefined}
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T } from '@threlte/core'
  import { Float, OrbitControls, ShadowAlpha } from '@threlte/extras'

  interface Props {
    meshOpacity: number
    shadowOpacity: number | undefined
  }

  let { meshOpacity, shadowOpacity }: Props = $props()
</script>

<T.PerspectiveCamera
  makeDefault
  position={[4, 4, 4]}
  fov={35}
>
  <OrbitControls
    autoRotate
    autoRotateSpeed={0.5}
    enableDamping
    target.y={0.8}
  />
</T.PerspectiveCamera>

<T.DirectionalLight
  castShadow
  intensity={2}
  position={[3, 6, 3]}
  shadow.mapSize.width={1024}
  shadow.mapSize.height={1024}
  shadow.camera.left={-4}
  shadow.camera.right={4}
  shadow.camera.top={4}
  shadow.camera.bottom={-4}
/>
<T.AmbientLight intensity={0.4} />

<!-- Ground -->
<T.Mesh
  receiveShadow
  rotation.x={-Math.PI / 2}
>
  <T.PlaneGeometry args={[10, 10]} />
  <T.MeshStandardMaterial color="#f0ebe3" />
</T.Mesh>

<!-- Floating torus knot -->
<Float
  floatIntensity={0.5}
  floatingRange={[0, 0.3]}
>
  <T.Mesh
    castShadow
    position.y={1.2}
    rotation={[0.4, 0.6, 0]}
  >
    <T.TorusKnotGeometry args={[0.6, 0.2, 128, 32]} />
    <T.MeshStandardMaterial
      color="#6c5ce7"
      transparent
      opacity={meshOpacity}
    />
    <ShadowAlpha opacity={shadowOpacity} />
  </T.Mesh>
</Float>

Component Signature

Props

name
type
required
description

alphaMap
THREE.Texture | false
no
Alpha map for the shadow. Defaults to parent material alphaMap. Pass false to disable.

opacity
number
no
Shadow opacity. Defaults to parent material opacity.