@react-three/drei
Version:
useful add-ons for react-three-fiber
389 lines (375 loc) • 16.8 kB
JavaScript
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 { useFBO } from './Fbo.js';
import { useHelper } from './Helper.js';
import { shaderMaterial } from './shaderMaterial.js';
import { Edges } from './Edges.js';
import { FullScreenQuad } from 'three-stdlib';
import { version } from '../helpers/constants.js';
function createNormalMaterial(side = THREE.FrontSide) {
const viewMatrix = {
value: new THREE.Matrix4()
};
return Object.assign(new THREE.MeshNormalMaterial({
side
}), {
viewMatrix,
onBeforeCompile: shader => {
shader.uniforms.viewMatrix = viewMatrix;
shader.fragmentShader = `vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {
return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );
}\n` + shader.fragmentShader.replace('#include <normal_fragment_maps>', `#include <normal_fragment_maps>
normal = inverseTransformDirection( normal, viewMatrix );\n`);
}
});
}
const CausticsProjectionMaterial = /* @__PURE__ */shaderMaterial({
causticsTexture: null,
causticsTextureB: null,
color: /* @__PURE__ */new THREE.Color(),
lightProjMatrix: /* @__PURE__ */new THREE.Matrix4(),
lightViewMatrix: /* @__PURE__ */new THREE.Matrix4()
}, `varying vec3 vWorldPosition;
void main() {
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.);
vec4 worldPosition = modelMatrix * vec4(position, 1.);
vWorldPosition = worldPosition.xyz;
}`, `varying vec3 vWorldPosition;
uniform vec3 color;
uniform sampler2D causticsTexture;
uniform sampler2D causticsTextureB;
uniform mat4 lightProjMatrix;
uniform mat4 lightViewMatrix;
void main() {
// Apply caustics
vec4 lightSpacePos = lightProjMatrix * lightViewMatrix * vec4(vWorldPosition, 1.0);
lightSpacePos.xyz /= lightSpacePos.w;
lightSpacePos.xyz = lightSpacePos.xyz * 0.5 + 0.5;
vec3 front = texture2D(causticsTexture, lightSpacePos.xy).rgb;
vec3 back = texture2D(causticsTextureB, lightSpacePos.xy).rgb;
gl_FragColor = vec4((front + back) * color, 1.0);
#include <tonemapping_fragment>
#include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
}`);
const CausticsMaterial = /* @__PURE__ */shaderMaterial({
cameraMatrixWorld: /* @__PURE__ */new THREE.Matrix4(),
cameraProjectionMatrixInv: /* @__PURE__ */new THREE.Matrix4(),
normalTexture: null,
depthTexture: null,
lightDir: /* @__PURE__ */new THREE.Vector3(0, 1, 0),
lightPlaneNormal: /* @__PURE__ */new THREE.Vector3(0, 1, 0),
lightPlaneConstant: 0,
near: 0.1,
far: 100,
modelMatrix: /* @__PURE__ */new THREE.Matrix4(),
worldRadius: 1 / 40,
ior: 1.1,
bounces: 0,
resolution: 1024,
size: 10,
intensity: 0.5
}, /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`, /* glsl */`
uniform mat4 cameraMatrixWorld;
uniform mat4 cameraProjectionMatrixInv;
uniform vec3 lightDir;
uniform vec3 lightPlaneNormal;
uniform float lightPlaneConstant;
uniform float near;
uniform float far;
uniform float time;
uniform float worldRadius;
uniform float resolution;
uniform float size;
uniform float intensity;
uniform float ior;
precision highp isampler2D;
precision highp usampler2D;
uniform sampler2D normalTexture;
uniform sampler2D depthTexture;
uniform float bounces;
varying vec2 vUv;
vec3 WorldPosFromDepth(float depth, vec2 coord) {
float z = depth * 2.0 - 1.0;
vec4 clipSpacePosition = vec4(coord * 2.0 - 1.0, z, 1.0);
vec4 viewSpacePosition = cameraProjectionMatrixInv * clipSpacePosition;
// Perspective division
viewSpacePosition /= viewSpacePosition.w;
vec4 worldSpacePosition = cameraMatrixWorld * viewSpacePosition;
return worldSpacePosition.xyz;
}
float sdPlane( vec3 p, vec3 n, float h ) {
// n must be normalized
return dot(p,n) + h;
}
float planeIntersect( vec3 ro, vec3 rd, vec4 p ) {
return -(dot(ro,p.xyz)+p.w)/dot(rd,p.xyz);
}
vec3 totalInternalReflection(vec3 ro, vec3 rd, vec3 pos, vec3 normal, float ior, out vec3 rayOrigin, out vec3 rayDirection) {
rayOrigin = ro;
rayDirection = rd;
rayDirection = refract(rayDirection, normal, 1.0 / ior);
rayOrigin = pos + rayDirection * 0.1;
return rayDirection;
}
void main() {
// Each sample consists of random offset in the x and y direction
float caustic = 0.0;
float causticTexelSize = (1.0 / resolution) * size * 2.0;
float texelsNeeded = worldRadius / causticTexelSize;
float sampleRadius = texelsNeeded / resolution;
float sum = 0.0;
if (texture2D(depthTexture, vUv).x == 1.0) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
vec2 offset1 = vec2(-0.5, -0.5);//vec2(rand() - 0.5, rand() - 0.5);
vec2 offset2 = vec2(-0.5, 0.5);//vec2(rand() - 0.5, rand() - 0.5);
vec2 offset3 = vec2(0.5, 0.5);//vec2(rand() - 0.5, rand() - 0.5);
vec2 offset4 = vec2(0.5, -0.5);//vec2(rand() - 0.5, rand() - 0.5);
vec2 uv1 = vUv + offset1 * sampleRadius;
vec2 uv2 = vUv + offset2 * sampleRadius;
vec2 uv3 = vUv + offset3 * sampleRadius;
vec2 uv4 = vUv + offset4 * sampleRadius;
vec3 normal1 = texture2D(normalTexture, uv1, -10.0).rgb * 2.0 - 1.0;
vec3 normal2 = texture2D(normalTexture, uv2, -10.0).rgb * 2.0 - 1.0;
vec3 normal3 = texture2D(normalTexture, uv3, -10.0).rgb * 2.0 - 1.0;
vec3 normal4 = texture2D(normalTexture, uv4, -10.0).rgb * 2.0 - 1.0;
float depth1 = texture2D(depthTexture, uv1, -10.0).x;
float depth2 = texture2D(depthTexture, uv2, -10.0).x;
float depth3 = texture2D(depthTexture, uv3, -10.0).x;
float depth4 = texture2D(depthTexture, uv4, -10.0).x;
// Sanity check the depths
if (depth1 == 1.0 || depth2 == 1.0 || depth3 == 1.0 || depth4 == 1.0) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
vec3 pos1 = WorldPosFromDepth(depth1, uv1);
vec3 pos2 = WorldPosFromDepth(depth2, uv2);
vec3 pos3 = WorldPosFromDepth(depth3, uv3);
vec3 pos4 = WorldPosFromDepth(depth4, uv4);
vec3 originPos1 = WorldPosFromDepth(0.0, uv1);
vec3 originPos2 = WorldPosFromDepth(0.0, uv2);
vec3 originPos3 = WorldPosFromDepth(0.0, uv3);
vec3 originPos4 = WorldPosFromDepth(0.0, uv4);
vec3 endPos1, endPos2, endPos3, endPos4;
vec3 endDir1, endDir2, endDir3, endDir4;
totalInternalReflection(originPos1, lightDir, pos1, normal1, ior, endPos1, endDir1);
totalInternalReflection(originPos2, lightDir, pos2, normal2, ior, endPos2, endDir2);
totalInternalReflection(originPos3, lightDir, pos3, normal3, ior, endPos3, endDir3);
totalInternalReflection(originPos4, lightDir, pos4, normal4, ior, endPos4, endDir4);
float lightPosArea = length(cross(originPos2 - originPos1, originPos3 - originPos1)) + length(cross(originPos3 - originPos1, originPos4 - originPos1));
float t1 = planeIntersect(endPos1, endDir1, vec4(lightPlaneNormal, lightPlaneConstant));
float t2 = planeIntersect(endPos2, endDir2, vec4(lightPlaneNormal, lightPlaneConstant));
float t3 = planeIntersect(endPos3, endDir3, vec4(lightPlaneNormal, lightPlaneConstant));
float t4 = planeIntersect(endPos4, endDir4, vec4(lightPlaneNormal, lightPlaneConstant));
vec3 finalPos1 = endPos1 + endDir1 * t1;
vec3 finalPos2 = endPos2 + endDir2 * t2;
vec3 finalPos3 = endPos3 + endDir3 * t3;
vec3 finalPos4 = endPos4 + endDir4 * t4;
float finalArea = length(cross(finalPos2 - finalPos1, finalPos3 - finalPos1)) + length(cross(finalPos3 - finalPos1, finalPos4 - finalPos1));
caustic += intensity * (lightPosArea / finalArea);
// Calculate the area of the triangle in light spaces
gl_FragColor = vec4(vec3(max(caustic, 0.0)), 1.0);
}`);
const NORMALPROPS = {
depth: true,
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
type: THREE.UnsignedByteType
};
const CAUSTICPROPS = {
minFilter: THREE.LinearMipmapLinearFilter,
magFilter: THREE.LinearFilter,
type: THREE.FloatType,
generateMipmaps: true
};
const Caustics = /* @__PURE__ */React.forwardRef(({
debug,
children,
frames = 1,
ior = 1.1,
color = 'white',
causticsOnly = false,
backside = false,
backsideIOR = 1.1,
worldRadius = 0.3125,
intensity = 0.05,
resolution = 2024,
lightSource = [5, 5, 5],
...props
}, fref) => {
extend({
CausticsProjectionMaterial
});
const ref = React.useRef(null);
const camera = React.useRef(null);
const scene = React.useRef(null);
const plane = React.useRef(null);
const gl = useThree(state => state.gl);
const helper = useHelper(debug && camera, THREE.CameraHelper);
// Buffers for front and back faces
const normalTarget = useFBO(resolution, resolution, NORMALPROPS);
const normalTargetB = useFBO(resolution, resolution, NORMALPROPS);
const causticsTarget = useFBO(resolution, resolution, CAUSTICPROPS);
const causticsTargetB = useFBO(resolution, resolution, CAUSTICPROPS);
// Normal materials for front and back faces
const [normalMat] = React.useState(() => createNormalMaterial());
const [normalMatB] = React.useState(() => createNormalMaterial(THREE.BackSide));
// The quad that catches the caustics
const [causticsMaterial] = React.useState(() => new CausticsMaterial());
const [causticsQuad] = React.useState(() => new FullScreenQuad(causticsMaterial));
React.useLayoutEffect(() => {
ref.current.updateWorldMatrix(false, true);
});
let count = 0;
const v = new THREE.Vector3();
const lpF = new THREE.Frustum();
const lpM = new THREE.Matrix4();
const lpP = new THREE.Plane();
const lightDir = new THREE.Vector3();
const lightDirInv = new THREE.Vector3();
const bounds = new THREE.Box3();
const focusPos = new THREE.Vector3();
const boundsVertices = [];
const worldVerts = [];
const projectedVerts = [];
const lightDirs = [];
const cameraPos = new THREE.Vector3();
for (let i = 0; i < 8; i++) {
boundsVertices.push(new THREE.Vector3());
worldVerts.push(new THREE.Vector3());
projectedVerts.push(new THREE.Vector3());
lightDirs.push(new THREE.Vector3());
}
useFrame(() => {
if (frames === Infinity || count++ < frames) {
var _scene$current$parent, _helper$current;
if (Array.isArray(lightSource)) lightDir.fromArray(lightSource).normalize();else lightDir.copy(ref.current.worldToLocal(lightSource.current.getWorldPosition(v)).normalize());
lightDirInv.copy(lightDir).multiplyScalar(-1);
(_scene$current$parent = scene.current.parent) == null || _scene$current$parent.matrixWorld.identity();
bounds.setFromObject(scene.current, true);
boundsVertices[0].set(bounds.min.x, bounds.min.y, bounds.min.z);
boundsVertices[1].set(bounds.min.x, bounds.min.y, bounds.max.z);
boundsVertices[2].set(bounds.min.x, bounds.max.y, bounds.min.z);
boundsVertices[3].set(bounds.min.x, bounds.max.y, bounds.max.z);
boundsVertices[4].set(bounds.max.x, bounds.min.y, bounds.min.z);
boundsVertices[5].set(bounds.max.x, bounds.min.y, bounds.max.z);
boundsVertices[6].set(bounds.max.x, bounds.max.y, bounds.min.z);
boundsVertices[7].set(bounds.max.x, bounds.max.y, bounds.max.z);
for (let i = 0; i < 8; i++) {
worldVerts[i].copy(boundsVertices[i]);
}
bounds.getCenter(focusPos);
boundsVertices.map(v => v.sub(focusPos));
const lightPlane = lpP.set(lightDirInv, 0);
boundsVertices.map((v, i) => lightPlane.projectPoint(v, projectedVerts[i]));
const centralVert = projectedVerts.reduce((a, b) => a.add(b), v.set(0, 0, 0)).divideScalar(projectedVerts.length);
const radius = projectedVerts.map(v => v.distanceTo(centralVert)).reduce((a, b) => Math.max(a, b));
const dirLength = boundsVertices.map(x => x.dot(lightDir)).reduce((a, b) => Math.max(a, b));
// Shadows
camera.current.position.copy(cameraPos.copy(lightDir).multiplyScalar(dirLength).add(focusPos));
camera.current.lookAt(scene.current.localToWorld(focusPos));
const dirMatrix = lpM.lookAt(camera.current.position, focusPos, v.set(0, 1, 0));
camera.current.left = -radius;
camera.current.right = radius;
camera.current.top = radius;
camera.current.bottom = -radius;
const yOffset = v.set(0, radius, 0).applyMatrix4(dirMatrix);
const yTime = (camera.current.position.y + yOffset.y) / lightDir.y;
camera.current.near = 0.1;
camera.current.far = yTime;
camera.current.updateProjectionMatrix();
camera.current.updateMatrixWorld();
// Now find size of ground plane
const groundProjectedCoords = worldVerts.map((v, i) => v.add(lightDirs[i].copy(lightDir).multiplyScalar(-v.y / lightDir.y)));
const centerPos = groundProjectedCoords.reduce((a, b) => a.add(b), v.set(0, 0, 0)).divideScalar(groundProjectedCoords.length);
const maxSize = 2 * groundProjectedCoords.map(v => Math.hypot(v.x - centerPos.x, v.z - centerPos.z)).reduce((a, b) => Math.max(a, b));
plane.current.scale.setScalar(maxSize);
plane.current.position.copy(centerPos);
if (debug) (_helper$current = helper.current) == null || _helper$current.update();
// Inject uniforms
normalMatB.viewMatrix.value = normalMat.viewMatrix.value = camera.current.matrixWorldInverse;
const dirLightNearPlane = lpF.setFromProjectionMatrix(lpM.multiplyMatrices(camera.current.projectionMatrix, camera.current.matrixWorldInverse)).planes[4];
causticsMaterial.cameraMatrixWorld = camera.current.matrixWorld;
causticsMaterial.cameraProjectionMatrixInv = camera.current.projectionMatrixInverse;
causticsMaterial.lightDir = lightDirInv;
causticsMaterial.lightPlaneNormal = dirLightNearPlane.normal;
causticsMaterial.lightPlaneConstant = dirLightNearPlane.constant;
causticsMaterial.near = camera.current.near;
causticsMaterial.far = camera.current.far;
causticsMaterial.resolution = resolution;
causticsMaterial.size = radius;
causticsMaterial.intensity = intensity;
causticsMaterial.worldRadius = worldRadius;
// Switch the scene on
scene.current.visible = true;
// Render front face normals
gl.setRenderTarget(normalTarget);
gl.clear();
scene.current.overrideMaterial = normalMat;
gl.render(scene.current, camera.current);
// Render back face normals, if enabled
gl.setRenderTarget(normalTargetB);
gl.clear();
if (backside) {
scene.current.overrideMaterial = normalMatB;
gl.render(scene.current, camera.current);
}
// Remove the override material
scene.current.overrideMaterial = null;
// Render front face caustics
causticsMaterial.ior = ior;
plane.current.material.lightProjMatrix = camera.current.projectionMatrix;
plane.current.material.lightViewMatrix = camera.current.matrixWorldInverse;
causticsMaterial.normalTexture = normalTarget.texture;
causticsMaterial.depthTexture = normalTarget.depthTexture;
gl.setRenderTarget(causticsTarget);
gl.clear();
causticsQuad.render(gl);
// Render back face caustics, if enabled
causticsMaterial.ior = backsideIOR;
causticsMaterial.normalTexture = normalTargetB.texture;
causticsMaterial.depthTexture = normalTargetB.depthTexture;
gl.setRenderTarget(causticsTargetB);
gl.clear();
if (backside) causticsQuad.render(gl);
// Reset render target
gl.setRenderTarget(null);
// Switch the scene off if caustics is all that's wanted
if (causticsOnly) scene.current.visible = false;
}
});
React.useImperativeHandle(fref, () => ref.current, []);
return /*#__PURE__*/React.createElement("group", _extends({
ref: ref
}, props), /*#__PURE__*/React.createElement("scene", {
ref: scene
}, /*#__PURE__*/React.createElement("orthographicCamera", {
ref: camera,
up: [0, 1, 0]
}), children), /*#__PURE__*/React.createElement("mesh", {
renderOrder: 2,
ref: plane,
"rotation-x": -Math.PI / 2
}, /*#__PURE__*/React.createElement("planeGeometry", null), /*#__PURE__*/React.createElement("causticsProjectionMaterial", {
transparent: true,
color: color,
causticsTexture: causticsTarget.texture,
causticsTextureB: causticsTargetB.texture,
blending: THREE.CustomBlending,
blendSrc: THREE.OneFactor,
blendDst: THREE.SrcAlphaFactor,
depthWrite: false
}), debug && /*#__PURE__*/React.createElement(Edges, null, /*#__PURE__*/React.createElement("lineBasicMaterial", {
color: "#ffff00",
toneMapped: false
}))));
});
export { Caustics };