UNPKG

@react-three/drei

Version:

useful add-ons for react-three-fiber

311 lines (300 loc) 11.3 kB
import _extends from '@babel/runtime/helpers/esm/extends'; import * as THREE from 'three'; import * as React from 'react'; import { extend, useThree, useFrame } from '@react-three/fiber'; import { shaderMaterial } from './shaderMaterial.js'; import { DiscardMaterial } from '../materials/DiscardMaterial.js'; import { version } from '../helpers/constants.js'; function isLight(object) { return object.isLight; } function isGeometry(object) { return !!object.geometry; } const accumulativeContext = /* @__PURE__ */React.createContext(null); 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 <tonemapping_fragment> #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }`); const AccumulativeShadows = /* @__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 }, forwardRef) => { 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 /*#__PURE__*/React.createElement("group", props, /*#__PURE__*/React.createElement("group", { traverse: () => null, ref: gLights }, /*#__PURE__*/React.createElement(accumulativeContext.Provider, { value: api }, children)), /*#__PURE__*/React.createElement("mesh", { receiveShadow: true, ref: gPlane, scale: scale, rotation: [-Math.PI / 2, 0, 0] }, /*#__PURE__*/React.createElement("planeGeometry", null), /*#__PURE__*/React.createElement("softShadowMaterial", { transparent: true, depthWrite: false, toneMapped: toneMapped, color: color, blend: colorBlend, map: plm.progressiveLightMap2.texture }))); }); const RandomizedLight = /* @__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 }, forwardRef) => { const gLights = React.useRef(null); const length = new THREE.Vector3(...position).length(); const parent = React.useContext(accumulativeContext); const update = React.useCallback(() => { let light; 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 = React.useMemo(() => ({ update }), [update]); React.useImperativeHandle(forwardRef, () => api, [api]); React.useLayoutEffect(() => { var _parent$lights; const group = gLights.current; if (parent) (_parent$lights = parent.lights) == null || _parent$lights.set(group.uuid, api); return () => { var _parent$lights2; return void (parent == null || (_parent$lights2 = parent.lights) == null ? void 0 : _parent$lights2.delete(group.uuid)); }; }, [parent, api]); return /*#__PURE__*/React.createElement("group", _extends({ ref: gLights }, props), Array.from({ length: amount }, (_, index) => /*#__PURE__*/React.createElement("directionalLight", { key: index, castShadow: castShadow, "shadow-bias": bias, "shadow-mapSize": [mapSize, mapSize], intensity: intensity / amount }, /*#__PURE__*/React.createElement("orthographicCamera", { attach: "shadow-camera", args: [-size, size, size, -size, near, far] })))); }); // Based on "Progressive Light Map Accumulator", by [zalo](https://github.com/zalo/) class ProgressiveLightMap { constructor(renderer, scene, res = 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; } } export { AccumulativeShadows, RandomizedLight, accumulativeContext };