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

 


Simulation 2D Boxes


Basic simulation using falling 2d boxes - with impulses for resolving box-box intersections/collisions.


Simulation 2d boxes (WebGPU Compute)
Simulation 2d boxes (WebGPU Compute)


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>';
}

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');
canvas.id 'canvas';
canvas.style.border '1px solid orange';
document.body.appendChildcanvas ); canvas.height canvas.width 600;


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


// Grouped data into vec4 groups - help with alignment/packing
const NUMBER_OBJECTS   3;
const 
SIZE_EACH_OBJECT 4;   // Number floats for each object structure (vel, pos, size, ..)





const objectArray0 = new Float32ArrayNUMBER_OBJECTS SIZE_EACH_OBJECT );

const 
objectBuffer0 device.createBuffer({ sizeobjectArray0.byteLengthusageGPUBufferUsage.STORAGE GPUBufferUsage.COPY_DST  GPUBufferUsage.COPY_SRC });
device.queue.writeBuffer(objectBuffer00objectArray0);

const 
objectBuffer1 device.createBuffer({ sizeobjectArray0.byteLengthusageGPUBufferUsage.STORAGE GPUBufferUsage.COPY_DST  GPUBufferUsage.COPY_SRC });
device.queue.writeBuffer(objectBuffer00objectArray0);

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


const objectBufferTmp device.createBuffer({ sizeobjectArray0.byteLengthusageGPUBufferUsage.COPY_DST GPUBufferUsage.MAP_READ });

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

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

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

