Help Wanted: Three.Fire - GLSL to TSL

Summary

Request to add a volumetric fire/flame effect example to the three.js examples collection, particularly showcasing TSL (Three.js Shading Language) implementation for WebGPU compatibility.

Motivation

Fire effects are a highly requested feature in the three.js community, while the TSL fire particle example on the official website is good demonstrating how to create realistic volumetric flames. Three.Fire provides a more advanced effect and benefits, unfortunately it’s written in GLSL.

Current Project

The current project THREE.Fire has such benefits:

  • Proven Implementation: Already has a working awesome GLSL-based volumetric fire shader with ray marching
  • Real-world Usage: Based on “Real-Time procedural volumetric fire” by Alfred et al.
  • Complete Package: Includes React components, TypeScript definitions, and comprehensive examples
  • Performance Optimized: Features configurable quality settings and efficient noise generation

TSL Migration Intent

My goal has been to modernize the existing GLSL implementation to TSL format to:

  1. WebGPU Compatibility: Enable fire effects to work with both WebGL and WebGPU renderers
  2. Modern Shader Architecture: Leverage TSL’s node-based system for better maintainability
  3. Future-Proofing: Align with three.js’s direction toward TSL as the primary shading language
  4. Educational Value: Demonstrate TSL patterns for complex effects like volumetric rendering

Technical Challenges Encountered

After spending 2 days attempting the GLSL→TSL migration, I’ve encountered several challenges:

  1. TSL Varying Syntax: Difficulty with proper varying assignment patterns in TSL
  2. Ray Marching Translation: Complex loops and sampling operations don’t translate directly
  3. Limited Documentation: TSL examples for complex effects are scarce
  4. Debugging Complexity: TSL compilation errors are often cryptic

Unfortunately, even with AI assistance (Claude), the shader expertise gap proved significant for completing this migration successfully.

Proposed Solution

Anyone who keen could help the community by:

  1. Creating an Official Example: Add webgl_materials_fire.html and webgpu_materials_fire.html examples
  2. TSL Implementation Guide: Provide a reference implementation showing proper TSL patterns for:
    • Volumetric ray marching
    • Noise generation and turbulence
    • Custom varying handling
    • Complex fragment shader logic
  3. Documentation: Include shader migration guidelines (GLSL→TSL) for advanced effects

Expected Benefits

  • Community Value: Frequently requested feature becomes officially supported
  • Educational Resource: Demonstrates advanced TSL techniques
  • WebGPU Adoption: Encourages migration to modern rendering backend
  • Ecosystem Growth: Enables more sophisticated three.js applications

Additional Context

The existing THREE.Fire implementation includes all the mathematical foundations needed:

  • Simplex noise functions
  • Cylindrical coordinate fire shaping
  • Turbulence displacement
  • Ray marching with configurable iterations
  • Proper alpha blending and transparency

Files of Interest

  • Original GLSL: src/FireShader.ts
  • Attempted TSL: src/materials/nodes/FireNodeMaterial.ts
  • Examples: examples/ directory with vanilla Javascript implementations

Request

Would anyone be interested in either:

  1. Implement the migration on the repo, or
  2. Providing guidance on proper TSL patterns for complex volumetric effects?

Any assistance would be greatly appreciated by the broader three.js community working with advanced shader effects.
While I may not offer much for anyone who can do it, but a coffee will be a sure thing I can offer.

Additional Info

WIP PR (Not working)

I kinda doubt this should be part of the core examples.
It is too specialized and special purpose to justify taking up the space.
Raymarching is pretty complex and wide field, and this is just one take on it.. and wouldn’t necessarily support other forms of raymarched volumetric effects. It also shows artifacts at certain angles.. the demo doesn’t let you zoom in close.
If it was a path tracer, or a raymarched SVO implementation
, or smth, that would make more sense as a general example.

It would be like dropping a spice cake into a spice catalog. Why stop at fire.. and not water, ice, fog, etc. idk.. if it could be shown that the technique applies more generally to other types of effects, then I might be on board.

