@wolffo/three-fire
Version:
Modern TypeScript volumetric fire effect for Three.js and React Three Fiber with WebGPU support.
252 lines (247 loc) • 8.61 kB
JavaScript
;
var three = require('three');
var webgpu = require('three/webgpu');
var tsl = require('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 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;
}
}
exports.FireMesh = FireTSL;
exports.createFireFragmentNode = createFireFragmentNode;
exports.createFireUniforms = createFireUniforms;
//# sourceMappingURL=vanilla.js.map