const mouse  = new Float32Array( [0.00.0] );
const 
mouseUniformBuffer device.createBuffer({ sizemouse.byteLength,  usageGPUBufferUsage.UNIFORM GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(mouseUniformBuffer,   0mouse );

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


// Bind group layout and bind group
const bindGroupLayout device.createBindGroupLayout({
  
entries: [  
    {
binding0visibilityGPUShaderStage.COMPUTEbuffer: { type"uniform" }   },
    {
binding1visibilityGPUShaderStage.COMPUTEbuffer: { type"uniform" }   },
    {
binding2visibilityGPUShaderStage.COMPUTEbuffer: { type"storage" }   },
    {
binding3visibilityGPUShaderStage.COMPUTEbuffer: { type"storage" }   }
           ]
});

const 
bindGroup0 device.createBindGroup({ layoutbindGroupLayout,
                                            
entries: [  { binding0,  resource: { buffertimerUniformBuffer     } },
                                                       { 
binding1,  resource: { buffermouseUniformBuffer     } },
                                                        { 
binding2,  resource: { bufferobjectBuffer0          } },
                                                       { 
binding3,  resource: { bufferobjectBuffer1          } }
                                                    ] });

const 
bindGroup1 device.createBindGroup({ layoutbindGroupLayout,
                                            
entries: [  { binding0,  resource: { buffertimerUniformBuffer     } },
                                                       { 
binding1,  resource: { buffermouseUniformBuffer     } },
                                                        { 
binding2,  resource: { bufferobjectBuffer1          } },
                                                       { 
binding3,  resource: { bufferobjectBuffer0          } }
                                                    ] });

// Compute shader code
const computeShader = ` 
struct stObject {  
    // object data
    p      : vec4<f32>, // position and rotation (xy position, z angle)
    s      : vec4<f32>, // size
    v      : vec4<f32>, // velocity - linearly (xy) angular (z)
    m      : vec4<f32>, // inverse mass - linear / angular
}


struct stObjectDiff {
    // object data
    p      : vec2<f32>, 
    a      : vec2<f32>,
    b      : vec2<f32>,
    i      : u32,
    ai     : u32,
    bi     : u32
}


struct stHit {
  pen        : f32,
  normal     : vec2<f32>,
  point      : vec2<f32>
};


@binding(0) @group(0) var<uniform>            mytimer   : f32;
@binding(1) @group(0) var<uniform>            mymouse   : vec2<f32>;
@binding(2) @group(0) var<storage,read_write> objects0  : array< stObject  , 
${NUMBER_OBJECTS} >;  
@binding(3) @group(0) var<storage,read_write> objects1  : array< stObject  , 
${NUMBER_OBJECTS} >; 

fn rotatePoint(point: vec2<f32>, angle: f32) -> vec2<f32> {
    let c = cos(angle);
    let s = sin(angle);
    return vec2<f32>(
        point.x * c - point.y * s,
        point.x * s + point.y * c
    );
}

fn cross2D(a: vec2<f32>, b: vec2<f32>) -> f32 {
    return a.x * b.y - a.y * b.x;
}

fn perp( n:vec2<f32> ) -> vec2<f32> { 
    return vec2<f32>(n.y, -n.x);                
}

fn closestPoint( pA:vec2<f32>, pB:vec2<f32>, pX:vec2<f32> ) -> vec2<f32>
{
    let b = length( pB - pA );
    if ( b < 0.0001 )
    {
        return pA;
    }
    let n = normalize( pB - pA );
    let pAX = pX - pA;
    let s = dot( n, pAX );
    let a = length( n * s );
    
    let t = a/b;
    if ( t <= 0 ) { return pA; }
    if ( t >= 1 ) { return pB; }
    
    let pT = pA + (pB - pA) * t;
    return pT;
}


// order points for a triangle are in a clockwise or counterclockwise direction

fn getCW( v1:vec2<f32>, v2:vec2<f32>, v3:vec2<f32> ) -> f32
{
    let n1 = normalize( v2 - v1 );
    let n2 = normalize( v3 - v1 );
    let d = cross2D(n1, n2);
    if ( d < 0.0 ) { return -1.0; }
    return 1.0;
}


struct stMinkowskiData 
{
    shapeA  : array< vec2<f32>,       4  >,
    shapeB  : array< vec2<f32>,       4  >,
    shapeBA : array< stObjectDiff,    16 >
};

fn MinkowskiBox(boxA:stObject, boxB:stObject) -> stMinkowskiData
{
    var md : stMinkowskiData;
    
    let corners =  array< vec2<f32>, 4 >(vec2<f32>( -1, -1 ), 
                                         vec2<f32>(  1, -1 ), 
                                         vec2<f32>(  1,  1 ),
                                         vec2<f32>( -1,  1 ) );
  
    for (var i=0; i<4; i++)
    {
        md.shapeA[i] = rotatePoint( corners[i] * boxA.s.xy , boxA.p.z ) + boxA.p.xy;
        md.shapeB[i] = rotatePoint( corners[i] * boxB.s.xy , boxB.p.z ) + boxB.p.xy;
    }
      
    var c:u32 = 0;
    for (var i:u32=0; i< 4;  i++)
    {
      let pA = md.shapeA[i];
      for (var k:u32=0; k< 4;  k++)
      {
        let pB  = md.shapeB[k];  
        let pBA = pB - pA;
        md.shapeBA[c].p  = pBA;
        md.shapeBA[c].a  = pA;
        md.shapeBA[c].b  = pB;
        md.shapeBA[c].i  = c;
        md.shapeBA[c].ai = i;
        md.shapeBA[c].bi = k;
        c++;
      }
    }
    return md;
}

fn getSupport( md:stMinkowskiData, dir:vec2<f32> ) -> stObjectDiff
{
    let n = normalize( dir );
    var d = dot( md.shapeBA[0].p, n );
    var h:u32 = 0;
    for (var i:u32=0; i< 16; i++)
    {
        let t = dot( md.shapeBA[i].p, n );
        if ( t > d )
        {
            d = t;
            h = i;
        }
    }
    return md.shapeBA[h];  


struct stEdge {
    e0 : stObjectDiff,
    e1 : stObjectDiff
}

fn collision( md:stMinkowskiData ) -> stHit
{
    var hit : stHit;
    hit.pen = 9999.9;
    
    
    var v1 = getSupport( md, vec2<f32>(0, 1) );
    var v2 = getSupport( md, vec2<f32>(1, 0) );
    var v3 = getSupport( md, vec2<f32>(0,-1) );

    if ( v2.i == v1.i || v2.i == v3.i )
    {
        // swap them around to keep things ordered clockwise
        v2 = v3;
        v3 = getSupport( md, vec2<f32>(-1, 0) );
    }

    //console.assert( v1.i != v2.i);
    //console.assert( v2.i != v3.i);

    var edges = array< stEdge , 5 >();
    var numEdges:u32 = 0;
    
    edges[numEdges].e0 = v1;   edges[numEdges].e1 = v2;        numEdges++;
    edges[numEdges].e0 = v2;   edges[numEdges].e1 = v3;        numEdges++;
    edges[numEdges].e0 = v3;   edges[numEdges].e1 = v1;        numEdges++;
    edges[numEdges].e0 = v3;   edges[numEdges].e1 = v1;        numEdges++;
    edges[numEdges].e0 = v3;   edges[numEdges].e1 = v1;        numEdges++;

    var hullEdges   = array< stEdge , 16 >();
    var numHullEdges = 0;

    var check = 0;
    while ( numEdges > 0 )
    {
        var eedge = edges[numEdges-1];
        numEdges--;

        let n = normalize( perp( eedge.e0.p - eedge.e1.p ) );

        let v4 = getSupport( md, n );
        
        if ( v4.i == eedge.e0.i ||
             v4.i == eedge.e1.i )
        {  
            // save edge 
            hullEdges[ numHullEdges ] = stEdge();
            hullEdges[ numHullEdges ] = eedge;
            numHullEdges++;
        }
        else 
        {
            numEdges++;
            edges[ numEdges-1 ] = stEdge();
            edges[ numEdges-1 ].e0 = eedge.e0;
            edges[ numEdges-1 ].e1 = v4; 
            
            numEdges++;
            edges[ numEdges-1 ] = stEdge();
            edges[ numEdges-1 ].e0 = v4;
            edges[ numEdges-1 ].e1 = eedge.e1;
        }

        //console.assert( edges.length < 100 );
        check++;
        //console.assert(check<100);
        if ( check > 100 ) 
        { 
            hit.pen = 99999.9; 
            return hit; 
        }
    }
    
    var sdist = 10000.0;
    var indx  = 0;

    var best = vec2<f32>(0.0);
    
    for (var i=0; i<numHullEdges; i++)
    {
        let edge = hullEdges[i];
        //drawLine( edge.e0.p, edge.e1.p, 'orange' );

        let cp = closestPoint( edge.e0.p, edge.e1.p, vec2<f32>(0,0) );
        //drawCircle( cp, 4, 'yellow' );

        let d = length( cp - vec2<f32>(0.0,0.0) );
        if ( d < sdist )
        {
            sdist = d;
            indx  = i;
            best  = cp;
        }
    }

    let inside = getCW( vec2<f32>(0,0),
                        hullEdges[indx].e0.p,
                        hullEdges[indx].e1.p );

    //let best = closestPoint( hullEdges[indx].e0.p, 
    //                         hullEdges[indx].e1.p, 
    //                         vec2<f32>(0,0) );
    
    //drawCircle( best, 6, 'yellow' );

    let df = length( hullEdges[indx].e0.p - hullEdges[indx].e1.p );
                                 
    let d1 = length( hullEdges[indx].e0.p - best );

     let t = d1/df;
    //console.log( 't:' + t + ', df:' + df + ', d1: ' + d1 );

    //console.assert( t >=0 && t <= 1 );

    let a0 = hullEdges[indx].e0.a;
    let a1 = hullEdges[indx].e1.a;
    let x  = a0 + ( a1 - a0 ) * t;
    // drawCross( x, 40, 'orange' );

    let b0 = hullEdges[indx].e0.b;
    let b1 = hullEdges[indx].e1.b;
    let y  = b0 + ( b1 - b0 ) * t;
    //drawCross( y, 40, 'orange' );

    //drawLine( x, y, 'red' );

    let pen = length( x - y );
    let nor = normalize( x - y );

    
    hit.pen    = pen;
    if ( inside <= 0.0 ) { hit.pen = pen * -1.0; }
    hit.point      = x;
    hit.normal     = nor;
    
    return hit; // return collision data

}// end collision function


// Parameters
const dt :f32              = 0.1;
const NUMBER_OBJECTS:  u32 = 
${NUMBER_OBJECTS};

struct stResponse {
    dv : vec2<f32>,
    dw : f32
}

fn impulseResponse(
    com   : vec2<f32>,
    hp    : vec2<f32>,
    vcom  : vec2<f32>,
    omega : f32,
    m     : f32, // inverse mass
    I     : f32, // inverse inertia
    e     : f32
) -> stResponse 
{
    var response : stResponse;
    
    // Calculate position vector from CoM to point of contact
    let r = hp - com;
    
    // Calculate velocity of point of contact
    let vp = vcom + vec2<f32>(-omega * r.y, omega * r.x);
    
    // Calculate impulse due to collision
    let J = -m * (1.0 + e) * vp;
    
    // Update linear velocity
    response.dv = J * m;
    
    // Update angular velocity
    response.dw = (r.x * J.y - r.y * J.x) * I;
    
    // Return delta velocities
    return response;
}


// Main compute shader function
@compute @workgroup_size( 
${NUMBER_OBJECTS}, 1 )
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>
) {
    // Initialization step
    if ( mytimer <= 0.0 )
    {
        objects0[0].p = vec4<f32>(300.0, 300.0, 0.6, 0.0);
        objects0[0].s = vec4<f32>(100.0, 50.0,  0.0, 0.0);
        objects0[0].v = vec4<f32>(0.0, -15.1, 0.0,  0.0);
        objects0[0].m = vec4<f32>(1.0,  1, 0.0,  0.0);
        
        objects0[1].p = vec4<f32>(300.0,  40.0,   0.0, 0.0);
        objects0[1].s = vec4<f32>(250.0,  30.0,  0.0, 0.0);
        objects0[1].v = vec4<f32>(0, 0, 0, 0);
        objects0[1].m = vec4<f32>(0, 0, 0, 0);
        
        if ( NUMBER_OBJECTS > 1 ){
          objects0[2].p = vec4<f32>(300.0,  500.0,   0.0, 0.0);
          objects0[2].s = vec4<f32>(20.0,  30.0,  0.0, 0.0);
          objects0[2].v = vec4<f32>(0, 0, 0, 0);
          objects0[2].m = vec4<f32>(1, 1, 0, 0);
        }
             
           //return;
    }
    
    // Read the current state of flock agent
    var cp = objects0[globalId.x].p.xy; // position
    var ca = objects0[globalId.x].p.z;  // angle
    var cs = objects0[globalId.x].s.xy; // size
    var cv = objects0[globalId.x].v.xy; // linear velocity
    var cw = objects0[globalId.x].v.z;  // angular velocity
    var cm = objects0[globalId.x].m.x;  // mass
    var ci = objects0[globalId.x].m.y;  // inertia
        
       
    // integrator
    cp += cm * cv * dt;
    ca += ci * cw * dt;
    
    // neighbour body interaction/collision response
    for (var i:u32 = 0; i<NUMBER_OBJECTS; i++)
    {
        if ( i==globalId.x ) { continue; };
        
        let md = MinkowskiBox( objects0[globalId.x], objects0[i] );

        let hit = collision( md );

        if ( hit.pen > 0.0 ) { continue; }
        
        // relative offset from com to point
        let r  = ( hit.point - cp );
        
        // velocity of the point
        let vp = cv + vec2<f32>(-cw * r.y, cw * r.x);
        
        // moving away from each other?
        if ( dot( hit.normal, vp ) < 0.0 ) { continue; }
        
       
        // penalty based force
        /* 
        // linear
        cv += hit.normal * hit.pen * (1.0/dt) * dot(hit.normal,vp) * 0.01;
        // angular
        cw -= cross2D( r, hit.normal ) * 0.001;
        */
        
        // impulse
        
        // impulse force
        let res = impulseResponse( cp, hit.point, cv, cw, cm, ci, 0.1);

        // linear response
        cv += res.dv;
        // angular response
        cw += res.dw*0.00005;
        
        // fudge factor - to resolve 'issues' - numerical problems/error
        cp += cm * hit.normal * hit.pen * 0.1;
        
        
    }// end for i
    
    //cw *= 0.95;
    
    // environmental interaction/reaction
    
    // donward gravity
    cv.y += -1.0;
   

    // Write output state of flock agent to flockOut
    objects1[globalId.x].p = vec4<f32>( cp, ca, 0 );
    objects1[globalId.x].s = vec4<f32>( cs, 0,  0 );
    objects1[globalId.x].v = vec4<f32>( cv, cw, 0 );
    objects1[globalId.x].m = vec4<f32>( cm, ci, 0, 0 );
}

`;
  

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


let off = {x:0y:0};
let cur = {x:0y:0};

async function drawCircle(ctxpxpyrcolor='blue')
{
    
// Draw circle
    
ctx.strokeStyle color;
    
ctx.beginPath();
    
ctx.arc(off.x+pxcanvas.height off.y-pyr0Math.PI 2);
    
ctx.stroke(); // Draw outline
    
ctx.closePath();
}

async function drawLine(ctxp0p1color='green'thickness=)
{
      
ctx.lineWidth thickness;
      
ctx.strokeStyle color;
    
ctx.beginPath();
    
ctx.moveTo(off.x+p0.xcanvas.height off.y-p0.y);
    
ctx.lineTo(off.x+p1.xcanvas.height off.y-p1.y);
    
ctx.closePath();
    
ctx.stroke();
}

function 
rotatePointpointangle )
{
    
let c Math.cos(angle);
    
let s Math.sin(angle);
    return new 
vec2(
        
point.point.s,
        
point.point.c
    
);
}

function 
vec2(xy)
{
  
this.x;
  
this.y;
  
  
this.set = function(xy)
  {
    
this.x;
    
this.y;
  }
};

vec2.add    = function(v0v1){  return new vec2(v0.x+v1.xv0.y+v1.y);    }
vec2.sub    = function(v0v1){  return new vec2(v0.x-v1.xv0.y-v1.y);    }
vec2.scale  = function(v0){ return new vec2(v0.x*sv0.y*s);          }
vec2.mul    = function(v0v1){ return new vec2(v0.x*v1.xv0.y*v1.y);    }


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

  {
  const 
passEncoder commandEncoder.beginComputePass();
  
passEncoder.setPipeline(computePipeline);
  
passEncoder.setBindGroup(0bindGroup0);
  
// workgroup size on the wgsl shader
  
passEncoder.dispatchWorkgroups);
  
await passEncoder.end();
  }
  
  
