@wolffo/three-fire
Version:
Modern TypeScript volumetric fire effect for Three.js and React Three Fiber
464 lines (423 loc) • 13.1 kB
JavaScript
'use strict';
var three = require('three');
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
var fiber = require('@react-three/fiber');
/**
* Volumetric fire shader using ray marching and simplex noise
*
* Based on "Real-Time procedural volumetric fire" by Alfred et al.
* Uses simplex noise for turbulence and ray marching for volume rendering.
*
* @example
* ```ts
* const material = new ShaderMaterial({
* defines: FireShader.defines,
* uniforms: FireShader.uniforms,
* vertexShader: FireShader.vertexShader,
* fragmentShader: FireShader.fragmentShader,
* transparent: true
* })
* ```
*/
const FireShader = {
defines: {
ITERATIONS: '20',
OCTAVES: '3',
},
uniforms: {
fireTex: { value: null },
color: { value: new three.Color(0xeeeeee) },
time: { value: 0.0 },
seed: { value: 0.0 },
invModelMatrix: { value: new three.Matrix4() },
scale: { value: new three.Vector3(1, 1, 1) },
noiseScale: { value: new three.Vector4(1, 2, 1, 0.3) },
magnitude: { value: 1.3 },
lacunarity: { value: 2.0 },
gain: { value: 0.5 },
},
vertexShader: /* glsl */ `
varying vec3 vWorldPos;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
}
`,
fragmentShader: /* glsl */ `
uniform vec3 color;
uniform float time;
uniform float seed;
uniform mat4 invModelMatrix;
uniform vec3 scale;
uniform vec4 noiseScale;
uniform float magnitude;
uniform float lacunarity;
uniform float gain;
uniform sampler2D fireTex;
varying vec3 vWorldPos;
// GLSL simplex noise function by ashima
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 mod289(vec4 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 permute(vec4 x) {
return mod289(((x * 34.0) + 1.0) * x);
}
vec4 taylorInvSqrt(vec4 r) {
return 1.79284291400159 - 0.85373472095314 * r;
}
float snoise(vec3 v) {
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}
float turbulence(vec3 p) {
float sum = 0.0;
float freq = 1.0;
float amp = 1.0;
for(int i = 0; i < OCTAVES; i++) {
sum += abs(snoise(p * freq)) * amp;
freq *= lacunarity;
amp *= gain;
}
return sum;
}
vec4 samplerFire(vec3 p, vec4 scale) {
vec2 st = vec2(sqrt(dot(p.xz, p.xz)), p.y);
if(st.x <= 0.0 || st.x >= 1.0 || st.y <= 0.0 || st.y >= 1.0) {
return vec4(0.0);
}
p.y -= (seed + time) * scale.w;
p *= scale.xyz;
st.y += sqrt(st.y) * magnitude * turbulence(p);
if(st.y <= 0.0 || st.y >= 1.0) {
return vec4(0.0);
}
return texture2D(fireTex, st);
}
vec3 localize(vec3 p) {
return (invModelMatrix * vec4(p, 1.0)).xyz;
}
void main() {
vec3 rayPos = vWorldPos;
vec3 rayDir = normalize(rayPos - cameraPosition);
float rayLen = 0.0288 * length(scale.xyz);
vec4 col = vec4(0.0);
for(int i = 0; i < ITERATIONS; i++) {
rayPos += rayDir * rayLen;
vec3 lp = localize(rayPos);
lp.y += 0.5;
lp.xz *= 2.0;
col += samplerFire(lp, noiseScale);
}
// Apply color tint to the fire
col.rgb *= color;
col.a = col.r;
gl_FragColor = col;
}
`,
};
/**
* Volumetric fire effect using ray marching shaders
*
* Creates a procedural fire effect that renders as a translucent volume.
* The fire shape is defined by a grayscale texture, with white areas being
* the most dense part of the fire.
*
* @example
* ```ts
* const texture = textureLoader.load('fire.png')
* const fire = new Fire({
* fireTex: texture,
* color: 0xff4400,
* magnitude: 1.5
* })
* scene.add(fire)
*
* // In animation loop
* fire.update(time)
* ```
*/
class Fire extends three.Mesh {
/**
* Creates a new Fire instance
*
* @param props - Configuration options for the fire effect
*/
constructor({ fireTex, color = 0xeeeeee, iterations = 20, octaves = 3, noiseScale = [1, 2, 1, 0.3], magnitude = 1.3, lacunarity = 2.0, gain = 0.5, }) {
const geometry = new three.BoxGeometry(1, 1, 1);
const material = new three.ShaderMaterial({
defines: {
ITERATIONS: iterations.toString(),
OCTAVES: octaves.toString(),
},
uniforms: {
fireTex: { value: fireTex },
color: { value: color instanceof three.Color ? color : new three.Color(color) },
time: { value: 0.0 },
seed: { value: Math.random() * 19.19 },
invModelMatrix: { value: new three.Matrix4() },
scale: { value: new three.Vector3(1, 1, 1) },
noiseScale: { value: new three.Vector4(...noiseScale) },
magnitude: { value: magnitude },
lacunarity: { value: lacunarity },
gain: { value: gain },
},
vertexShader: FireShader.vertexShader,
fragmentShader: FireShader.fragmentShader,
transparent: true,
depthWrite: false,
depthTest: false,
});
super(geometry, material);
this._time = 0;
// Configure texture
fireTex.magFilter = fireTex.minFilter = three.LinearFilter;
fireTex.wrapS = fireTex.wrapT = three.ClampToEdgeWrapping;
}
/**
* Updates the fire animation and matrix uniforms
*
* Call this method in your animation loop to animate the fire effect.
*
* @param time - Current time in seconds (optional)
*
* @example
* ```ts
* function animate() {
* fire.update(performance.now() / 1000)
* renderer.render(scene, camera)
* requestAnimationFrame(animate)
* }
* ```
*/
update(time) {
if (time !== undefined) {
this._time = time;
this.material.uniforms.time.value = time;
}
this.updateMatrixWorld();
this.material.uniforms.invModelMatrix.value.copy(this.matrixWorld).invert();
this.material.uniforms.scale.value.copy(this.scale);
}
/**
* Current animation time in seconds
*/
get time() {
return this._time;
}
set time(value) {
this._time = value;
this.material.uniforms.time.value = value;
}
/**
* Fire color tint
*
* @example
* ```ts
* fire.fireColor = 'orange'
* fire.fireColor = 0xff4400
* fire.fireColor = new Color(1, 0.5, 0)
* ```
*/
get fireColor() {
return this.material.uniforms.color.value;
}
set fireColor(color) {
this.material.uniforms.color.value = color instanceof three.Color ? color : new three.Color(color);
}
/**
* Fire shape intensity
*
* Higher values create more dramatic fire shapes.
* Range: 0.5 - 3.0, Default: 1.3
*/
get magnitude() {
return this.material.uniforms.magnitude.value;
}
set magnitude(value) {
this.material.uniforms.magnitude.value = value;
}
/**
* Noise lacunarity (frequency multiplier)
*
* Controls how much the frequency increases for each noise octave.
* Range: 1.0 - 4.0, Default: 2.0
*/
get lacunarity() {
return this.material.uniforms.lacunarity.value;
}
set lacunarity(value) {
this.material.uniforms.lacunarity.value = value;
}
/**
* Noise gain (amplitude multiplier)
*
* Controls how much the amplitude decreases for each noise octave.
* Range: 0.1 - 1.0, Default: 0.5
*/
get gain() {
return this.material.uniforms.gain.value;
}
set gain(value) {
this.material.uniforms.gain.value = value;
}
}
/**
* Helper hook for texture loading (alternative to @react-three/drei)
*/
const useTexture = (url) => fiber.useLoader(three.TextureLoader, url);
// Extend R3F with our Fire class
fiber.extend({ Fire: Fire });
/**
* React Three Fiber component for volumetric fire effect
*
* Creates a procedural fire effect that can be easily integrated into R3F scenes.
* The component automatically handles texture loading, animation updates, and
* provides props for all fire parameters.
*
* @example
* ```tsx
* <Canvas>
* <Fire
* texture="/fire.png"
* color="orange"
* magnitude={1.5}
* scale={[2, 3, 2]}
* position={[0, 0, 0]}
* />
* </Canvas>
* ```
*
* @example With custom animation
* ```tsx
* <Fire
* texture="/fire.png"
* onUpdate={(fire, time) => {
* fire.fireColor.setHSL((time * 0.1) % 1, 1, 0.5)
* }}
* />
* ```
*/
const FireComponent = react.forwardRef(({ texture, color = 0xeeeeee, iterations = 20, octaves = 3, noiseScale = [1, 2, 1, 0.3], magnitude = 1.3, lacunarity = 2.0, gain = 0.5, autoUpdate = true, onUpdate, children, ...props }, ref) => {
const fireRef = react.useRef(null);
// Load texture if string is provided
const loadedTexture = useTexture(typeof texture === 'string' ? texture : '');
const finalTexture = typeof texture === 'string' ? loadedTexture : texture;
// Memoize fire props to prevent unnecessary recreations
const fireProps = react.useMemo(() => ({
fireTex: finalTexture,
color: color instanceof three.Color ? color : new three.Color(color),
iterations,
octaves,
noiseScale,
magnitude,
lacunarity,
gain,
}), [finalTexture, color, iterations, octaves, noiseScale, magnitude, lacunarity, gain]);
// Auto-update with useFrame
fiber.useFrame((state) => {
if (fireRef.current && autoUpdate) {
const time = state.clock.getElapsedTime();
fireRef.current.update(time);
onUpdate?.(fireRef.current, time);
}
});
// Expose imperative handle
react.useImperativeHandle(ref, () => ({
get fire() {
return fireRef.current;
},
update: (time) => {
if (fireRef.current) {
fireRef.current.update(time);
}
},
}), []);
return (jsxRuntime.jsx("fire", { ref: fireRef, args: [fireProps], ...props, children: children }));
});
FireComponent.displayName = 'Fire';
/**
* Hook for easier access to fire instance and controls
*
* Provides a ref and helper methods for controlling fire imperatively.
*
* @returns Object with ref, fire instance, and update method
*
* @example
* ```tsx
* function MyComponent() {
* const fireRef = useFire()
*
* const handleClick = () => {
* if (fireRef.fire) {
* fireRef.fire.magnitude = 2.0
* }
* }
*
* return (
* <Fire ref={fireRef.ref} texture="/fire.png" />
* )
* }
* ```
*/
const useFire = () => {
const ref = react.useRef(null);
return {
/** Ref to pass to Fire component */
ref,
/** Fire mesh instance (null until mounted) */
fire: ref.current?.fire || null,
/** Update fire animation manually */
update: (time) => ref.current?.update(time),
};
};
exports.Fire = FireComponent;
exports.FireComponent = FireComponent;
exports.FireMesh = Fire;
exports.FireShader = FireShader;
exports.useFire = useFire;
//# sourceMappingURL=index.js.map