import * as THREE from 'three' import * as React from 'react' import { extend, ReactThreeFiber, useFrame, useThree } from '@react-three/fiber' import { shaderMaterial } from './shaderMaterial' import { DiscardMaterial } from '../materials/DiscardMaterial' import { ForwardRefComponent } from '../helpers/ts-utils' import { version } from '../helpers/constants' function isLight(object: any): object is THREE.Light { return object.isLight } function isGeometry(object: any): object is THREE.Mesh { return !!object.geometry } export type AccumulativeShadowsProps = { /** How many frames it can render, more yields cleaner results but takes more time, 40 */ frames?: number /** If frames === Infinity blend controls the refresh ratio, 100 */ blend?: number /** Can limit the amount of frames rendered if frames === Infinity, usually to get some performance back once a movable scene has settled, Infinity */ limit?: number /** Scale of the plane, */ scale?: number /** Temporal accumulates shadows over time which is more performant but has a visual regression over instant results, false */ temporal?: boolean /** Opacity of the plane, 1 */ opacity?: number /** Discards alpha pixels, 0.65 */ alphaTest?: number /** Shadow color, black */ color?: string /** Colorblend, how much colors turn to black, 0 is black, 2 */ colorBlend?: number /** Buffer resolution, 1024 */ resolution?: number /** Texture tonemapping */ toneMapped?: boolean } interface AccumulativeContext { lights: Map temporal: boolean frames: number blend: number count: number /** Returns the plane geometry onto which the shadow is cast */ getMesh: () => THREE.Mesh /** Resets the buffers, starting from scratch */ reset: () => void /** Updates the lightmap for a number of frames accumulartively */ update: (frames?: number) => void } interface AccumulativeLightContext { /** Jiggles the lights */ update: () => void } type SoftShadowMaterialProps = { map: THREE.Texture color?: ReactThreeFiber.Color alphaTest?: number blend?: number } declare global { namespace JSX { interface IntrinsicElements { softShadowMaterial: JSX.IntrinsicElements['shaderMaterial'] & SoftShadowMaterialProps } } } export const accumulativeContext = /* @__PURE__ */ React.createContext( null as unknown as AccumulativeContext ) const SoftShadowMaterial = /* @__PURE__ */ shaderMaterial( { color: /* @__PURE__ */ new THREE.Color(), blend: 2.0, alphaTest: 0.75, opacity: 0, map: null, }, `varying vec2 vUv; void main() { gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.); vUv = uv; }`, `varying vec2 vUv; uniform sampler2D map; uniform vec3 color; uniform float opacity; uniform float alphaTest; uniform float blend; void main() { vec4 sampledDiffuseColor = texture2D(map, vUv); gl_FragColor = vec4(color * sampledDiffuseColor.r * blend, max(0.0, (1.0 - (sampledDiffuseColor.r + sampledDiffuseColor.g + sampledDiffuseColor.b) / alphaTest)) * opacity); #include #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }` ) export const AccumulativeShadows: ForwardRefComponent< JSX.IntrinsicElements['group'] & AccumulativeShadowsProps, AccumulativeContext > = /* @__PURE__ */ React.forwardRef( ( { children, temporal, frames = 40, limit = Infinity, blend = 20, scale = 10, opacity = 1, alphaTest = 0.75, color = 'black', colorBlend = 2, resolution = 1024, toneMapped = true, ...props }: JSX.IntrinsicElements['group'] & AccumulativeShadowsProps, forwardRef: React.ForwardedRef ) => { extend({ SoftShadowMaterial }) const gl = useThree((state) => state.gl) const scene = useThree((state) => state.scene) const camera = useThree((state) => state.camera) const invalidate = useThree((state) => state.invalidate) const gPlane = React.useRef>(null!) const gLights = React.useRef(null!) const [plm] = React.useState(() => new ProgressiveLightMap(gl, scene, resolution)) React.useLayoutEffect(() => { plm.configure(gPlane.current) }, []) const api = React.useMemo( () => ({ lights: new Map(), temporal: !!temporal, frames: Math.max(2, frames), blend: Math.max(2, frames === Infinity ? blend : frames), count: 0, getMesh: () => gPlane.current, reset: () => { // Clear buffers, reset opacities, set frame count to 0 plm.clear() const material = gPlane.current.material material.opacity = 0 material.alphaTest = 0 api.count = 0 }, update: (frames = 1) => { // Adapt the opacity-blend ratio to the number of frames const material = gPlane.current.material if (!api.temporal) { material.opacity = opacity material.alphaTest = alphaTest } else { material.opacity = Math.min(opacity, material.opacity + opacity / api.blend) material.alphaTest = Math.min(alphaTest, material.alphaTest + alphaTest / api.blend) } // Switch accumulative lights on gLights.current.visible = true // Collect scene lights and meshes plm.prepare() // Update the lightmap and the accumulative lights for (let i = 0; i < frames; i++) { api.lights.forEach((light) => light.update()) plm.update(camera, api.blend) } // Switch lights off gLights.current.visible = false // Restore lights and meshes plm.finish() }, }), [plm, camera, scene, temporal, frames, blend, opacity, alphaTest] ) React.useLayoutEffect(() => { // Reset internals, buffers, ... api.reset() // Update lightmap if (!api.temporal && api.frames !== Infinity) api.update(api.blend) }) // Expose api, allow children to set itself as the main light source React.useImperativeHandle(forwardRef, () => api, [api]) useFrame(() => { if ((api.temporal || api.frames === Infinity) && api.count < api.frames && api.count < limit) { invalidate() api.update() api.count++ } }) return ( null} ref={gLights}> {children} ) } ) export type RandomizedLightProps = { /** How many frames it will jiggle the lights, 1. * Frames is context aware, if a provider like AccumulativeShadows exists, frames will be taken from there! */ frames?: number /** Light position, [0, 0, 0] */ position?: [x: number, y: number, z: number] /** Radius of the jiggle, higher values make softer light, 5 */ radius?: number /** Amount of lights, 8 */ amount?: number /** Light intensity, 1 */ intensity?: number /** Ambient occlusion, lower values mean less AO, hight more, you can mix AO and directional light, 0.5 */ ambient?: number /** If the lights cast shadows, this is true by default */ castShadow?: boolean /** Default shadow bias, 0 */ bias?: number /** Default map size, 512 */ mapSize?: number /** Default size of the shadow camera, 10 */ size?: number /** Default shadow camera near, 0.5 */ near?: number /** Default shadow camera far, 500 */ far?: number } export const RandomizedLight: ForwardRefComponent< JSX.IntrinsicElements['group'] & RandomizedLightProps, AccumulativeLightContext > = /* @__PURE__ */ React.forwardRef( ( { castShadow = true, bias = 0.001, mapSize = 512, size = 5, near = 0.5, far = 500, frames = 1, position = [0, 0, 0], radius = 1, amount = 8, intensity = version >= 155 ? Math.PI : 1, ambient = 0.5, ...props }: JSX.IntrinsicElements['group'] & RandomizedLightProps, forwardRef: React.ForwardedRef ) => { const gLights = React.useRef(null!) const length = new THREE.Vector3(...position).length() const parent = React.useContext(accumulativeContext) const update = React.useCallback(() => { let light: THREE.Object3D | undefined if (gLights.current) { for (let l = 0; l < gLights.current.children.length; l++) { light = gLights.current.children[l] if (Math.random() > ambient) { light.position.set( position[0] + THREE.MathUtils.randFloatSpread(radius), position[1] + THREE.MathUtils.randFloatSpread(radius), position[2] + THREE.MathUtils.randFloatSpread(radius) ) } else { let lambda = Math.acos(2 * Math.random() - 1) - Math.PI / 2.0 let phi = 2 * Math.PI * Math.random() light.position.set( Math.cos(lambda) * Math.cos(phi) * length, Math.abs(Math.cos(lambda) * Math.sin(phi) * length), Math.sin(lambda) * length ) } } } }, [radius, ambient, length, ...position]) const api: AccumulativeLightContext = React.useMemo(() => ({ update }), [update]) React.useImperativeHandle(forwardRef, () => api, [api]) React.useLayoutEffect(() => { const group = gLights.current if (parent) parent.lights?.set(group.uuid, api) return () => void parent?.lights?.delete(group.uuid) }, [parent, api]) return ( {Array.from({ length: amount }, (_, index) => ( ))} ) } ) // Based on "Progressive Light Map Accumulator", by [zalo](https://github.com/zalo/) class ProgressiveLightMap { renderer: THREE.WebGLRenderer res: number scene: THREE.Scene object: THREE.Mesh | null buffer1Active: boolean progressiveLightMap1: THREE.WebGLRenderTarget progressiveLightMap2: THREE.WebGLRenderTarget discardMat: THREE.ShaderMaterial targetMat: THREE.MeshLambertMaterial previousShadowMap: { value: THREE.Texture } averagingWindow: { value: number } clearColor: THREE.Color clearAlpha: number lights: { object: THREE.Light; intensity: number }[] meshes: { object: THREE.Mesh; material: THREE.Material | THREE.Material[] }[] constructor(renderer: THREE.WebGLRenderer, scene: THREE.Scene, res: number = 1024) { this.renderer = renderer this.res = res this.scene = scene this.buffer1Active = false this.lights = [] this.meshes = [] this.object = null this.clearColor = new THREE.Color() this.clearAlpha = 0 // Create the Progressive LightMap Texture const textureParams = { type: THREE.HalfFloatType, magFilter: THREE.NearestFilter, minFilter: THREE.NearestFilter, } this.progressiveLightMap1 = new THREE.WebGLRenderTarget(this.res, this.res, textureParams) this.progressiveLightMap2 = new THREE.WebGLRenderTarget(this.res, this.res, textureParams) // Inject some spicy new logic into a standard phong material this.discardMat = new DiscardMaterial() this.targetMat = new THREE.MeshLambertMaterial({ fog: false }) this.previousShadowMap = { value: this.progressiveLightMap1.texture } this.averagingWindow = { value: 100 } this.targetMat.onBeforeCompile = (shader) => { // Vertex Shader: Set Vertex Positions to the Unwrapped UV Positions shader.vertexShader = 'varying vec2 vUv;\n' + shader.vertexShader.slice(0, -1) + 'vUv = uv; gl_Position = vec4((uv - 0.5) * 2.0, 1.0, 1.0); }' // Fragment Shader: Set Pixels to average in the Previous frame's Shadows const bodyStart = shader.fragmentShader.indexOf('void main() {') shader.fragmentShader = 'varying vec2 vUv;\n' + shader.fragmentShader.slice(0, bodyStart) + 'uniform sampler2D previousShadowMap;\n uniform float averagingWindow;\n' + shader.fragmentShader.slice(bodyStart - 1, -1) + `\nvec3 texelOld = texture2D(previousShadowMap, vUv).rgb; gl_FragColor.rgb = mix(texelOld, gl_FragColor.rgb, 1.0/ averagingWindow); }` // Set the Previous Frame's Texture Buffer and Averaging Window shader.uniforms.previousShadowMap = this.previousShadowMap shader.uniforms.averagingWindow = this.averagingWindow } } clear() { this.renderer.getClearColor(this.clearColor) this.clearAlpha = this.renderer.getClearAlpha() this.renderer.setClearColor('black', 1) this.renderer.setRenderTarget(this.progressiveLightMap1) this.renderer.clear() this.renderer.setRenderTarget(this.progressiveLightMap2) this.renderer.clear() this.renderer.setRenderTarget(null) this.renderer.setClearColor(this.clearColor, this.clearAlpha) this.lights = [] this.meshes = [] this.scene.traverse((object) => { if (isGeometry(object)) { this.meshes.push({ object, material: object.material }) } else if (isLight(object)) { this.lights.push({ object, intensity: object.intensity }) } }) } prepare() { this.lights.forEach((light) => (light.object.intensity = 0)) this.meshes.forEach((mesh) => (mesh.object.material = this.discardMat)) } finish() { this.lights.forEach((light) => (light.object.intensity = light.intensity)) this.meshes.forEach((mesh) => (mesh.object.material = mesh.material)) } configure(object) { this.object = object } update(camera, blendWindow = 100) { if (!this.object) return // Set each object's material to the UV Unwrapped Surface Mapping Version this.averagingWindow.value = blendWindow this.object.material = this.targetMat // Ping-pong two surface buffers for reading/writing const activeMap = this.buffer1Active ? this.progressiveLightMap1 : this.progressiveLightMap2 const inactiveMap = this.buffer1Active ? this.progressiveLightMap2 : this.progressiveLightMap1 // Render the object's surface maps const oldBg = this.scene.background this.scene.background = null this.renderer.setRenderTarget(activeMap) this.previousShadowMap.value = inactiveMap.texture this.buffer1Active = !this.buffer1Active this.renderer.render(this.scene, camera) this.renderer.setRenderTarget(null) this.scene.background = oldBg } }