www.xbdev.net
xbdev - software development
Thursday April 30, 2026
Home | Contact | Support | WebGPU Graphics and Compute ... | WebGPU 'Compute'.. Compute, Algorithms, and Code.....
     
 

WebGPU 'Compute'..

Compute, Algorithms, and Code.....

 

Fluid Dynamics


Emulate the visual characteristics of fluids and smoke using multiple buffers on the compute shader.


Simple fluid dynamics demo using a start pattern with words.
Simple fluid dynamics demo using a start pattern with words.


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

So we can distribute the fluid dynamic calculations across the GPU threads, we use a double buffering approach for the density and velocity arrays.

We implement the diffusion, advect and curl mechanisms.

• Diffusion - Process by which molecules in a fluid move from regions of higher concentration to regions of lower concentration, resulting in a more uniform distribution. This movement occurs due to random thermal motion, leading to the gradual mixing of substances within the fluid. Acts as a mechanism for the redistribution of mass, energy, and momentum, playing a crucial role in phenomena such as mass transport, heat conduction, and the dissipation of vortices within fluid systems.

• Advect - The advect step is a computational process where the fluid properties, such as velocity, temperature, or concentration of a substance, are transported through the flow field according to the local velocity field. This step involves calculating the movement of fluid parcels over small time intervals, typically using numerical methods like finite difference or finite volume schemes. The advect step is essential for accurately simulating the advection of quantities within the fluid domain, capturing phenomena such as fluid mixing, dispersion, and transport of properties like heat or chemical species.

• Curl - The curl is a mathematical operation that describes the rotation or vorticity of a vector field. It quantifies the tendency of the field to circulate around a point or axis, indicating the local spinning motion within the field. The curl of a vector field is a vector quantity obtained by taking the cross product of the gradient operator with the vector field itself. It's particularly useful for understanding and analyzing the presence of vortices, eddies, and rotational flow patterns within a fluid, providing insights into the dynamics of fluid motion and turbulence.




Complete Code


<?php
let div = document.createElement('div');
document.body.appendChild( div );
div.style['font-size'] = '20pt';
function log( s )
{
  console.log( s );
  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();


let canvas = document.createElement('canvas');
document.body.appendChild( canvas ); canvas.height = canvas.width = 500;

const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); 
console.log('presentationFormat:', presentationFormat );


context.configure({ device: device, 
                    usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
                    format: "rgba8unorm" /*presentationFormat*/  });

let canvasTexture = context.getCurrentTexture();

let imgWidth  = canvas.width;
let imgHeight = imgWidth;


const texture1 = device.createTexture({
  size: [imgWidth, imgHeight, 1],
  format: "rgba8unorm",
  usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING
});

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

const bufferSize = imgWidth * imgHeight * 4 * 2; // for vec2<f32>

const velocityBuffer0 = device.createBuffer({ size: bufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST });

const velocityBuffer1 = device.createBuffer({ size: bufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST });

const densityBuffer0 = device.createBuffer({ size: bufferSize / 2,  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST });

const densityBuffer1 = device.createBuffer({ size: bufferSize / 2,  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST });

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

/*
// Circle density test pattern
function createDensityPattern() {
  // Create density data with test pattern
  const density = new Float32Array(imgWidth * imgWidth);
  const size = imgWidth;
  const densityData = new Float32Array(size * size);
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      // Example test pattern (e.g., a circular density)
      const centerX = size / 2;
      const centerY = size / 2;
      const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
      const densityValue = distance < 50 ? 1.0 : 0.0; // Adjust threshold and shape as needed
      density[y * size + x] = densityValue * Math.random() * 100.0;
    }
  }
  return density;
}
*/

