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.....

 


Flocking and Boids


Define a large number of boids (simple agents) that move around. They have a position and a velocity. For the compute shader we create two arrays for double buffering. Input is written to the output then we swap the buffers and it repeats.


Flocking behaviour of boids flying around in 2d.
Flocking behaviour of boids flying around in 2d.


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

The behaviours are influenced using control forces. The three main forces are:

• Separation: Flocking force that encourages individuals to maintain distance from nearby neighbors, preventing overcrowding and collisions.
• Cohesion: Flocking force that draws individuals towards the center of the group, promoting unity and collective movement.
• Alignment: Flocking force that steers individuals to match the average direction of their neighbors' movement, ensuring synchronized motion within the group.


Flocking forces for separation, cohesion and alignment.
Flocking forces for separation, cohesion and alignment.



The values are initialized at the array creation. The position is 2d, so we only use the 'x' and 'y' values. The x and y value are constrained to 0 to 1. When we draw the values on screen we can scale the position to the size of the canvas.

Create the storage buffers using the following code.

// Helper function for random numbers
function rand(lohi)
{
   return 
lo Math.random() * (hi lo);
}

const 
FLOCK_SIZE 256;
let flock = [];

for (
let nn=0nn<FLOCK_SIZEnn++) 
{
    
let f = {};
    
f.= [ rand(0.0,1.0),    rand(0.0,1.0),    0]; // position;
    
f.= [ rand(-0.01,0.01), rand(-0.01,0.01), 0]; // velocity;
  
    
flock.pushObject.values).flat() );
}


const 
flockArray0 = new Float32Arrayflock.flat(3) );
const 
flockArray1 = new Float32Arrayflock.flat(3) );
const 
flockSize  = ( Object.values(flock[0]).flat(3) ).length;

const 
flockBuffer0 device.createBuffer({ sizeflockArray0.byteLengthusageGPUBufferUsage.STORAGE GPUBufferUsage.COPY_DST  GPUBufferUsage.COPY_SRC });
device.queue.writeBuffer(flockBuffer00flockArray0);

const 
flockBuffer1 device.createBuffer({ sizeflockArray1.byteLengthusageGPUBufferUsage.STORAGE GPUBufferUsage.COPY_DST GPUBufferUsage.COPY_SRC });
device.queue.writeBuffer(flockBuffer10flockArray1);

const 
flockBufferTmp device.createBuffer({ sizeflockArray1.byteLengthusageGPUBufferUsage.COPY_DST GPUBufferUsage.MAP_READ });



For visualization, we copy the array of boids and draw them to a HTML canvas. The position is scaled by the canvas width and height. The velocity is used as the direction for the void. An acute triangle is used so it has a sharp point - helps emphasis the direction the boid is heading.

// Extract the positions and velocities
const ctx canvas.getContext('2d');
ctx.clearRect(00canvas.widthcanvas.height);
for (var 
i=0i<outputArray.lengthi+=8)
{
    
// position
    
var px outputArray[i+0] * canvas.width;
    var 
py outputArray[i+1] * canvas.height;
    
    
// velocity
    
var vx outputArray[i+4];
    var 
vy outputArray[i+5];
    
    
// Set properties
    
const triangleSize 10// Size of the triangle
    
const color 'blue'// Color of the triangle
    
    // Calculate direction from velocity
    
var angle Math.atan2(vyvx);
    
    
// Calculate vertices of the triangle
    
var frontX px triangleSize Math.cos(angle);
    var 
frontY py triangleSize Math.sin(angle);
    var 
backLeftX px - (triangleSize 2) * Math.cos(angle Math.PI 6); // 30 degrees
    
var backLeftY py - (triangleSize 2) * Math.sin(angle Math.PI 6); // 30 degrees
    
var backRightX px - (triangleSize 2) * Math.cos(angle Math.PI 6); // 30 degrees
    
var backRightY py - (triangleSize 2) * Math.sin(angle Math.PI 6); // 30 degrees
    
    // Draw triangle
    
ctx.beginPath();
    
ctx.moveTo(frontXfrontY);
    
ctx.lineTo(backLeftXbackLeftY);
    
ctx.lineTo(backRightXbackRightY);
    
ctx.closePath();
    
ctx.fillStyle color;
    
ctx.fill();
}


For testing, initially you can reading the positions and velocities - making updates and passing the result onto the output in the WGSL compute shader.

