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'