//timestep[0] = timestep[0] + 0.1;
  //await device.queue.writeBuffer(timerUniformBuffer,   0, timestep );
  
  
{
  const 
passEncoder commandEncoder.beginComputePass();
  
passEncoder.setPipeline(computePipeline);
  
passEncoder.setBindGroup(0bindGroup1);
  
// workgroup size on the wgsl shader
  
passEncoder.dispatchWorkgroups);
  
await passEncoder.end();
  }
  
  
//timestep[0] = timestep[0] + 0.1;
  //device.queue.writeBuffer(timerUniformBuffer,   0, timestep );

  // Encode commands for copying buffer to buffer.
  
await
  commandEncoder
.copyBufferToBuffer(
      
objectBuffer1,      // source buffer
      
0,                  // source offset
      
objectBufferTmp,    // destination buffer
      
0,                  // destination offset
      
objectArray0.byteLength  // size
  
);
                                
  
// Submit GPU commands.
  
const gpuCommands commandEncoder.finish();
  
await device.queue.submit([gpuCommands]);
  
  
timestep[0] = timestep[0] + 0.1;
  
await device.queue.writeBuffer(timerUniformBuffer,   0timestep );
  
 
  
// Read buffer.
  
await objectBufferTmp.mapAsync(GPUMapMode.READ);
  const 
