www.xbdev.net
xbdev - software development
Tuesday April 29, 2025
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


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 timestep  = new Float32Array( [0.0] );
const 
timerUniformBuffer device.createBuffer({ sizetimestep.byteLengthusageGPUBufferUsage.UNIFORM GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(timerUniformBuffer,  0,  timestep  );

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

const mouse  = new Float32Array( [900.0900.0 ] );
const 
mouseBuffer device.createBuffer({ sizemouse.byteLengthusageGPUBufferUsage.UNIFORM GPUBufferUsage.COPY_DST });
device.queue.writeBuffermouseBuffer,  0,  mouse );

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

function Particlex) {
    
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.appendChildcanvas );
canvas.id "canvas1";
canvas.width  1024;
canvas.height 512;
const 
ctx canvas.getContext("2d");

ctx.font 'bold 18px Verdana';
ctx.fillText('XBDEV'020 );

const 
imgData ctx.getImageData(00canvas.widthcanvas.height);

console.log'canvas:'canvas.widthcanvas.height );

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

let particleSpacing 15;
let particles   = [];

for (var 
0y2 canvas.heighty2y++) {
  for (var 
0x2 canvas.widthx2x++) {
    if (
imgData.data[(canvas.width) + (4) + 3] > 128) {
      
let positionX x;
      
let positionY y;

      
particles.push(new Particle(positionX particleSpacingpositionY particleSpacing));      
    }
  }
}

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

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

const 
particleArray = new Float32ArrayparticlesTmp );

const 
particleStride particleArray.length particles.length;

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


const 
particleBuffer device.createBuffer({ sizeparticleArray.byteLengthusageGPUBufferUsage.STORAGE GPUBufferUsage.COPY_DST  GPUBufferUsage.COPY_SRC });
device.queue.writeBuffer(particleBuffer0particleArray);

const 
particleBufferTmp device.createBuffer({ sizeparticleArray.byteLengthusageGPUBufferUsage.COPY_DST GPUBufferUsage.MAP_READ });

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

const GCOMPUTE GPUShaderStage.COMPUTE;

// Bind group layout and bind group
const bindGroupLayout device.createBindGroupLayout({
  
entries: [ {binding0visibilityGCOMPUTE,  buffer: { type"uniform"  }  },
             {
binding1visibilityGCOMPUTE,  buffer: { type"uniform"  }  },
             {
binding2visibilityGCOMPUTE,  buffer: { type"storage"  }  }
           ]
});

const 
bindGroup0 device.createBindGroup({
    
layoutbindGroupLayout,
    
entries: [  {   binding0,  resource: { buffertimerUniformBuffer  } },
                {   
binding1,  resource: { buffermouseBuffer         } },
                {   
binding2,  resource: { bufferparticleBuffer      } }
    ]
});


  
// 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( )
{
  
mouse[0] = e.canvas.clientLeft/2;
  
mouse[1] = e.canvas.clientTop/2;
}

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

  
device.queue.writeBuffermouseBuffer,  0,  mouse );

  
// 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.dispatchWorkgroupsparticles.length/8);
  
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.fromparticleArrayTmp );
  
particleBufferTmp.unmap();
  
  
// ----------------------

  
ctx.clearRect(0,0canvas.widthcanvas.height );
  
  for (
let i=0i<particles.lengthi++)
  {
      
let particle particles[i];
    
      
let newx particleArrayOutparticleStride i  0];
      
let newy particleArrayOutparticleStride i  1];
    
      
particles[i].newx;
      
particles[i].newy;
    
      
particle.newx;
      
particle.newy;
    
   
      if (
particle.random 0.05)
      {
        
ctx.fillStyle 'black';
        
ctx.beginPath();
        
ctx.arc(particle.xparticle.yparticle.size0Math.PI 2);
        
ctx.closePath();
        
ctx.fill();
      }
      else 
      {
        
ctx.save();
        
ctx.translate(particle.xparticle.y);
        
ctx.rotate(particle.angle);
        
ctx.drawImage(sprite
                      
particle.frameX 213.3
                      
particle.frameY 213.3213.3,213.3,
                      
particle.spriteSize/2
                      
particle.spriteSize/2particle.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 0particles.lengtha++) {
        for (
let b aparticles.lengthb++) {
            
let distance = (( particles[a].particles[b].x) * (particles[a].particles[b].x))
            + ((
particles[a].particles[b].y) * (particles[a].particles[b].y));
            
            const 
maxdist 2000;
            if (
distance maxdist) {
                
opacityValue - (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].xparticles[a].y);
                
ctx.lineTo(particles[b].xparticles[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-2025 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.