www.xbdev.net
xbdev - software development
Friday June 27, 2025
Home | Contact | Support | WebGPU Graphics and Compute ... | WebGPU 'Compute'.. Compute, Algorithms, and Code.....
     
 

WebGPU 'Compute'..

Compute, Algorithms, and Code.....

 

Fire Effect (Animated)


Simple but catchy fire effect using a scrolling texture density. The density is mapped to a color pattern for the fire.


Animated 2d fire effect - example of what the output looks like.
Animated 2d fire effect - example of what the output looks like.


Functions Used: requestAdapter(), getPreferredCanvasFormat(), createCommandEncoder(), beginRenderPass(), setPipeline(), draw(), end(), submit(), getCurrentTexture(), createView(), createShaderModule()

The implementation demo uses a double buffering (buf0 and buf1) which are 'ping-pong' back and forth with the density. During the update one is used as the read and the other write.

After each update the data is written to a texture storage output buffer which is copied to the canvas output.


Complete Code


let div document.createElement('div');
document.body.appendChilddiv );
div.style['font-size'] = '20pt';
function 
log)
{
  
console.log);
  
let args = [...arguments].join(' ');
  
div.innerHTML += args '<br><br>';
}

log('WebGPU Compute Example');


if (!
navigator.gpu) { log("WebGPU is not supported (or is it disabled? flags/settings)"); return; }

const 
adapter await navigator.gpu.requestAdapter();
const 
device  await adapter.requestDevice();

const 
imgWidth 256;
const 
imgHeight imgWidth;

const 
bufferSize imgWidth imgHeight;

// ----------------------------------------------------------

// Basic canvas which will be used to display the output from the compute shader

let canvasa document.createElement('canvas');
document.body.appendChildcanvasa ); canvasa.height canvasa.width imgWidth;
const 
context canvasa.getContext('webgpu');
const 
presentationFormat navigator.gpu.getPreferredCanvasFormat(); 
console.log('presentationFormat:'presentationFormat );

context.configure({ devicedevice
                    
usageGPUTextureUsage.RENDER_ATTACHMENT GPUTextureUsage.COPY_SRC GPUTextureUsage.COPY_DST,
                    
format"rgba8unorm" /*presentationFormat*/  });

let canvasTexture context.getCurrentTexture();

// ----------------------------------------------------------

const texture1 device.createTexture({
  
size: [imgWidthimgHeight1],
  
format"rgba8unorm",
  
usageGPUTextureUsage.COPY_DST GPUTextureUsage.COPY_SRC GPUTextureUsage.TEXTURE_BINDING GPUTextureUsage.STORAGE_BINDING
});

// ----------------------------------------------------------

const timerUniformBuffer device.createBuffer({
  
size4
  
usageGPUBufferUsage.UNIFORM GPUBufferUsage.COPY_DST
});

const 
timestep  = new Float32Array( [0.0] );

device.queue.writeBuffer(timerUniformBuffer,   0timestep             );

// ----------------------------------------------------------

function Colour(rgb) {
    
this.r;
    
this.g;
    
this.b;
    
this.255;
}

let colors = [];

for (
let i 0256i++) {
  
colors[i] = new Colour(000);
}

for (
let i 032; ++i) {
  
// black to blue
  
colors[i].<< 1;

  
// blue to red
  
colors[32].<< 3;
  
colors[32].64 - (<< 1);

  
// red to yellow
  
colors[64].255;
  
colors[64].<< 3;

  
// yellow to white
  
colors[96].255;
  
colors[96].255;
  
colors[96].<< 2;
  
colors[128].255;
  
colors[128].255;
  
colors[128].64 + (<< 2);
  
colors[160].255;
  
colors[160].255;
  
colors[160].128 + (<< 2);
  
colors[192].255;
  
colors[192].255;
  
colors[192].192 i;
  
colors[224].255;
  
colors[224].255;
  
colors[224].224 i;
}

colors colors.map( (a)=>{ return Object.values(a); } ).flat();

const 
colorArray = new Float32Arraycolors ); // vec4<f32>