function createDensityPattern() {
  // Create temporary canvas to draw the text
  const canvas = document.createElement('canvas');
  canvas.width = imgWidth;
  canvas.height = imgWidth;
  const ctx = canvas.getContext('2d');

  // Set font and text properties
  ctx.font = 'bold 150px Arial';
  ctx.fillStyle = 'white';

  // Draw the text 'cat' onto the canvas
  ctx.fillText('xbdev', 30, 250);

  // Extract pixel data from the canvas
  const imageData = ctx.getImageData(0, 0, imgWidth, imgWidth);
  const pixels = imageData.data;

  // Convert pixel data to density array
  const density = new Float32Array(imgWidth * imgWidth);
  for (let i = 0; i < pixels.length; i += 4) {
    // Convert RGBA pixel values to grayscale
    const grayscale = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
    // Normalize grayscale value to range [0, 1]
    const densityValue = grayscale / 255;
    density[i / 4] = densityValue;
  }

  return density;
}

const initialDensityData = createDensityPattern();

device.queue.writeBuffer(densityBuffer0, 0, initialDensityData);
device.queue.writeBuffer(densityBuffer1, 0, initialDensityData);

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

function createVortexPattern() {
  const data = new Float32Array(imgWidth * imgWidth * 2);
  const centerX = imgWidth / 2;
  const centerY = imgWidth / 2;
  const strength = 10.0;
  for (let y = 0; y < imgWidth; y++) {
    for (let x = 0; x < imgWidth; x++) {
      const dx = x - centerX;
      const dy = y - centerY;
      const distance = Math.sqrt(dx * dx + dy * dy);
      const angle = Math.atan2(dy, dx);
      const index = 2 * (y * imgWidth + x);
     // data[index + 0] = -strength * Math.sin(angle) * Math.exp(-distance / 100);
     // data[index + 1] =  strength * Math.cos(angle) * Math.exp(-distance / 100);
      data[index + 0 ] = -10.0 + Math.random() * 20.0;
      data[index + 1 ] = -10.0 + Math.random() * 20.0;
    }
  }
  return data;
}

const initialVelocityData = createVortexPattern();

device.queue.writeBuffer(velocityBuffer0, 0, initialVelocityData);
device.queue.writeBuffer(velocityBuffer1, 0, initialVelocityData);

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

const timerUniformBuffer = device.createBuffer({
  size: 4, 
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});

const timestep  = new Float32Array( [0.0] );

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

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


// Bind group layout and bind group
const bindGroupLayout = device.createBindGroupLayout({
  entries: [  
    {binding: 0, visibility: GPUShaderStage.COMPUTE,  buffer: { type: "uniform" }   },
    {binding: 1, visibility: GPUShaderStage.COMPUTE,  buffer: { type: "storage"  }  },
    {binding: 2, visibility: GPUShaderStage.COMPUTE,  buffer: { type: "storage"  }  },
    {binding: 3, visibility: GPUShaderStage.COMPUTE,  buffer: { type: "storage"  }  },
    {binding: 4, visibility: GPUShaderStage.COMPUTE,  buffer: { type: "storage"  }  },
    {binding: 5, visibility: GPUShaderStage.COMPUTE,  storageTexture: {format:"rgba8unorm", access:"write-only", viewDimension:"2d"}   }
           ]
});

const bindGroup0 = device.createBindGroup({ layout: bindGroupLayout,
                                            entries: [  {  binding: 0,  resource: { buffer: timerUniformBuffer} },
                                                       {  binding: 1,  resource: { buffer: velocityBuffer0 } },
                                                       {  binding: 2,  resource: { buffer: velocityBuffer1 } },
                                                       {  binding: 3,  resource: { buffer: densityBuffer0 } },
                                                       {  binding: 4,  resource: { buffer: densityBuffer1 } },
                                                       {  binding: 5,  resource: texture1.createView()  },
                                                    ] });

const bindGroup1 = device.createBindGroup({ layout: bindGroupLayout,
                                            entries: [  {  binding: 0,  resource: { buffer: timerUniformBuffer} },
                                                       {  binding: 1,  resource: { buffer: velocityBuffer1 } },
                                                       {  binding: 2,  resource: { buffer: velocityBuffer0 } },
                                                       {  binding: 3,  resource: { buffer: densityBuffer1 } },
                                                       {  binding: 4,  resource: { buffer: densityBuffer0 } },
                                                       {  binding: 5,  resource: texture1.createView()  },
                                                    ] });


