use-shader-fx
is a library designed to easily implement shader effects such as fluid simulations and noise. It relies on react-three-fiber and has been designed with performance control in mind, especially when combined with drei.
For details on each FX, please refer to Storybook 👉 Storybook 👈
npm install @funtech-inc/use-shader-fx
effects | useMotionBlur, useSimpleBlur, useWave |
---|---|
interactions | useBrush, useFluid, useRipple |
misc | useChromaKey |
noises | useColorStrata, useMarble, useNoise |
utils | useAlphaBlending, useBlending, useBrightnessPicker, useCoverTexture, useDuoTone, useFxBlending, useFxTexture, useHSV |
3D | useMorphParticles, useWobble3D |
misc | useBeat, useFPSLimiter, usePointer, useDomSyncer |
---|
From each fxHooks
, you can receive [updateFx
, setParams
, fxObject
] in array format. The config
is an object, which varies for each Hook, containing details such as size
,dpr
and samples
.
updateFx
- A function to be invoked insideuseFrame
, returning aTHREE.Texture
.setParams
- A function to refresh the parameters, beneficial for performance tweaking, etc.fxObject
- An object that holds various FX components, such as scene, camera, material,renderTarget, andoutput
(final rendered texture).
const [updateFx, setParams, fxObject] = useSomeFx(config);
invoke updateFx
in useFrame
. The first argument receives the RootState from useFrame
, and the second one takes HookPrams
. Each fx has its HookPrams
, and each type is exported.
useFrame((props) => {
const texture = updateFx(props, params);
const main = mainShaderRef.current;
if (main) {
main.u_bufferTexture = texture;
}
});
This is the simplest example!
import * as THREE from "three";
import { useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { useFluid } from "@funtech-inc/use-shader-fx";
export const Home = () => {
const ref = useRef<THREE.ShaderMaterial>(null);
const { size, viewport } = useThree();
const [updateFluid, , { output }] = useFluid({ size, dpr: viewport.dpr });
useFrame((props) => updateFluid(props));
return (
<mesh>
<planeGeometry args={[2, 2]} />
<shaderMaterial
ref={ref}
vertexShader={`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`}
fragmentShader={`
precision highp float;
varying vec2 vUv;
uniform sampler2D u_fx;
void main() {
vec2 uv = vUv;
gl_FragColor = texture2D(u_fx, uv);
}
`}
uniforms={{
u_fx: { value: output },
}}
/>
</mesh>
);
};
You can use r3f/createPortal
to make some mesh render off-screen. All that remains is to combine the generated textures with FX!
import * as THREE from "three";
import { useMemo, useRef, useState } from "react";
import { useFrame, useThree, createPortal } from "@react-three/fiber";
import { useNoise, useSingleFBO } from "@hmng8/use-shader-fx";
function Box(props: any) {
// This reference will give us direct access to the mesh
const meshRef = useRef<THREE.Mesh>();
// Set up state for the hovered and active state
const [hovered, setHover] = useState(false);
const [active, setActive] = useState(false);
// Subscribe this component to the render-loop, rotate the mesh every frame
useFrame((state, delta) => {
meshRef.current!.rotation.x += delta;
meshRef.current!.rotation.y -= delta;
});
// Return view, these are regular three.js elements expressed in JSX
return (
<mesh
{...props}
ref={meshRef}
scale={active ? 2 : 1.5}
onClick={(event) => setActive(!active)}
onPointerOver={(event) => setHover(true)}
onPointerOut={(event) => setHover(false)}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? "hotpink" : "orange"} />
</mesh>
);
}
export const Home = () => {
const ref = useRef<THREE.ShaderMaterial>(null);
const { size, viewport, camera } = useThree();
const [updateNoise, , { output }] = useNoise({
size,
dpr: viewport.dpr,
});
// This scene is rendered offscreen
const offscreenScene = useMemo(() => new THREE.Scene(), []);
// create FBO for offscreen rendering
const [boxView, updateRenderTarget] = useSingleFBO({
scene: offscreenScene,
camera,
size,
dpr: viewport.dpr,
samples: 4,
});
useFrame((props) => {
updateNoise(props);
updateRenderTarget(props.gl);
});
return (
<>
{createPortal(
<mesh>
<ambientLight intensity={Math.PI} />
<spotLight
position={[10, 10, 10]}
angle={0.15}
penumbra={1}
decay={0}
intensity={Math.PI}
/>
<pointLight
position={[-10, -10, -10]}
decay={0}
intensity={Math.PI}
/>
<Box position={[-1.5, 0, 0]} />
<Box position={[1.5, 0, 0]} />
</mesh>,
offscreenScene
)}
<mesh>
<planeGeometry args={[2, 2]} />
<shaderMaterial
ref={ref}
transparent
vertexShader={`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`}
fragmentShader={`
precision highp float;
varying vec2 vUv;
uniform sampler2D u_fx;
uniform sampler2D u_texture;
void main() {
vec2 uv = vUv;
vec3 noiseMap = texture2D(u_fx, uv).rgb;
vec3 nNoiseMap = noiseMap * 2.0 - 1.0;
uv = uv * 2.0 - 1.0;
uv *= mix(vec2(1.0), abs(nNoiseMap.rg), .6);
uv = (uv + 1.0) / 2.0;
gl_FragColor = texture2D(u_texture, uv);
}
`}
uniforms={{
u_texture: { value: boxView.texture },
u_fx: { value: output },
}}
/>
</mesh>
</>
);
};
You can control the dpr
using the PerformanceMonitor
from drei. For more details, please refer to the scaling-performance of r3f.
export const Fx = () => {
const [dpr, setDpr] = useState(1.5);
return (
<Canvas dpr={dpr}>
<PerformanceMonitor
factor={1}
onChange={({ factor }) => {
console.log(`dpr:${dpr}`);
setDpr(Math.round((0.5 + 1.5 * factor) * 10) / 10);
}}>
<Suspense fallback={null}>
<Scene />
</Suspense>
<Perf position={"bottom-right"} minimal={false} />
</PerformanceMonitor>
</Canvas>
);
};
By using the PerformanceMonitor
, you can subscribe to performance changes with usePerformanceMonitor
. For more details, refer to drei.
With setParams
received from fxHooks
, it's possible to independently control high-load items such as iteration counts.
usePerformanceMonitor({
onChange({ factor }) {
setParams({
pressure_iterations: Math.round(20 * factor),
});
},
});
When using some expensive FX (such as useFluid
), lowering the dpr
of the FBO of that FX can improve performance.
const [updateFx, setParams, fxObject] = useSomeFx({ size, dpr: 0.01 });
Also, you can make more detailed adjustments by passing an object to dpr
instead of number
.
type Dpr =
| number
| {
dpr: number;
/** you can set whether `dpr` affects `shader` and `fbo`. default is `true` for both */
effect?: {
/** default : `true` */
shader?: boolean;
/** default : `true` */
fbo?: boolean;
};
};
The second argument contains the dependency array that updates the DOM. For example, you can pass a pathname
when navigating pages.
const [updateDomSyncer, setDomSyncer, domSyncerObj] = useDomSyncer(
{ size, dpr },
[state]
);
useLayoutEffect(() => {
if (state === 0) {
domArr.current = [...document.querySelectorAll(".item")!];
} else {
domArr.current = [...document.querySelectorAll(".item2")!];
}
setDomSyncer({
// Because DOM rendering and React updates occur asynchronously, there may be a lag between updating dependent arrays and setting DOM arrays. That's what the Key is for. If the dependent array is updated but the Key is not, the loop will skip and return an empty texture. By updating the timing key when DOM acquisition is complete, you can perfectly synchronize DOM and Mesh updates.updateKey must be a unique value for each update, for example `performance.now()
updateKey: performance.now(),
dom: domArr.current,
boderRadius: [...Array(domArr.current.length)].map((_, i) => i * 50.0),
onIntersect: [...Array(domArr.current.length)].map((_, i) => (entry) => {
if (entry.isIntersecting && !domSyncerObj.isIntersecting(i, true)) {
// some callback
}
}),
});
}, [state]);
const [, copyTexture] = useCopyTexture(
{ scene: fxTextureObj.scene, camera: fxTextureObj.camera, size, dpr },
domArr.current.length
);
useFrame((props) => {
const syncedTexture = updateDomSyncer(props, {
texture: [...Array(domArr.current.length)].map((_, i) => {
if (domSyncerObj.isIntersecting(i, false)) {
textureRef.current = updateFxTexture(props, {
map: someFx,
texture0: someTexture,
});
return copyTexture(props.gl, i);
}
}),
});
});
domSyncerObj
contains an isIntersecting function that returns the DOM intersection test
The boolean will be updated after executing the onIntersect
function.
type DomSyncerObject = {
scene: THREE.Scene;
camera: THREE.Camera;
renderTarget: THREE.WebGLRenderTarget;
output: THREE.Texture;
/**
* A function that returns a determination whether the DOM intersects or not.
* The boolean will be updated after executing the onIntersect function.
* @param index - Index of the dom for which you want to return an intersection decision. -1 will return the entire array.
* @param once - If set to true, it will continue to return true once crossed.
*/
isIntersecting: IsIntersecting;
/** target's DOMRect[] */
DOMRects: DOMRect[];
/** target's intersetions boolean[] */
intersections: boolean[];
/** You can set callbacks for when at least one DOM is visible and when it is completely hidden. */
useDomView: UseDomView;
};
DomSyncerParams
can be passed the onIntersect
function.
type DomSyncerParams = {
/** DOM array you want to synchronize */
dom?: (HTMLElement | Element | null)[];
/** Texture array that you want to synchronize with the DOM rectangle */
texture?: THREE.Texture[];
/** default:0.0[] */
boderRadius?: number[];
/** the angle you want to rotate */
rotation?: THREE.Euler[];
/** Array of callback functions when crossed */
onIntersect?: ((entry: IntersectionObserverEntry) => void)[];
/** Because DOM rendering and React updates occur asynchronously, there may be a lag between updating dependent arrays and setting DOM arrays. That's what the Key is for. If the dependent array is updated but the Key is not, the loop will skip and return an empty texture. By updating the timing key when DOM acquisition is complete, you can perfectly synchronize DOM and Mesh updates. */
updateKey?: Key;
};
updateKey
: Because DOM rendering and React updates occur asynchronously, there may be a lag between updating dependent arrays and setting DOM arrays. That's what the Key is for. If the dependent array is updated but the Key is not, the loop will skip and return an empty texture. By updating the timing key when DOM acquisition is complete, you can perfectly synchronize DOM and Mesh updates.
When given the pointer
vector2 from r3f's RootState
, it generates an update function that returns {currentPointer, prevPointer, diffPointer, isVelocityUpdate, velocity}.
You can also add lerp
(0~1, lerp intensity (0 to less than 1) , default: 0)
const updatePointer = usePointer(lerp);
const { currentPointer, prevPointer, diffPointer, isVelocityUpdate, velocity } =
updatePointer(pointer);
You can override the pointer process by passing pointerValues
to updateFx
in the useFrame
.
useFrame((props) => {
const pointerValues = updatePointer(props.pointer);
updateBrush(props, {
pointerValues: pointerValues,
});
});
Time-sensitive hooks such as useNoise
and useMarble
accept beat
.
The second argument can be easing
.
easing functions are referenced from https://github.com/ai/easings.net , default : "easeOutQuart"
const beting = useBeat(bpm, "easeOutQuad");
useFrame((props) => {
const { beat, hash } = beting(props.clock);
updateMarble(props, {
beat: beat,
});
});
type BeatValues = {
beat: number;
floor: number;
fract: number;
/** unique hash specific to the beat */
hash: number;
};
Allows you to skip FX that do not need to be processed at 60 FPS.
const limiter = useFPSLimiter(30);
useFrame((props) => {
if (!limiter(props.clock)) {
return;
}
});
Generate an FBO array to copy the texture.
const [renderTargets, copyTexture] = useCopyTexture(UseFboProps, length);
copyTexture(gl, index); // return texture
The 3D
series has a set of exported hooks, each with Create
, like useCreateWobble3D
, which can be used as a texture, but also to add object3D
as a primitive
to an r3f scene. It is also possible to add object3D
as a primitive
to an r3f scene.
const [updateWobble, wobble] = useCreateWobble3D({
baseMaterial: THREE.MeshPhysicalMaterial,
materialParameters: {
roughness: 0.0,
transmission: 1,
thickness: 1,
},
});
useFrame((props) => updateWobble(props));
return (
<mesh>
<Environment preset="warehouse" background />
<primitive object={wobble.mesh} />
</mesh>
);
👉 wobble3D demo 👈