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 @hmng8/use-shader-fx
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
and dpr
.
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, and renderTarget.
const [updateFx, setParams, fxObject] = useSomeFx(config);
Execute 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 "@hmng8/use-shader-fx";
export const Demo = () => {
const ref = useRef<THREE.ShaderMaterial>(null);
const size = useThree((state) => state.size);
const dpr = useThree((state) => state.viewport.dpr);
const [updateFluid] = useFluid({ size, dpr });
useFrame((props) => {
ref.current!.uniforms.u_fx.value = 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: null },
}}
/>
</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),
});
},
});
With some functions provided by use-shader-fx
, creating a custom hook is straightforward (the challenging part is only the shader!). Please refer to existing fxHooks
for details.
In addition, we have prepared a template in the repository below that is useful for creating custom hooks, so please clone and use it in the location where you created your custom hook.
git clone https://github.com/takuma-hmng8/CreateShaderFx
If you can create a cool FX, please contribute! 👉 CONTRIBUTING! 👈
Generates FBO and returns a double-buffered buffer texture after swapping. The useFBO
of drei
by default performs setSize
for THREE.WebGLRenderTarget
upon changes in dpr
and size
, making it challenging to handle buffer textures during changes like dpr adjustments. Therefore, a non-reactive hook against updates of dpr and size was created. It's possible to make them reactive individually through options. If you want to setSize
at a custom timing, the fxObject
that the fxHook receives as the third argument also stores renderTarget
.
type UseFboProps = {
scene: THREE.Scene;
camera: THREE.Camera;
size: Size;
/** If dpr is set, dpr will be multiplied, default:false */
dpr?: number | false;
/** Whether to resize on resizes. If isDpr is true, set FBO to setSize even if dpr is changed, default:false */
isSizeUpdate?: boolean;
};
const [velocityFBO, updateVelocityFBO] = useDoubleFBO(UseFboProps);
When you call the update function, it returns a double-buffered texture. The second argument gets a function called before gl.render()
, allowing for operations like swapping materials or setting uniforms.
const texture = updateVelocityFBO(gl, ({ read, write }) => {
// callback before gl.render()
setMeshMaterial(materials.advectionMaterial);
setUniform(materials.advectionMaterial, "uVelocity", read);
});
This is a version without double buffering.
const [renderTarget, updateRenderTarget] = useSingleFBO(UseFboProps);
Generates and returns a THREE.OrthographicCamera
.
const camera = useCamera(size);
When given the pointer
vector2 from r3f's RootState
, it generates an update function that returns {currentPointer, prevPointer, diffPointer, isVelocityUpdate, velocity}.
const updatePointer = usePointer();
const { currentPointer, prevPointer, diffPointer, isVelocityUpdate, velocity } =
updatePointer(pointer);
This hook returns resolution
. If dpr
isn't set (or set to false), dpr won't be multiplied.
const resolution = useResolution(size: Size, dpr: number | false = false);
Creates a mesh and adds it to scene, geometry, and material. Returns the mesh.
useAddMesh(scene, geometry, material);
A function to set values in the uniforms of the shader material.
setUniform(material, "key", someValue);
Returns the refObject of params and its update function.
const [params, setParams] = useParams<HooksParams>;
{
// HookPrams
}
Generate an FBO array to copy the texture.
const [renderTargets, copyTexture] = useCopyTexture(
{ scene, camera, size, dpr },
length
);
copyTexture(gl, index); // return texture
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({
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);
}
}),
resolution: [...Array(domArr.current.length)].map(() =>
resolutionRef.current.set(props.size.width, props.size.height)
),
});
});
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;
/**
* 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;
/** Returns the target's DOMRect[] */
DOMRects: DOMRect[];
};
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[];
/** Texture resolution array to pass */
resolution?: THREE.Vector2[];
/** default:0.0[] */
boderRadius?: number[];
/** Array of callback functions when crossed */
onIntersect?: ((entry: IntersectionObserverEntry) => void)[];
};