Early-stage collection of VFX shaders, utils, and SDFs for Three.js Shading Language (TSL).
The effects work with the WebGPU renderer and its WebGL backend fallback. To set up your project with WebGPURenderer
, check out this repo. The examples use React Three Fiber but the shaders can be used with vanilla Three.js or any other wrapper.
import { MeshBasicNodeMaterial } from 'three/webgpu'
import { impact } from 'tslfx'
const { uniforms, nodes } = impact()
const material = new MeshBasicNodeMaterial()
material.colorNode = nodes.colorNode
const animate = (delta: number) => {
// Update uniforms in your animation loop
uniforms.time.value += delta
}
Somewhere top-level in your project, extend MeshBasicNodeMaterial
and declare its type:
import { extend, type ThreeElement } from '@react-three/fiber'
import { MeshBasicNodeMaterial } from 'three/webgpu'
extend({ MeshBasicNodeMaterial })
declare module '@react-three/fiber' {
interface ThreeElements {
meshBasicNodeMaterial: ThreeElement<typeof MeshBasicNodeMaterial>
}
}
Tip
I like to put these in an extend.ts
file that I import in my main entry file (_app.tsx
in Next.js for example).
You can then use meshBasicNodeMaterial
in your scene:
import { useFrame } from '@react-three/fiber'
import { impact } from 'tslfx'
const ImpactVFX = () => {
const { uniforms, nodes } = useMemo(() => impact(), [])
useFrame((_, delta) => {
// Update uniforms here
uniforms.time.value += delta
})
return (
<mesh>
<planeGeometry />
<meshBasicNodeMaterial {...nodes} transparent />
</mesh>
)
}
Caution
You should memoize any TSL logic to not re-create nodes on every render.
Primitives are basic single effects. You can combine them together to create more complex effects, which I am calling "compositions" in this library. There is no API to combine them yet, but check out the code of the examples to see how it works.
Some Signed Distance Functions (SDFs) ported from Inigo Quilez's website are included in the library:
import { sdCircle, sdVesica, sdHeart } from 'tslfx'
const circle = sdCircle(p, 0.5)
const vesica = sdVesica(p, 0.5, 0.2)
const heart = sdHeart(p)
The Timing example shows how to combine multiple effects triggered at different times over a given duration, with delays and durations per effect.
All the effects have a normalized time
uniform and a duration of 1
, and their timing is relative to the whole animation, making it easier to tweak them all at once and create slow-motion or speed-up effects.
TSL functions that take 2 nodes as arguments can be piped together into a single expression without introducing extra variables or reassignments:
import { blendAlpha, pipe } from 'tslfx'
// Simple layering of 3 effects on top of each other
const colorNode = pipe(blendAlpha, effectA.colorNode, effectB.colorNode, effectC.colorNode)
pipe
is really just a more readable .reduce()
of the arguments, nothing fancy.
blendAlpha
blends two colors together using the alpha channel of the second color.
import { blendAlpha } from 'tslfx'
const colorNode = blendAlpha(effectA.colorNode, effectB.colorNode)
A few easing functions are included in the library:
linear
easeInCubic
easeOutCubic
easeInOutCubic
Some MaterialX-based noises are directly included in Three.js.
For example, the following code creates a basic water-like effect using mx_noise_vec3
:
import { color, float, mix, mx_noise_vec3, time, uv, vec3 } from 'three/tsl'
const blue = color('#3871ff')
const lightBlue = color('#579dff')
const p = uv().sub(0.5).mul(8).add(0.5)
const rawNoise = mx_noise_vec3(vec3(p, time.mul(0.5))).xxx
const adjustedNoise = float(0.5).add(float(0.5).mul(rawNoise))
const colorNode = mix(blue, lightBlue, adjustedNoise)