// Compute shader code
const computeShader = ` 
@binding(0) @group(0) var<uniform> mytimer : f32;
@binding(1) @group(0) var<storage, read_write> velocity0: array<vec2<f32>>;
@binding(2) @group(0) var<storage, read_write> velocity1: array<vec2<f32>>;
@binding(3) @group(0) var<storage, read_write> density0: array<f32>;
@binding(4) @group(0) var<storage, read_write> density1: array<f32>;
@binding(5) @group(0) var myTexture1: texture_storage_2d<rgba8unorm, write>;

// Scene parameters
const imgWidth:  u32 = ${canvas.width};
const imgHeight: u32 = ${canvas.width};
const gridSize:  i32 = ${canvas.width};

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

const dt: f32 = 0.1; // Time step
const diffusion: f32 = 0.1; // Diffusion coefficient
const vdiff : f32 = 0.1;
const curlCoefficient = 0.4;
const size: u32 = ${canvas.width}; // Simulation grid size

fn index(x: u32, y: u32) -> u32 {
    return y * size + x;
}

fn diffuseDensity( factor:f32, x:u32, y:u32 )
{
    let center = density0[index(x, y)];
    let left   = density0[index(max(x, 1u) - 1u, y)];
    let right  = density0[index(min(x + 1u, size - 1u), y)];
    let up     = density0[index(x, max(y, 1u) - 1u)];
    let down   = density0[index(x, min(y + 1u, size - 1u))];
    density1[index(x,y)] = ( center + factor * (left + right + up + down - 4.0 * center) );
}

fn diffuseVelocity( factor:f32, x:u32, y:u32 )
{
    let center = velocity0[index(x, y)];
    let left   = velocity0[index(max(x, 1u) - 1u, y)];
    let right  = velocity0[index(min(x + 1u, size - 1u), y)];
    let up     = velocity0[index(x, max(y, 1u) - 1u)];
    let down   = velocity0[index(x, min(y + 1u, size - 1u))];
    velocity1[index(x,y)] = ( center + factor * (left + right + up + down - 4.0 * center) );
}

@compute @workgroup_size(8, 8)
fn diffuse_step(@builtin(global_invocation_id) id: vec3<u32>) {
    let x = id.x;
    let y = id.y;
    if (x >= size || y >= size) {
        return;
    }

    diffuseDensity( diffusion, x, y );
    
    diffuseVelocity( vdiff, x, y );
}

@compute @workgroup_size(8, 8)
fn advect_step(@builtin(global_invocation_id) id: vec3<u32>) {
    let x = id.x;
    let y = id.y;
    if (x >= size || y >= size) {
        return;
    }

    // Simple advection
    let uv = velocity0[index(x, y)];
    let pos = vec2<f32>(f32(x), f32(y)) - dt * uv;
    let x0 = clamp(floor(pos.x), 0.0, f32(size - 1));
    let x1 = clamp(ceil(pos.x), 0.0, f32(size - 1));
    let y0 = clamp(floor(pos.y), 0.0, f32(size - 1));
    let y1 = clamp(ceil(pos.y), 0.0, f32(size - 1));
    let s1 = pos.x - x0;
    let s0 = 1.0 - s1;
    let t1 = pos.y - y0;
    let t0 = 1.0 - t1;
    let advectedDensity = s0 * (t0 * density0[index(u32(x0), u32(y0))] + t1 * density0[index(u32(x0), u32(y1))]) +
                          s1 * (t0 * density0[index(u32(x1), u32(y0))] + t1 * density0[index(u32(x1), u32(y1))]);
    density1[index(x, y)] = advectedDensity;
}


@compute @workgroup_size(8, 8)
fn curl_step(@builtin(global_invocation_id) id: vec3<u32>) {
    let x = id.x;
    let y = id.y;
    if (x >= size || y >= size) {
        return;
    }

    
    let du_dy = ( velocity0[index(x    ,y + 1)].x  - velocity0[index(x    ,y - 1)].x ) / (2.0);
    let dv_dx = ( velocity0[index(x + 1,y    )].y  - velocity0[index(x - 1,y    )].y ) / (2.0);
    let curl = dv_dx - du_dy;
    
    velocity1[index(x,y)].x += -curl * dt * curlCoefficient;
    velocity1[index(x,y)].y +=  curl * dt * curlCoefficient;

}

@compute @workgroup_size(8, 8)
fn display(@builtin(global_invocation_id) globalId: vec3<u32>) {
    let x = globalId.x;
    let y = globalId.y;
    if (x >= imgWidth || y >= imgHeight) {
        return;
    }

    let d = density0[index(x, y)];
    var finalColor = vec4<f32>(d, d, d, 1.0);
    textureStore(myTexture1, vec2<i32>(globalId.xy), finalColor);
}


`;
  