const colorBuffer device.createBuffer({ sizecolorArray.byteLengthusageGPUBufferUsage.STORAGE GPUBufferUsage.COPY_DST  GPUBufferUsage.COPY_SRC });
device.queue.writeBuffer(colorBuffer0colorArray);

// ----------------------------------------------------------

const bufArray0 = new Float32ArrayimgWidth imgHeight ); 

const 
bufBuffer0 device.createBuffer({ sizebufArray0.byteLengthusageGPUBufferUsage.STORAGE GPUBufferUsage.COPY_DST  GPUBufferUsage.COPY_SRC });
device.queue.writeBuffer(bufBuffer00colorArray);


const 
bufBuffer1 device.createBuffer({ sizebufArray0.byteLengthusageGPUBufferUsage.STORAGE GPUBufferUsage.COPY_DST  GPUBufferUsage.COPY_SRC });
device.queue.writeBuffer(bufBuffer10colorArray);

// ----------------------------------------------------------

const GCOMPUTE GPUShaderStage.COMPUTE;

// Bind group layout and bind group
const bindGroupLayout device.createBindGroupLayout({
  
entries: [ {binding0visibilityGCOMPUTEbuffer:  { type"uniform"  }   },
             {
binding1visibilityGPUShaderStage.COMPUTEbuffer: {type"read-only-storage"}  },
             {
binding2visibilityGPUShaderStage.COMPUTEbuffer: {type"storage"}  },
             {
binding3visibilityGPUShaderStage.COMPUTEbuffer: {type"storage"}  },
             {
binding4visibilityGCOMPUTEstorageTexture: {format:"rgba8unorm"access:"write-only"viewDimension:"2d"}   }
           ]
});

const 
bindGroup0 device.createBindGroup({
    
layoutbindGroupLayout,
    
entries: [  {   binding0,  resource: { buffertimerUniformBuffer  } },
                  {   
binding1,  resource: { buffercolorBuffer         } },
                {   
binding2,  resource: { bufferbufBuffer0          } }, // swap buf0/1
                
{   binding3,  resource: { bufferbufBuffer1          } },
                {   
binding4,  resourcetexture1.createView()           }
    ]
});

const 
bindGroup1 device.createBindGroup({
    
layoutbindGroupLayout,
    
entries: [  {   binding0,  resource: { buffertimerUniformBuffer  } },
                  {   
binding1,  resource: { buffercolorBuffer         } },
                {   
binding2,  resource: { bufferbufBuffer1          } }, // swap buf0/1
                
{   binding3,  resource: { bufferbufBuffer0          } },
                {   
binding4,  resourcetexture1.createView()           }
    ]
});

  
// Compute shader code
const computeShader = ` 
@group(0) @binding(0) var<uniform>             mytimer  : f32; // timer increments each frame
@group(0) @binding(1) var<storage, read>       mycolors : array< vec4<f32>, 256 >;
@group(0) @binding(2) var<storage, read_write> buf0     : array< f32, 
${bufArray0.length} >; // width x height
@group(0) @binding(3) var<storage, read_write> buf1     : array< f32, 
${bufArray0.length} >; // width x height
@group(0) @binding(4) var myTexture1: texture_storage_2d<rgba8unorm, write>; // output image

const imgWidth  = u32( 
${imgWidth}  ); 
const imgHeight = u32( 
${imgHeight} ); 
//const imgSize = vec2<f32>(imgWidth, imgHeight);
    
// Simple random number generator based on the index
fn rand(seed: u32) -> f32 {
    var x = seed * 1234567u + 987654321u;
    x = (x >> 16u) ^ x;
    x = x * 1103515245u + 12345u;
    return f32(x & 0x7fffffffu) / f32(0x7fffffffu);
}

fn random(uv: vec2<f32>) -> f32 {
    return fract(sin(dot(uv, vec2<f32>(12.9898, 78.233))) * 43758.5453);
}

@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) globalId      : vec3<u32>,
        @builtin(local_invocation_id)  localId       : vec3<u32>,
        @builtin(workgroup_id)         workgroupId   : vec3<u32>,
        @builtin(num_workgroups)       workgroupSize : vec3<u32>
        ) 
{
    var coords = vec2<u32>(globalId.xy);

    if (coords.x >= imgWidth || coords.y >= imgHeight) {
        return;
    }

    let index = coords.y * imgWidth + coords.x;
    
    // Simulate fire effect
    if (coords.y == imgHeight - 1) 
    {
        // Randomize the bottom row of the fire array
        if ( random( vec2<f32>(f32(index)/1024.0, mytimer*0.1 ) ) > 0.5) {
            buf1[index] = 255.0;
        } else {
            buf1[index] = 0;
        }
    } 
    else 
    {
        // Move fire upwards
        var temp = f32(buf0[index]);
        temp    += f32(buf0[index + 1]);
        temp    += f32(buf0[index - 1]);
        temp    += f32(buf0[index + imgWidth]);
        temp /= 3.975;

        if (temp > 1.0) {
            temp -= 1.0;
        }

        buf1[index] = f32(clamp(temp, 0.0, 255.0));
    }

    var finalcolor = mycolors[ u32( buf0[ index ] ) ];
    
    finalcolor *= 1.0/255;
    finalcolor.w = 1.0;
    
    // Store the result in the output texture
    textureStore(myTexture1, vec2<i32>(globalId.xy), finalcolor);
}
`;
  

