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