struct FlockAgent {
    
vec4<f32>,
    
vec4<f32>
}
@
binding(0) @group(0) var<uniform>       mytimer   f32;
@
binding(1) @group(0) var<storage,read_writeflockIn:  array< FlockAgent , ${FLOCK_SIZE} >; 
@
binding(2) @group(0) var<storage,read_writeflockOut: array< FlockAgent , ${FLOCK_SIZE} >; 

// Update constant for timestep
const dt 0.1;

// Main compute shader function
@compute @workgroup_size(161)
fn 
main(
    @
builtin(global_invocation_idglobalIdvec3<u32>,
    @
builtin(local_invocation_idlocalIdvec3<u32>,
    @
builtin(workgroup_idworkgroupIdvec3<u32>,
    @
builtin(num_workgroupsworkgroupSizevec3<u32>
) {
    
// Read the current state of flock agent
    
var cp flockIn[globalId.x].p.xy;
    var 
cv flockIn[globalId.x].v.xy;
  
    
cp += cv dt;
    
    
// Write output state of flock agent to flockOut
    
flockOut[globalId.x].vec4<f32>( cp00);
    
flockOut[globalId.x].vec4<f32>( cv00);
}


The problem with the code is that the boids just move slowly and eventually all go off screen! To fix this, we 'wrap' the boids positions (so the wrap around to the opposite side when they go off screen).

The following adds a mod function which wraps the x and y coordinate for the position.

fn mymod(x:f32y:f32) -> f32
{
    return ( 
floor(x/y) );
}
...


// Repeat coordinates
cp vec2<f32>( mymod(cp.x1.0),  mymod(cp.y1.0) );


Debug Information


We'll define distances for the boids - defining if they're too close or if they are close enough to join a group (attraction/repulsion). As we're using 0 to 1 for the boundaries - it the values can be difficult to visualizate. So to make your life a bit easier - you can draw the distances as circles around each boid on the output.

In the compute shader and in the body of the code you can define some common parameters for the flocking/boids

// Flocking parameters
const dt 0.1// Simulation speed
const FLOCKSIZE:  u32 = ${FLOCK_SIZE};
const 
sightRadius = ${sightRadius}; // Boid sight radius
const minSep      = ${minSep};      // Boid minimum seperation
const speed       0.005;          // Boid movement speed 


In the draw loop, you can add a few extra lines to draw the diameters for the sight radius and the minimum seperation.

// debug - draw boundaries
const radius1 sightRadius canvas.width;
// Draw circle
ctx.beginPath();
ctx.arc(pxpyradius10Math.PI 2);
ctx.strokeStyle 'red';
ctx.stroke(); // Draw outline
ctx.closePath();

const 
radius2 minSep canvas.width;
// Draw circle
ctx.beginPath();
ctx.arc(pxpyradius20Math.PI 2);
ctx.strokeStyle 'orange';
ctx.stroke(); // Draw outline
ctx.closePath();



Drawing influence radius for each boid (left starting state while right shows the flocking behaviour after a few iterations).
Drawing influence radius for each boid (left starting state while right shows the flocking behaviour after a few iterations).



Iterate Over Each Boid


On the compute shader - each thread will perform the computation for each boid. So each boid will check all of its neighbours and determine what action it should take. It only updates its 'own' information.

Use the 'globalId.x' to identify the current boid in the array. Only using a single dimension array (so we only use the x dimension for the workgroup size).

1. For any neighbours within a tolerance distance we sum the total position and direction (velocity).
2. Any neighbour boids that are too close (within a tolerance - minSep), we apply a small 'push' to move it away.
3. After looping over all the neighbour voids we apply the correcting direction (velocity) so it moves with the influence of any neighbouring boids ('flock').

...
var 
avgDir vec2<f32>(0); // Average direction of all boids in the sight radius
var avgPos vec2<f32>(0); // Average position of all boids in the sight radius

var nb 0.0// Number of boids in sight of current boid

// Go through all voids
for (var u32 0uFLOCKSIZE1u
{
    if (
== globalId.x) { continue; }
    
    
let np flockIn[i].p.xy;
    
let nv flockIn[i].v.xy;
   
    var 
length(cp np); // Distance from this Boid to current Boid

    // This boid is in sight
    
if (<= sightRadius){
            
avgDir += nv;
            
avgPos += np;
            
nb += 1.0;
    }

    
// This boid is too close, push away
    
if (<= minSep){
        var 
dir normalize(cp np);
        
cp += dir minSep 0.01;
    }
}


// At least 1 boid in sight
if (nb 0.0){
    
avgPos /= nb;
    
cv normalize(avgDir) * speed// Set new velocity based on avg direction

    
var dir normalize(avgPos cp); // Move boid in direction of avg position of boids in sight
    
cv += dir 0.0005;
}
...


Bit of Noise


At this point, the simulation works fine and it looks good - but to make the motion more organic and to help make things flow more fluidly - we can add in a bit of 'random' noise into the mix.

Not much noise, if you add too much randomness, it will cause jittering and break the flocking effect. But a small amount of noise helps the boids 'intertwine' (flocking units better by adding a small wiggle causing the boids entangle in a synergstic motion - more like what you'd see in real flocking).

// Add some randomness to their direction
cv.+= 0.000082*cos(mytimer 555.);
cv.+= 0.000052*sin(mytimer*1.3 355.);




Complete Code


Let's put it all together and look at the complete compute shader for the flocking simluation.


struct FlockAgent {
    
vec4<f32>,
    
vec4<f32>
}
@
binding(0) @group(0) var<uniform>       mytimer   f32;
@
binding(1) @group(0) var<storage,read_writeflockIn:  array< FlockAgent , ${FLOCK_SIZE} >; 
@
binding(2) @group(0) var<storage,read_writeflockOut: array< FlockAgent , ${FLOCK_SIZE} >; 

fn 
mymod(x:f32y:f32) -> f32
{
    return ( 
floor(x/y) );
}

// Flocking parameters
const dt 0.1// Simulation speed
const FLOCKSIZE:  u32 = ${FLOCK_SIZE};
const 
sightRadius = ${sightRadius}; // Boid sight radius
const minSep      = ${minSep};      // Boid minimum seperation
const speed       0.005;          // Boid movement speed 


// Main compute shader function
@compute @workgroup_size(161)
fn 
main(
    @
builtin(global_invocation_idglobalIdvec3<u32>,
    @
builtin(local_invocation_idlocalIdvec3<u32>,
    @
builtin(workgroup_idworkgroupIdvec3<u32>,
    @
builtin(num_workgroupsworkgroupSizevec3<u32>
) {
    
// Read the current state of flock agent
    
var cp flockIn[globalId.x].p.xy;
    var 
cv flockIn[globalId.x].v.xy;
  
    
cp += cv dt;

    
// Repeat coordinates
    
cp vec2<f32>( mymod(cp.x1.0),  mymod(cp.y1.0) );

    var 
avgDir vec2<f32>(0); // Average direction of all boids in the sight radius
    
var avgPos vec2<f32>(0); // Average position of all boids in the sight radius

    
var nb 0.0// Number of boids in sight of current boid

    // Go through all voids
    
for (var u32 0uFLOCKSIZE1u
    {
        if (
== globalId.x) { continue; }
        
        
let np flockIn[i].p.xy;
        
let nv flockIn[i].v.xy;
       
        var 
length(cp np); // Distance from this Boid to current Boid

        // This boid is in sight
        
if (<= sightRadius){
                
avgDir += nv;
                
avgPos += np;
                
nb += 1.0;
        }

        
// This boid is too close, push away
        
if (<= minSep){
            var 
dir normalize(cp np);
            
cp += dir minSep 0.01;
        }
    }

    
// At least 1 boid in sight
    
if (nb 0.0){
        
avgPos /= nb;
        
cv normalize(avgDir) * speed// Set new velocity based on avg direction

        
var dir normalize(avgPos cp); // Move boid in direction of avg position of boids in sight
        
cv += dir 0.0005;
    }

    
// Add some randomness to their direction
    
cv.+= 0.000082*cos(mytimer 555.);
    
cv.+= 0.000052*sin(mytimer*1.3 355.);

    
// Write output state of flock agent to flockOut
    
flockOut[globalId.x].vec4<f32>( cp00);
    
flockOut[globalId.x].vec4<f32>( cv00);
}




Things to Try


• Add color - each boid has its own color (use the index as its identifier)
• Experiment with different flocking parameters (larger/smaller radius, different speeds, amounts of random noise)
• Try giving boids 'mass' so they are more or less influenced by the group (neighbours)
• Add 'types' so only certain boids 'flock' together (but they still avoid bumping into one another)
• Add cursor input (add forces/noise)
• Store past positions and create a 'tail' effect when drawing the boids
• Explore 3d boids (not just x and y but also z) - should be able to update the compute shader to move in 3d - however, the render would need to shift to a 3d graphics (rendering pipeline for the visualization)



Resources and Links


• WebGPU Lab Flocking 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.