diff --git a/.storybook/stories/ShadowAlpha.stories.tsx b/.storybook/stories/ShadowAlpha.stories.tsx
new file mode 100644
index 000000000..003259a73
--- /dev/null
+++ b/.storybook/stories/ShadowAlpha.stories.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react'
+
+import { Setup } from '../Setup'
+
+import { useFrame } from '@react-three/fiber'
+import { BufferGeometry, MeshStandardMaterial, type Mesh } from 'three'
+import { Icosahedron, Plane, ShadowAlpha } from '../../src'
+
+export default {
+ title: 'Misc/ShadowAlpha',
+ component: ShadowAlpha,
+ decorators: [(storyFn) => {storyFn()}],
+}
+
+function ShadowAlphaScene() {
+ const mesh = React.useRef>(null!)
+
+ useFrame(({ clock }) => {
+ const time = clock.elapsedTime
+ mesh.current.material.opacity = Math.sin(time * 2) * 0.5 + 0.5
+ })
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export const ShadowAlphaSt = () =>
+ShadowAlphaSt.storyName = 'Default'
diff --git a/README.md b/README.md
index ce13f34bb..74307e1ef 100644
--- a/README.md
+++ b/README.md
@@ -227,6 +227,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
useEnvironment
useMatcapTexture
useNormalTexture
+ ShadowAlpha
@@ -4767,3 +4768,21 @@ return (
...
)
```
+
+#### ShadowAlpha
+
+Makes an object's shadow respect its opacity and alphaMap.
+
+```jsx
+
+
+
+
+
+
+```
+
+> Note: This component uses Screendoor transparency using a dither pattern. This pattern is notacible when the camera gets close to the shadow.
diff --git a/src/core/ShadowAlpha.tsx b/src/core/ShadowAlpha.tsx
new file mode 100644
index 000000000..1c8365d8d
--- /dev/null
+++ b/src/core/ShadowAlpha.tsx
@@ -0,0 +1,122 @@
+/**
+ * Integration and compilation: Faraz Shaikh (https://twitter.com/CantBeFaraz)
+ *
+ * Based on:
+ * - https://gkjohnson.github.io/threejs-sandbox/screendoor-transparency/ by Garrett Johnson (https://github.com/gkjohnson)
+ *
+ * Note:
+ * - Must depreciate in favor of https://github.com/mrdoob/three.js/issues/10600 when it's ready.
+ */
+
+import { useFrame } from '@react-three/fiber'
+import * as React from 'react'
+import * as THREE from 'three'
+
+interface ShadowAlphaProps {
+ opacity?: number
+ alphaMap?: THREE.Texture | boolean
+}
+
+export function ShadowAlpha({ opacity, alphaMap }: ShadowAlphaProps) {
+ const depthMaterialRef = React.useRef(null!)
+ const distanceMaterialRef = React.useRef(null!)
+
+ const uShadowOpacity = React.useRef({
+ value: 1,
+ })
+
+ const uAlphaMap = React.useRef({
+ value: null,
+ })
+
+ const uHasAlphaMap = React.useRef({
+ value: false,
+ })
+
+ React.useLayoutEffect(() => {
+ depthMaterialRef.current.onBeforeCompile = distanceMaterialRef.current.onBeforeCompile = (shader) => {
+ // Need to get the "void main" line dynamically because the lines for
+ // MeshDistanceMaterial and MeshDepthMaterial are different 🤦♂️
+ const mainLineStart = shader.fragmentShader.indexOf('void main')
+ let mainLine = ''
+ let ch
+ let i = mainLineStart
+ while (ch !== '\n' && i < mainLineStart + 100) {
+ ch = shader.fragmentShader.charAt(i)
+ mainLine += ch
+ i++
+ }
+ mainLine = mainLine.trim()
+
+ shader.vertexShader = shader.vertexShader.replace(
+ 'void main() {',
+ `
+ varying vec2 custom_vUv;
+
+ void main() {
+ custom_vUv = uv;
+
+ `
+ )
+
+ shader.fragmentShader = shader.fragmentShader.replace(
+ mainLine,
+ `
+ uniform float uShadowOpacity;
+ uniform sampler2D uAlphaMap;
+ uniform bool uHasAlphaMap;
+
+ varying vec2 custom_vUv;
+
+ float bayerDither2x2( vec2 v ) {
+ return mod( 3.0 * v.y + 2.0 * v.x, 4.0 );
+ }
+
+ float bayerDither4x4( vec2 v ) {
+ vec2 P1 = mod( v, 2.0 );
+ vec2 P2 = mod( floor( 0.5 * v ), 2.0 );
+ return 4.0 * bayerDither2x2( P1 ) + bayerDither2x2( P2 );
+ }
+
+ void main() {
+ float alpha =
+ uHasAlphaMap ?
+ uShadowOpacity * texture2D(uAlphaMap, custom_vUv).x
+ : uShadowOpacity;
+
+ if( ( bayerDither4x4( floor( mod( gl_FragCoord.xy, 4.0 ) ) ) ) / 16.0 >= alpha ) discard;
+
+ `
+ )
+
+ shader.uniforms['uShadowOpacity'] = uShadowOpacity.current
+ shader.uniforms['uAlphaMap'] = uAlphaMap.current
+ shader.uniforms['uHasAlphaMap'] = uHasAlphaMap.current
+ }
+ }, [])
+
+ useFrame(() => {
+ const parent = (depthMaterialRef.current as any).__r3f?.parent
+ if (parent) {
+ const parentMainMaterial = parent.material
+ if (parentMainMaterial) {
+ uShadowOpacity.current.value = opacity ?? parentMainMaterial.opacity
+
+ if (alphaMap === false) {
+ uAlphaMap.current.value = null
+ uHasAlphaMap.current.value = false
+ } else {
+ uAlphaMap.current.value = alphaMap || parentMainMaterial.alphaMap
+ uHasAlphaMap.current.value = !!uAlphaMap.current.value
+ }
+ }
+ }
+ })
+
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/src/core/index.ts b/src/core/index.ts
index 9fb4cf16e..758467753 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -124,6 +124,7 @@ export * from './useEnvironment'
export * from './useMatcapTexture'
export * from './useNormalTexture'
export * from './Wireframe'
+export * from './ShadowAlpha'
// Performance
export * from './Points'