www.xbdev.net
xbdev - software development
Wednesday May 6, 2026
Home | Contact | Support | WebGPU Graphics and Compute ... | WebGPU 'Compute'.. Compute, Algorithms, and Code.....
     
 

WebGPU 'Compute'..

Compute, Algorithms, and Code.....

 

Web of Springs (Interactive Animation)


Particles connected using simple springs (penalty-based forces). The springs are connected to a fixed location (calculated at startup). As you move the cursor around the window the springs respond by moving out of the way.


Fun with springs - connected in a
Fun with springs - connected in a 'web'.


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






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();

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

const timestep  = new Float32Array( [0.0] );
const timerUniformBuffer = device.createBuffer({ size: timestep.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(timerUniformBuffer,  0,  timestep  );

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

const mouse  = new Float32Array( [900.0, 900.0 ] );
const mouseBuffer = device.createBuffer({ size: mouse.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer( mouseBuffer,  0,  mouse );

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

function Particle( x, y ) {
    this.x          = x;
    this.y          = y;
    this.xBase      = x;
    this.yBase      = y;
    this.size       = 3,
    this.density    = ((Math.random() * 30) + 1);
    this.random     = Math.random();
    this.spriteSize = Math.random() * 50 + 50;
    this.frameX     = Math.floor(Math.random() * 3);
    this.frameY     = Math.floor(Math.random() * 8);
    this.angle      = Math.random() * 2; 
    this.pad0        = 0;
    this.pad1        = 0;
    this.pad2        = 0;
    this.pad3        = 0;
    this.pad4        = 0;
}

const canvas = document.createElement('canvas');
canvas.style.border = '1px solid orange';
document.body.appendChild( canvas );
canvas.id = "canvas1";
canvas.width  = 1024;
canvas.height = 512;
const ctx = canvas.getContext("2d");

ctx.font = 'bold 18px Verdana';
ctx.fillText('XBDEV', 0, 20 );

const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

console.log( 'canvas:', canvas.width, canvas.height );

const sprite = new Image();
sprite.src = 'https://xbdev.net/retro/code/games/images/spiders.png';

let particleSpacing = 15;
let particles   = [];

for (var y = 0, y2 = canvas.height; y < y2; y++) {
  for (var x = 0, x2 = canvas.width; x < x2; x++) {
    if (imgData.data[(y * 4 * canvas.width) + (x * 4) + 3] > 128) {
      let positionX = x;
      let positionY = y;

      particles.push(new Particle(positionX * particleSpacing, positionY * particleSpacing));      
    }
  }
}

console.log('number particles:', particles.length );

let particlesTmp = particles.map( (a)=>{ return Object.values(a); } ).flat();

const particleArray = new Float32Array( particlesTmp );

const particleStride = particleArray.length / particles.length;

console.log('particle stride:', particleStride );
console.log('number particles:', particles.length, particleArray.length );
console.log('length buffer in bytes:', particleArray.byteLength );


const particleBuffer = device.createBuffer({ size: particleArray.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST  | GPUBufferUsage.COPY_SRC });
device.queue.writeBuffer(particleBuffer, 0, particleArray);

const particleBufferTmp = device.createBuffer({ size: particleArray.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });

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

const GCOMPUTE = GPUShaderStage.COMPUTE;

// Bind group layout and bind group
const bindGroupLayout = device.createBindGroupLayout({
  entries: [ {binding: 0, visibility: GCOMPUTE,  buffer: { type: "uniform"  }  },
             {binding: 1, visibility: GCOMPUTE,  buffer: { type: "uniform"  }  },
             {binding: 2, visibility: GCOMPUTE,  buffer: { type: "storage"  }  }
           ]
});

const bindGroup0 = device.createBindGroup({
    layout: bindGroupLayout,
    entries: [  {   binding: 0,  resource: { buffer: timerUniformBuffer  } },
                {   binding: 1,  resource: { buffer: mouseBuffer         } },
                {   binding: 2,  resource: { buffer: particleBuffer      } }
    ]
});


  
// Compute shader code
const computeShader = ` 

struct Particle 
{
    @align(4) @size(4)  x       : f32,
    @align(4) @size(4)  y       : f32,  
    @align(4) @size(4)  baseX   : f32,  
    @align(4) @size(4)  baseY   : f32,  
    @align(4) @size(4)  size    : f32,  
    @align(4) @size(4)  density : f32,  
    @align(4) @size(4)  random  : f32,
    @align(4) pad : array< f32, 9 >
}

@group(0) @binding(0) var<uniform>             mytimer   : f32;       // timer increments each frame
@group(0) @binding(1) var<uniform>             mymouse   : vec2<f32>; // oouse coordinates
@group(0) @binding(2) var<storage, read_write> particles : array< Particle, ${particles.length} >; 


@compute @workgroup_size( 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>
        ) 
{
    let index = globalId.x;

    const mouseRadius = 150.0;

    var dx = mymouse.x - particles[index].x;
    var dy = mymouse.y - particles[index].y;

    var distance = sqrt(dx*dx + dy*dy);
    var forceDirectionX = dx / distance;
    var forceDirectionY = dy / distance;
    // distance past which the force is zero
    var maxDistance = mouseRadius;
    var force = (maxDistance - distance) / maxDistance;

    // if we went below zero, set it to zero.
    if (force < 0) { force = 0; }

    var directionX = (forceDirectionX * force * particles[index].density);
    var directionY = (forceDirectionY * force * particles[index].density);

    if (distance < mouseRadius + particles[index].size){
        particles[index].x -= directionX;
        particles[index].y -= directionY;
    } else {
        if (particles[index].x != particles[index].baseX ) {
            let dx = particles[index].x - particles[index].baseX;
            particles[index].x -= dx/10;
        } if (particles[index].y != particles[index].baseY) {
            let dy = particles[index].y - particles[index].baseY;
            particles[index].y -= dy/10;
        }
    }

}
`;
  

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


document.onmousemove = function( e )
{
  mouse[0] = e.x + canvas.clientLeft/2;
  mouse[1] = e.y + canvas.clientTop/2;
}

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

  device.queue.writeBuffer( mouseBuffer,  0,  mouse );

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

  {
  const passEncoder = commandEncoder.beginComputePass();
  passEncoder.setPipeline(computePipeline);
  passEncoder.setBindGroup(0, bindGroup0);
  // workgroup size of 8x8 on the wgsl shader
  passEncoder.dispatchWorkgroups( particles.length/8, 1 );
  await passEncoder.end();
  }
  
  // --------------------------
    
  commandEncoder.copyBufferToBuffer(particleBuffer, 
                                    0, 
                                    particleBufferTmp, 
                                    0, 
                                    particleArray.byteLength);
  
  const gpuCommands = commandEncoder.finish();
  await device.queue.submit([gpuCommands]);
  
  
  await particleBufferTmp.mapAsync(GPUMapMode.READ);
  const particleArrayTmp = new Float32Array(particleBufferTmp.getMappedRange());
  const particleArrayOut = Array.from( particleArrayTmp );
  particleBufferTmp.unmap();
  
  // ----------------------

  ctx.clearRect(0,0, canvas.width, canvas.height );
  
  for (let i=0; i<particles.length; i++)
  {
      let particle = particles[i];
    
      let newx = particleArrayOut[ particleStride * i  + 0];
      let newy = particleArrayOut[ particleStride * i  + 1];
    
      particles[i].x = newx;
      particles[i].y = newy;
    
      particle.x = newx;
      particle.y = newy;
    
   
      if (particle.random > 0.05)
      {
        ctx.fillStyle = 'black';
        ctx.beginPath();
        ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
        ctx.closePath();
        ctx.fill();
      }
      else 
      {
        ctx.save();
        ctx.translate(particle.x, particle.y);
        ctx.rotate(particle.angle);
        ctx.drawImage(sprite, 
                      particle.frameX * 213.3, 
                      particle.frameY * 213.3, 213.3,213.3,
                      0 - particle.spriteSize/2, 
                      0 - particle.spriteSize/2, particle.spriteSize, 
                      particle.spriteSize);
        ctx.restore();
      }
  }// end for i
  
  // ---------------------
  
  // Draw the links so it looks 'webby' (fits in with all the spider images)
  let mouseRadius = 150;
  let opacityValue = 1;
    for (let a = 0; a < particles.length; a++) {
        for (let b = a; b < particles.length; b++) {
            let distance = (( particles[a].x - particles[b].x) * (particles[a].x - particles[b].x))
            + ((particles[a].y - particles[b].y) * (particles[a].y - particles[b].y));
            
            const maxdist = 2000;
            if (distance < maxdist) {
                opacityValue = 1 - (distance/maxdist);
                let dx = mouse[0] - particles[a].x;
                let dy = mouse[1] - particles[a].y;
                let mouseDistance = Math.sqrt(dx*dx+dy*dy);
                if (mouseDistance < mouseRadius / 2) {
                  ctx.strokeStyle='rgba(0,0,0,' + opacityValue + ')';
                } else if (mouseDistance < mouseRadius - 50) {
                  ctx.strokeStyle='rgba(0,0,90,' + opacityValue + ')';
                } else if (mouseDistance < mouseRadius + 20) {
                  ctx.strokeStyle='rgba(0,0,50,' + opacityValue + ')';
                } else  {
                ctx.strokeStyle='rgba(0,0,0,' + opacityValue + ')';
                }
                ctx.lineWidth = 1;
                ctx.beginPath();
                ctx.moveTo(particles[a].x, particles[a].y);
                ctx.lineTo(particles[b].x, particles[b].y);
                ctx.stroke();
            }
        }
    
    }
  
  
  
  // ---------------------
  
  requestAnimationFrame(frame);
}

frame();




Things to Try


• Try other text and patterns
• Add other disturbances (e.g., rippling due to noises)
• Mix in more colors (particles change color based on velocity/penalty)




Resources and Links


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