UNPKG

@wolffo/three-fire

Version:

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

359 lines (353 loc) 11.8 kB
'use strict'; var three = require('three'); var webgpu = require('three/webgpu'); var tsl = require('three/tsl'); var jsxRuntime = require('react/jsx-runtime'); var react = require('react'); var fiber = require('@react-three/fiber'); /** * @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 three.Color ? config.color : new three.Color(config.color ?? 0xeeeeee); config.fireTex.magFilter = three.LinearFilter; config.fireTex.minFilter = three.LinearFilter; config.fireTex.wrapS = three.ClampToEdgeWrapping; config.fireTex.wrapT = three.ClampToEdgeWrapping; return { fireTex: config.fireTex, color: tsl.uniform(colorValue), time: tsl.uniform(0), seed: tsl.uniform(Math.random() * 19.19), invModelMatrix: tsl.uniform(new three.Matrix4()), scale: tsl.uniform(new three.Vector3(1, 1, 1)), noiseScale: tsl.uniform(new three.Vector4(...(config.noiseScale ?? [1, 2, 1, 0.3]))), magnitude: tsl.uniform(config.magnitude ?? 1.3), lacunarity: tsl.uniform(config.lacunarity ?? 2.0), gain: tsl.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) => tsl.Fn(([p, lacunarityUniform, gainUniform]) => { const sum = tsl.float(0).toVar('turbSum'); const freq = tsl.float(1).toVar('turbFreq'); const amp = tsl.float(1).toVar('turbAmp'); const pos = tsl.vec3(p).toVar('turbPos'); tsl.Loop(tsl.int(octaves), () => { sum.addAssign(tsl.abs(tsl.mx_noise_float(pos.mul(freq))).mul(amp)); freq.mulAssign(lacunarityUniform); amp.mulAssign(gainUniform); }); return sum; }); const turbulence3 = createTurbulence(3); const localize = tsl.Fn(([worldPos, invMatrix]) => { return invMatrix.mul(tsl.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) => tsl.Fn(([p, scaleVec]) => { const radius = tsl.sqrt(tsl.dot(p.xz, p.xz)); const st = tsl.vec2(radius, p.y).toVar('st'); const animP = tsl.vec3(p).toVar('animP'); const timeOffset = uniforms.seed.add(tsl.time).mul(scaleVec.w); animP.y.subAssign(timeOffset); animP.assign(animP.mul(tsl.vec3(scaleVec.x, scaleVec.y, scaleVec.z))); const turbulenceValue = turbulence3(animP, uniforms.lacunarity, uniforms.gain); st.y.addAssign(tsl.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 = tsl.texture(uniforms.fireTex, st); return tsl.select(outOfBounds, tsl.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 tsl.Fn(() => { const rayPos = tsl.vec3(tsl.positionWorld).toVar('rayPos'); const rayDir = tsl.normalize(rayPos.sub(tsl.cameraPosition)).toVar('rayDir'); const rayLen = tsl.float(0.0288).mul(tsl.length(uniforms.scale)); const col = tsl.vec4(0.0).toVar('col'); tsl.Loop(tsl.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 = tsl.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 three.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 three.BoxGeometry(1, 1, 1); const config = { fireTex, color: color instanceof three.Color ? color : new three.Color(color), noiseScale, magnitude, lacunarity, gain, }; const uniforms = createFireUniforms(config); const material = new webgpu.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 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.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; } } fiber.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 = react.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 = react.useRef(null); const isTextureUrl = typeof texture === 'string'; const loadedTexture = isTextureUrl ? fiber.useLoader(three.TextureLoader, texture) : null; const finalTexture = isTextureUrl ? loadedTexture : texture; const fireProps = react.useMemo(() => ({ fireTex: finalTexture, color: color instanceof three.Color ? color : new three.Color(color), iterations, noiseScale, magnitude, lacunarity, gain, }), [finalTexture, color, iterations, noiseScale, magnitude, lacunarity, gain]); fiber.useFrame((state) => { if (fireRef.current && autoUpdate) { const time = state.clock.getElapsedTime(); fireRef.current.update(time); onUpdate?.(fireRef.current, time); } }); react.useImperativeHandle(ref, () => ({ get fire() { return fireRef.current; }, update: (time) => { if (fireRef.current) { fireRef.current.update(time); } }, }), []); return (jsxRuntime.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 = react.useRef(null); return { ref, fire: ref.current?.fire || null, update: (time) => ref.current?.update(time), }; }; exports.Fire = FireComponent; exports.FireComponent = FireComponent; exports.FireMesh = FireTSL; exports.createFireFragmentNode = createFireFragmentNode; exports.createFireUniforms = createFireUniforms; exports.useFire = useFire; //# sourceMappingURL=index.js.map