Another issue I have is the desire to convert everything from GLSL to TSL when the whole point of webGPU stuff is to enable things that you can’t already do with GLSL. Doing this in webgpu wouldn’t really incur any performance advantages.. and from my understanding, you can still mix glsl stuff with webgpu yeah?

idk I could be missing something.. im not an expert in TSL but this is just my gut reaction…

2 Likes

I dug into it a bit more, and stripped out all the extra cruft and made it a single file importable object that just works without all the framework boilerplate/ts/react etc.

import*as THREE from "three"

let fireTex = new THREE.TextureLoader().load('./js/fire.png');
fireTex.magFilter = fireTex.minFilter = THREE.LinearFilter
fireTex.wrapS = fireTex.wrapT = THREE.ClampToEdgeWrapping

let fireMaterial = new THREE.ShaderMaterial({
  defines: {
    ITERATIONS: '20',
    OCTAVES: '3',
  }, 
    transparent: true,
      depthWrite: false,
      depthTest: false,
    uniforms:  {
        fireTex: { value: fireTex },//Texture | null },//** Fire texture (grayscale mask)
        color: { value: new THREE.Color(0xeeeeee)},//Color }Fire color tint */
        time: { value: 0},//number Current time for animation
        seed: { value: Math.random() * 19.19},//number  Random seed for fire variation
        invModelMatrix: { value: new THREE.Matrix4() },//Matrix4 Inverse model matrix for ray marching
        scale: { value: new THREE.Vector3(1,1,1) },//Vector3 Scale of the fire object
        noiseScale: { value: new THREE.Vector4(1, 2, 1, 0.3)  },//Vector4 Noise scaling parameters [x, y, z, time]
        magnitude: { value: 1.3 },//number Fire shape intensity
        lacunarity: { value: 2 },//number Noise lacunarity (frequency multiplier)
        gain: { value: .5 },//number Noise gain (amplitude multiplier)
    },vertexShader: /* glsl */ `
    varying vec3 vWorldPos;
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
    }
  `,
    fragmentShader: `
    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;

      //col = vec4(1.);
      gl_FragColor = col;
    }
`
});

let fireBox = new THREE.Mesh(new THREE.BoxGeometry(),fireMaterial);
fireBox.onBeforeRender=function(){
    this.material.uniforms.time.value = performance.now()/1000;
    this.updateMatrixWorld()
    this.material.uniforms.invModelMatrix.value.copy(this.matrixWorld).invert()
    this.material.uniforms.scale.value.copy(this.scale)
}

export default fireBox;

Create it like this:

import fireBox from “./fire.js”
scene.add(fireBox);

… as I suspected, if the camera gets too close to it, the performance goes off a cliff proportional to screenspace coverage.
It’s a cool effect, but not really general purpose enough to be a mainstream component. Might be fine as a sample tho.. idk.

3 Likes

up close: 24fps

a little further out: 60fps

Check it out here:

5 Likes

I started my project with TSL and WebGPU, and I cannot mix with GLSL (correct me if I’m wrong) and it’s too late to go back. My only option is to get the TSL migrated.

GLSL (OpenGL Shading Language) cannot be directly used in the WebGPURenderer in Three.js.
WebGPU uses its own shading language called WGSL (WebGPU Shading Language), which is distinct from GLSL. While Three.js's WebGPURenderer is designed to leverage WebGPU's capabilities, it requires shaders written in WGSL.
However, Three.js is developing a new shading language called TSL (Three.js Shading Language) which is a node-based JavaScript shader abstraction. TSL allows you to write shaders using JavaScript nodes, and it can then generate either GLSL (for WebGL) or WGSL (for WebGPU) depending on the renderer in use. This provides a way to create shaders that are compatible with both WebGL and WebGPU within the Three.js ecosystem. Additionally, there are efforts to improve TSL's transpiler to convert existing GLSL code into TSL, making it potentially usable with WebGPU.

… as I suspected, if the camera gets too close to it, the performance goes off a cliff proportional to screenspace coverage.

Thanks for the feedback on the performance concern, while I believe my project is pretty lightweight and should be fine, I will note that for next step if I somehow get it working in TSL.

1 Like

This topic was automatically closed after 30 days. New replies are no longer allowed.