let shaderModule = device.createShaderModule({code:computeShader});
// Pipeline setup

var pipelines = {};
function addPipeline( pipelineName )
{
    let p = device.createComputePipeline({
              layout :   device.createPipelineLayout({bindGroupLayouts: [bindGroupLayout]}),
              compute: { module: shaderModule,
                         entryPoint: pipelineName }
    });
    pipelines[ pipelineName ] = p;
  
}

addPipeline( 'diffuse_step' );
addPipeline( 'advect_step' );
addPipeline( 'curl_step' );
addPipeline( 'display' );

async function frame()
{
  // Commands submission
  const commandEncoder = device.createCommandEncoder();

  /*
  {
  const passEncoder = commandEncoder.beginComputePass();
  passEncoder.setPipeline(computePipeline);
  passEncoder.setBindGroup(0, bindGroup0);
  // workgroup size defined on the wgsl shader
  passEncoder.dispatchWorkgroups( imgWidth/8, imgWidth/8 );
  await passEncoder.end();
  }
  */
  async function computePass( theCommandEncoder, thePipeline, theBindGroup )
  {
    const passEncoder = theCommandEncoder.beginComputePass();
    passEncoder.setPipeline(thePipeline);
    passEncoder.setBindGroup(0, theBindGroup);
    // workgroup size defined on the wgsl shader
    passEncoder.dispatchWorkgroups( imgWidth/8, imgWidth/8 );
    await passEncoder.end();
  }
  
  await computePass( commandEncoder, pipelines['diffuse_step'], bindGroup0 );
  await computePass( commandEncoder, pipelines['diffuse_step'], bindGroup1 );
  
  await computePass( commandEncoder, pipelines['advect_step'], bindGroup0 );
  await computePass( commandEncoder, pipelines['advect_step'], bindGroup1 );
  
  await computePass( commandEncoder, pipelines['curl_step'], bindGroup0 );
  await computePass( commandEncoder, pipelines['curl_step'], bindGroup1 );
  


  await computePass( commandEncoder, pipelines['display'], bindGroup0 );
  
  
  canvasTexture = context.getCurrentTexture();
  
  await
  commandEncoder.copyTextureToTexture( { texture:texture1 },
                                       { texture: canvasTexture },
                                       { width:imgWidth, height:imgHeight, depthOrArrayLayers:1} );
                                        
  // Submit GPU commands.
  const gpuCommands = commandEncoder.finish();
  await device.queue.submit([gpuCommands]);
  
  timestep[0] = timestep[0] + 0.01;
  device.queue.writeBuffer(timerUniformBuffer,   0, timestep             );
  
  requestAnimationFrame(frame);
}


frame();




Things to Try


• Try other text and test patterns for the initial density
• Try different initial 'velocity' patterns
• Visualize the velocities information
• Use other colors for the density visualization (instead of black and white - use a color gradient such as blue to red)
• Add in other fluid characteristics (e.g., boyancy)



Resources and Links


• WebGPU Lab Demo [LINK]

• Smoke/Gas Page [LINK]

• Notebook Page [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-2026 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.