UNPKG

@wolffo/three-fire

Version:

Modern TypeScript volumetric fire effect for Three.js and React Three Fiber

447 lines (407 loc) 12.8 kB
import { jsx } from 'react/jsx-runtime'; import { forwardRef, useRef, useMemo, useImperativeHandle } from 'react'; import { extend, useLoader, useFrame } from '@react-three/fiber'; import { Vector4, Vector3, Matrix4, Color, Mesh, BoxGeometry, ShaderMaterial, LinearFilter, ClampToEdgeWrapping, TextureLoader } from 'three'; /** * 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 = { uniforms: { color: { value: new Color(0xeeeeee) }, invModelMatrix: { value: new Matrix4() }, scale: { value: new Vector3(1, 1, 1) }, noiseScale: { value: new Vector4(1, 2, 1, 0.3) }}, 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 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 BoxGeometry(1, 1, 1); const material = new ShaderMaterial({ defines: { ITERATIONS: iterations.toString(), OCTAVES: octaves.toString(), }, uniforms: { fireTex: { value: fireTex }, color: { value: color instanceof Color ? color : new Color(color) }, time: { value: 0.0 }, seed: { value: Math.random() * 19.19 }, invModelMatrix: { value: new Matrix4() }, scale: { value: new Vector3(1, 1, 1) }, noiseScale: { value: new 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 = LinearFilter; fireTex.wrapS = fireTex.wrapT = 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 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.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) => useLoader(TextureLoader, url); // Extend R3F with our Fire class 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 = 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 = 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 = useMemo(() => ({ fireTex: finalTexture, color: color instanceof Color ? color : new Color(color), iterations, octaves, noiseScale, magnitude, lacunarity, gain, }), [finalTexture, color, iterations, octaves, noiseScale, magnitude, lacunarity, gain]); // Auto-update with useFrame useFrame((state) => { if (fireRef.current && autoUpdate) { const time = state.clock.getElapsedTime(); fireRef.current.update(time); onUpdate?.(fireRef.current, time); } }); // Expose imperative handle useImperativeHandle(ref, () => ({ get fire() { return fireRef.current; }, update: (time) => { if (fireRef.current) { fireRef.current.update(time); } }, }), []); return (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 = 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), }; }; export { FireComponent as Fire, useFire }; //# sourceMappingURL=react.esm.js.map