@wolffo/three-fire
Version:
Modern TypeScript volumetric fire effect for Three.js and React Three Fiber with WebGPU support.
352 lines (347 loc) • 11.5 kB
JavaScript
import { jsx } from 'react/jsx-runtime';
import { forwardRef, useRef, useMemo, useImperativeHandle } from 'react';
import { extend, useLoader, useFrame } from '@react-three/fiber';
import { Color, LinearFilter, ClampToEdgeWrapping, Vector4, Vector3, Matrix4, Mesh, BoxGeometry, TextureLoader } from 'three';
import { MeshBasicNodeMaterial } from 'three/webgpu';
import { Fn, vec4, float, vec3, Loop, int, abs, mx_noise_float, uniform, positionWorld, normalize, cameraPosition, length, sqrt, dot, vec2, time, texture, select } from 'three/tsl';
/**
* @fileoverview TSL (Three.js Shading Language) implementation of volumetric fire shader
*
* Uses WebGPU-compatible node-based shaders with Perlin noise (mx_noise_float).
* This is the TSL equivalent of the GLSL FireShader.
*/
const createFireUniforms = (config) => {
const colorValue = config.color instanceof Color ? config.color : new Color(config.color ?? 0xeeeeee);
config.fireTex.magFilter = LinearFilter;
config.fireTex.minFilter = LinearFilter;
config.fireTex.wrapS = ClampToEdgeWrapping;
config.fireTex.wrapT = ClampToEdgeWrapping;
return {
fireTex: config.fireTex,
color: uniform(colorValue),
time: uniform(0),
seed: uniform(Math.random() * 19.19),
invModelMatrix: uniform(new Matrix4()),
scale: uniform(new Vector3(1, 1, 1)),
noiseScale: uniform(new Vector4(...(config.noiseScale ?? [1, 2, 1, 0.3]))),
magnitude: uniform(config.magnitude ?? 1.3),
lacunarity: uniform(config.lacunarity ?? 2.0),
gain: uniform(config.gain ?? 0.5),
};
};
/**
* Turbulence function using Fractional Brownian Motion (FBM)
* Uses mx_noise_float (Perlin noise) instead of simplex noise
*/
const createTurbulence = (octaves) => Fn(([p, lacunarityUniform, gainUniform]) => {
const sum = float(0).toVar('turbSum');
const freq = float(1).toVar('turbFreq');
const amp = float(1).toVar('turbAmp');
const pos = vec3(p).toVar('turbPos');
Loop(int(octaves), () => {
sum.addAssign(abs(mx_noise_float(pos.mul(freq))).mul(amp));
freq.mulAssign(lacunarityUniform);
amp.mulAssign(gainUniform);
});
return sum;
});
const turbulence3 = createTurbulence(3);
const localize = Fn(([worldPos, invMatrix]) => {
return invMatrix.mul(vec4(worldPos, 1.0)).xyz;
});
/**
* Creates fire sampler function with uniforms captured in closure
* This is necessary because TSL Fn parameters must be TSL nodes, not plain objects
* Uses TSL's built-in `time` node for automatic animation
*/
const createSamplerFire = (uniforms) => Fn(([p, scaleVec]) => {
const radius = sqrt(dot(p.xz, p.xz));
const st = vec2(radius, p.y).toVar('st');
const animP = vec3(p).toVar('animP');
const timeOffset = uniforms.seed.add(time).mul(scaleVec.w);
animP.y.subAssign(timeOffset);
animP.assign(animP.mul(vec3(scaleVec.x, scaleVec.y, scaleVec.z)));
const turbulenceValue = turbulence3(animP, uniforms.lacunarity, uniforms.gain);
st.y.addAssign(sqrt(st.y).mul(uniforms.magnitude).mul(turbulenceValue));
const outOfBounds = st.x
.lessThanEqual(0.0)
.or(st.x.greaterThanEqual(1.0))
.or(st.y.lessThanEqual(0.0))
.or(st.y.greaterThanEqual(1.0));
const texSample = texture(uniforms.fireTex, st);
return select(outOfBounds, vec4(0.0), texSample);
});
/**
* Creates the main fire fragment node for ray marching
*
* @param uniforms - Fire shader uniforms
* @param iterations - Number of ray marching iterations (default: 20)
* @returns TSL node for the fragment shader
*/
const createFireFragmentNode = (uniforms, iterations = 20) => {
const samplerFire = createSamplerFire(uniforms);
return Fn(() => {
const rayPos = vec3(positionWorld).toVar('rayPos');
const rayDir = normalize(rayPos.sub(cameraPosition)).toVar('rayDir');
const rayLen = float(0.0288).mul(length(uniforms.scale));
const col = vec4(0.0).toVar('col');
Loop(int(iterations), () => {
rayPos.addAssign(rayDir.mul(rayLen));
const lp = localize(rayPos, uniforms.invModelMatrix).toVar('lp');
lp.y.addAssign(0.5);
lp.x.mulAssign(2.0);
lp.z.mulAssign(2.0);
col.addAssign(samplerFire(lp, uniforms.noiseScale));
});
const colorVec = vec3(uniforms.color);
col.x.mulAssign(colorVec.x);
col.y.mulAssign(colorVec.y);
col.z.mulAssign(colorVec.z);
col.w.assign(col.x);
return col;
})();
};
/**
* @fileoverview TSL Fire mesh class for vanilla Three.js with WebGPU support
*
* This is the TSL equivalent of the GLSL Fire class, using MeshBasicNodeMaterial
* and node-based shaders for WebGPU compatibility.
*/
/**
* Volumetric fire effect using TSL ray marching shaders
*
* WebGPU-compatible version using Three.js Shading Language (TSL).
* Creates a procedural fire effect that renders as a translucent volume.
* Uses Perlin noise (mx_noise_float) instead of simplex noise.
*
* @example
* ```ts
* import { FireTSL } from '@wolffo/three-fire/tsl/vanilla'
*
* const texture = textureLoader.load('fire.png')
* const fire = new FireTSL({
* fireTex: texture,
* color: 0xff4400,
* magnitude: 1.5
* })
* scene.add(fire)
*
* // In animation loop
* fire.update(time)
* ```
*/
class FireTSL extends Mesh {
/**
* Creates a new FireTSL instance
*
* @param props - Configuration options for the fire effect
*/
constructor({ fireTex, color = 0xeeeeee, iterations = 20,
// octaves is fixed at 3 in TSL version for performance
noiseScale = [1, 2, 1, 0.3], magnitude = 1.3, lacunarity = 2.0, gain = 0.5, }) {
const geometry = new BoxGeometry(1, 1, 1);
const config = {
fireTex,
color: color instanceof Color ? color : new Color(color),
noiseScale,
magnitude,
lacunarity,
gain,
};
const uniforms = createFireUniforms(config);
const material = new MeshBasicNodeMaterial();
material.fragmentNode = createFireFragmentNode(uniforms, iterations);
material.transparent = true;
material.depthWrite = false;
material.depthTest = false;
super(geometry, material);
this._time = 0;
this.uniforms = uniforms;
}
/**
* 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.uniforms.time.value = time;
}
this.updateMatrixWorld();
this.uniforms.invModelMatrix.value.copy(this.matrixWorld).invert();
this.uniforms.scale.value.copy(this.scale);
}
get time() {
return this._time;
}
set time(value) {
this._time = value;
this.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.uniforms.color.value;
}
set fireColor(color) {
this.uniforms.color.value = color instanceof Color ? color : new Color(color);
}
/**
* Fire shape intensity
*
* Higher values create more dramatic fire shapes.
* Range: 0.5 - 3.0, Default: 1.3
*/
get magnitude() {
return this.uniforms.magnitude.value;
}
set magnitude(value) {
this.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.uniforms.lacunarity.value;
}
set lacunarity(value) {
this.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.uniforms.gain.value;
}
set gain(value) {
this.uniforms.gain.value = value;
}
}
extend({ FireTSL });
/**
* React Three Fiber component for volumetric fire effect (TSL/WebGPU version)
*
* Creates a procedural fire effect using TSL (Three.js Shading Language)
* for WebGPU compatibility. Uses Perlin noise instead of simplex noise.
*
* Note: This component requires WebGPURenderer or a compatible WebGL fallback.
*
* @example
* ```tsx
* import { Fire } from '@wolffo/three-fire/tsl/react'
*
* <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 = forwardRef(({ texture, color = 0xeeeeee, iterations = 20, noiseScale = [1, 2, 1, 0.3], magnitude = 1.3, lacunarity = 2.0, gain = 0.5, autoUpdate = true, onUpdate, children, ...props }, ref) => {
const fireRef = useRef(null);
const isTextureUrl = typeof texture === 'string';
const loadedTexture = isTextureUrl ? useLoader(TextureLoader, texture) : null;
const finalTexture = isTextureUrl ? loadedTexture : texture;
const fireProps = useMemo(() => ({
fireTex: finalTexture,
color: color instanceof Color ? color : new Color(color),
iterations,
noiseScale,
magnitude,
lacunarity,
gain,
}), [finalTexture, color, iterations, noiseScale, magnitude, lacunarity, gain]);
useFrame((state) => {
if (fireRef.current && autoUpdate) {
const time = state.clock.getElapsedTime();
fireRef.current.update(time);
onUpdate?.(fireRef.current, time);
}
});
useImperativeHandle(ref, () => ({
get fire() {
return fireRef.current;
},
update: (time) => {
if (fireRef.current) {
fireRef.current.update(time);
}
},
}), []);
return (jsx("fireTSL", { 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 = useRef(null);
return {
ref,
fire: ref.current?.fire || null,
update: (time) => ref.current?.update(time),
};
};
export { FireComponent as Fire, useFire };
//# sourceMappingURL=react.esm.js.map