// Pipeline setup
const computePipeline device.createComputePipeline({
    
layout :   device.createPipelineLayout({bindGroupLayouts: [bindGroupLayout]}),
    
compute: { module    device.createShaderModule({code:computeShader}),
               
entryPoint"main" }
});


async function frame()
{
  
timestep[0] = timestep[0] + 0.01;
  
device.queue.writeBuffer(timerUniformBuffer,   0timestep             );


  
// Commands submission
  
const commandEncoder device.createCommandEncoder();

  {
  const 
passEncoder commandEncoder.beginComputePass();
  
passEncoder.setPipeline(computePipeline);
  
passEncoder.setBindGroup(0bindGroup0);
  
// workgroup size of 8x8 on the wgsl shader
  
passEncoder.dispatchWorkgroupsimgWidth/8imgWidth/);
  
await passEncoder.end();
  }

  {
  const 
passEncoder commandEncoder.beginComputePass();
  
passEncoder.setPipeline(computePipeline);
  
passEncoder.setBindGroup(0bindGroup1);
  
// workgroup size of 8x8 on the wgsl shader
  
passEncoder.dispatchWorkgroupsimgWidth/8imgWidth/);
  
await passEncoder.end();
  }

  
canvasTexture context.getCurrentTexture();

  
await
  commandEncoder
.copyTextureToTexture( { texturetexture1 },
                                      { 
texturecanvasTexture },
                                      { 
width:imgWidthheight:imgHeightdepthOrArrayLayers:1} );

  
// Submit GPU commands.
  
const gpuCommands commandEncoder.finish();
  
await device.queue.submit([gpuCommands]);
  
  
requestAnimationFrame(frame);
}

frame();



Animated 2d fire effect - fire moves up slowly - generated on the bottom row of pixels.
Animated 2d fire effect - fire moves up slowly - generated on the bottom row of pixels.


Things to Try


• Instead of adding the random emitter for the fire at the bottom (emit it in a circular shape in the middle of the screen)
• Experiment with different noise and color patterns
• Adding noise to the direction (not just limited to up - but wiggles left/right more)



Resources and Links


• WebGPU Lab Demo [LINK]

• Notebook Demo [LINK]





WebGPU by Example: Fractals, Image Effects, Ray-Tracing, Procedural Geometry, 2D/3D, Particles, Simulations WebGPU Compute graphics and animations using the webgpu api 12 week course kenwright learn webgpu api kenwright programming compute and graphics applications with html5 and webgpu api kenwright real-time 3d graphics with webgpu kenwright webgpu api develompent a quick start guide kenwright webgpu by example 2022 kenwright webgpu gems kenwright webgpu interactive compute and graphics visualization cookbook kenwright wgsl webgpu shading language cookbook kenwright wgsl webgpugems shading language cookbook kenwright



 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2025 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.