arrayBuffer objectBufferTmp.getMappedRange();
  const array = new 
Float32ArrayarrayBuffer );
  const 
outputArray = Array.from( array );
  
//const arr = Array.from( array );
  
objectBufferTmp.unmap();
  
  
//console.log('outputArray:', outputArray );

  //log('Collision data:')
  
  //for (let i=0; i<arr.length; i++)
  //{
      // display over multiple lines
      //log( arr.splice( 0, 4 ) );  
  //}
  
  
  // Extract the positions and velocities
  
const ctx canvas.getContext('2d');
  
ctx.clearRect(00canvas.widthcanvas.height);
  
//console.log( 'outputArray.length:', outputArray.length );
  
  
drawCirclectx005'black' );
  
  
drawLine(ctx, {x:0,y:0}, {x:50,y:}, 'red'  6.0 );
  
drawLine(ctx, {x:0,y:0}, {x:0y:50}, 'green'6.0 );
  
  
  
  
  for (var 
0NUMBER_OBJECTS++ )
  {
      
let stride 4*4;
         
//console.log( i ); 
      
     
      // position
      
var px outputArray[i*stride+0];
      var 
py outputArray[i*stride+1];
    
      
//console.log( px, py );
        
      
var angle    outputArray[i*stride+2];
    
      
//var vx = outputArray[i+4];
      //var vy = outputArray[i+5];
    
      
var sx outputArray[i*stride+4];
      var 
sy outputArray[i*stride+5];

    
      
//if ( pen < 0 )
      
{
          
ctx.font "20px Arial";
          
ctx.fillText'Simulation' ,10,40);
      }
    
    
      {
      
      
let corners =  [ new vec2( -1, -), 
                       new 
vec2(  1, -), 
                       new 
vec2(  1,  ),
                       new 
vec2( -1,  ) ];
  
      
      for (
let g=0g<4g++)
      {
          
let p0 vec2.addrotatePointvec2.mulcorners[g],       new vec2(sx,sy)) , angle ), new vec2(px,py) );
          
let p1 vec2.addrotatePointvec2.mulcorners[(g+1)%4], new vec2(sx,sy)) , angle ), new vec2(px,py) );
        
          
drawLinectxp0p1'black' );
      }
        
      }
      
      
/*
      // draw square
      ctx.strokeStyle = 'black';
      ctx.save();
      ctx.translate(off.x+px, off.y+py);
      ctx.rotate(angle);
      ctx.beginPath();
      ctx.rect(   -2*sx/2,   -2*sy/2, 
                   2*sx,      2*sy);
      ctx.stroke();
      ctx.restore();
      */
    
      
mouse[0] = cur.x;
      
mouse[1] = cur.y;
       
device.queue.writeBuffer(mouseUniformBuffer,   0mouse );
  }
// end for i
  
  
  
requestAnimationFrame(frame);
}
// end frame(..)


document.onmousemove = function(e)
{  
  
cur.e.pageX off.x;
  
cur.e.pageY off.y;
}
document.onmousemove( {pageX:450pageY:450} );



frame();



Resources and Links


WebGPU Demo Code - Stacking 2D Boxes in Real-Time





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.