www.xbdev.net
xbdev - software development
Saturday June 13, 2026
Home | Contact | Support | WebGPU Graphics and Compute ... | WebGPU.. Games, Tutorials, Demos, Projects, and Code.....
     
 

WebGPU..

Games, Tutorials, Demos, Projects, and Code.....

 



Per Pixel Lighting


Calculating the lighting in the fragment shader (simple directional light).


Per pixel lighting - example lighting simple spheres.
Per pixel lighting - example lighting simple spheres.


Functions Used: setVertexBuffer(), setIndexBuffer(), drawIndexed(), createBuffer(), getMappedRange(), getContext(), requestAdapter(), getPreferredCanvasFormat(), createCommandEncoder(), beginRenderPass(), setPipeline(), draw(), end(), submit(), getCurrentTexture(), createView(), createShaderModule()

The function object for generating a 3d sphere and drawing the mesh on screen (fragment shader does the lighting calculation).

<?php
sphere = function()
{
// ------------------------------------------------

function geo()
{
      let widthSegments  = 10, 
        heightSegments = 10, 
        radius = 1.0,
        phiStart = 0, phiLength = Math.PI * 2, 
        thetaStart = 0, thetaLength = Math.PI;

    widthSegments = Math.max( 3, Math.floor( widthSegments ) );
    heightSegments = Math.max( 2, Math.floor( heightSegments ) );

    const thetaEnd = Math.min( thetaStart + thetaLength, Math.PI );

    let index = 0;
    const grid = [];

    const vertex = {x:0, y:0, z:0};
    const normal = {x:0, y:0, z:0};

    // buffers
    const mesh = { v:[], n:[], t:[], i:[] }

    // generate vertices, normals and uvs
    for ( let iy = 0; iy <= heightSegments; iy ++ ) {

      const verticesRow = [];
      const v = iy / heightSegments;
      // special case for the poles

      let uOffset = 0;
      if ( iy === 0 && thetaStart === 0 ) {
        uOffset = 0.5 / widthSegments;
      } else if ( iy === heightSegments && thetaEnd === Math.PI ) {
        uOffset = - 0.5 / widthSegments;
      }

      for ( let ix = 0; ix <= widthSegments; ix ++ ) {
        const u = ix / widthSegments;
        // vertex
        vertex.x = - radius * Math.cos( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
        vertex.y = radius * Math.cos( thetaStart + v * thetaLength );
        vertex.z = radius * Math.sin( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
        mesh.v.push( [vertex.x, vertex.y, vertex.z] );

        // normal
        let vl = vertex.x*vertex.x + vertex.y*vertex.y + vertex.z*vertex.z;
        normal.x = vertex.x/vl;
        normal.y = vertex.y/vl;
        normal.z = vertex.z/vl;
        mesh.n.push( [normal.x, normal.y, normal.z] );

        // uv
        mesh.t.push( [u + uOffset, v] );
        verticesRow.push( index++ );
      }
      grid.push( verticesRow );
    }
  
      // indices
    for ( let iy = 0; iy < heightSegments; iy ++ ) {

      for ( let ix = 0; ix < widthSegments; ix ++ ) {

        const a = grid[ iy ][ ix + 1 ];
        const b = grid[ iy ][ ix ];
        const c = grid[ iy + 1 ][ ix ];
        const d = grid[ iy + 1 ][ ix + 1 ];

        if ( iy !== 0 || thetaStart > 0 ) mesh.i.push( a, b, d );
        if ( iy !== heightSegments - 1 || thetaEnd < Math.PI ) mesh.i.push( b, c, d );
      }
    }
    return mesh;
}

// -------------------------------
  
this.create = async function(device, presentationFormat, presentationSize, textureName="https://webgpulab.xbdev.net/var/images/earth.jpg")
{
  
let mesh = geo();

console.log('num Vertices:', mesh.v.length);
console.log('num Coords:' ,  mesh.t.length);
console.log('num Normals:',  mesh.n.length);
console.log('num Indices:',  mesh.i.length);

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

// Lets biuld the buffers to hold the data

if ( mesh.n.length==0 ) mesh.n.length = mesh.v.length * 3;
if ( mesh.t.length==0 ) mesh.t.length = mesh.v.length * 2;
  
mesh.i = mesh.i.flat();
mesh.v = mesh.v.flat();
mesh.n = mesh.n.flat();
mesh.t = mesh.t.flat();
  
this.numTris         = mesh.i.length; // number of indices
  
// ------------------

const s = 1.0;
this.positions = new Float32Array( mesh.v );
this.indices   = new Uint32Array( mesh.i );
this.normals   = new Float32Array( mesh.n );
this.uvs       = new Float32Array( mesh.t );
this.timer     = new Float32Array([0.0]);
  
this.positionBuffer = device.createBuffer({
  size:  this.positions.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
});

this.normalBuffer = device.createBuffer({
  size:  this.normals.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
});

this.uvBuffer = device.createBuffer({
  size:  this.uvs.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
});

this.indicesBuffer = device.createBuffer({
  size:  this.indices.byteLength,
  usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
});

this.timerBuffer = device.createBuffer({
  size:  4, // single float
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});

device.queue.writeBuffer(this.positionBuffer, 0, this.positions);
device.queue.writeBuffer(this.normalBuffer  , 0, this.normals  );
device.queue.writeBuffer(this.indicesBuffer , 0, this.indices  );
device.queue.writeBuffer(this.uvBuffer      , 0, this.uvs      );
device.queue.writeBuffer(this.timerBuffer   , 0, this.timer    );

// var vertWGSL = document.getElementById('vertex.wgsl').innerHTML;
// var fragWGSL = document.getElementById('fragment.wgsl').innerHTML;

var vertWGSL = `
struct Uniforms {
    modelMatrix : mat4x4<f32>,
    viewMatrix  : mat4x4<f32>,
    projMatrix  : mat4x4<f32>,
};
@binding(0) @group(0) var<uniform> uniforms : Uniforms;

struct VSOut {
    @builtin(position) Position : vec4<f32>,
    @location(0)       normal   : vec3<f32>,
    @location(1)       uvs      : vec2<f32>,
    @location(2)       pos      : vec3<f32>,
};

@vertex 
fn main(@location(0) inPos  : vec3<f32>,
        @location(1) normal : vec3<f32>,
        @location(2) uvs    : vec2<f32>) -> VSOut  
{ 

  let mvMatrix = uniforms.viewMatrix * uniforms.modelMatrix;
  let mvp      = uniforms.projMatrix * mvMatrix;
  
  var vsOut: VSOut;
  vsOut.Position   = mvp * vec4<f32>( inPos, 1.0);
  vsOut.uvs        = uvs;
  vsOut.normal     = ( uniforms.modelMatrix * vec4<f32>(normal, 0.0) ).xyz;
  vsOut.pos        = ( uniforms.modelMatrix * vec4<f32>(inPos,1.0) ).xyz;
  return vsOut;
}
`;
  
var fragWGSL = `
@group(0) @binding(1) var mySampler: sampler;
@group(0) @binding(2) var myTexture: texture_2d<f32>;
@group(0) @binding(3) var <uniform> myTimer:   f32;

@fragment
fn main(@location(0) normal   : vec3<f32>,
        @location(1) uvs      : vec2<f32>,
        @location(2) position : vec3<f32> ) -> @location(0) vec4<f32> 
{
    let scrolluv = uvs + vec2<f32>( myTimer*0.2, 0.0  );
    let texCol = textureSample(myTexture, mySampler, scrolluv );
       
    let lightdirection = normalize( vec3<f32>(1, 0, 1) );
    let nnormal        = normalize( -normal );

    // scale brightness based on the normal vs ref drection
    let illum = clamp( dot( lightdirection, nnormal ), 0.0, 1.0 ) * 2.0;

    return vec4<f32>( texCol.xyz*illum , 1.0 ); 

}
`;

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

let textureSampler = device.createSampler({
     minFilter:    "linear",
     magFilter:    "linear",
  
     addressModeU: "repeat",
     addressModeV: "repeat",
     addressModeW: "repeat", 
});

const img = document.createElement("img");
img.src = textureName; // 'https://webgpulab.xbdev.net/var/images/earth.jpg';
await img.decode();

const basicTexture = device.createTexture({
    size: [img.width, img.height, 1],
    format: "rgba8unorm" , // "bgra8unorm",
    usage:  GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING
});  

const imageCanvas = document.createElement('canvas');
imageCanvas.width =  img.width;
imageCanvas.height = img.height;
const imageCanvasContext = imageCanvas.getContext('2d');
imageCanvasContext.drawImage(img, 0, 0, imageCanvas.width, imageCanvas.height);
const imageData = imageCanvasContext.getImageData(0, 0, imageCanvas.width, imageCanvas.height);
const textureData = imageData.data;
  
device.queue.writeTexture( { texture: basicTexture },
            textureData,
            {   offset     :  0,
                bytesPerRow:  img.width * 4,
                rowsPerImage: img.height
             },
            [ img.width  ,  img.height,  1  ]   );


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

this.mvpUniformBuffer = device.createBuffer({
  size: 64*3,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});

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

this.sceneUniformBindGroupLayout = device.createBindGroupLayout({
  entries: [
    { binding: 0, visibility: GPUShaderStage.VERTEX,   buffer:  { type: "uniform"  }   }, 
    { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering"}   },
    { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float",
                                                                  viewDimension: "2d"} },
    { binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer:  { type: "uniform"  }   }, 
  ]
});

this.uniformBindGroup = device.createBindGroup({
  layout:   this.sceneUniformBindGroupLayout,
  entries: [
    { binding : 0, resource: { buffer: this.mvpUniformBuffer } },
    { binding : 1, resource: textureSampler },
    { binding : 2, resource: basicTexture.createView()  },
    { binding : 3, resource: { buffer: this.timerBuffer        } }
   ],
});

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


this.pipeline = device.createRenderPipeline({
    layout: device.createPipelineLayout({bindGroupLayouts: [this.sceneUniformBindGroupLayout]}),
    vertex:   {  module    : device.createShaderModule({ 
                             code : vertWGSL }),
                 entryPoint: 'main',
                 buffers    : [ { arrayStride: 12, attributes: [{ shaderLocation: 0,
                                                                  format: "float32x3",
                                                                  offset: 0  }]        },
                                { arrayStride: 12, attributes: [{ shaderLocation: 1,
                                                                  format: "float32x3",
                                                                  offset: 0  }]        },
                                { arrayStride: 8,  attributes: [{ shaderLocation: 2,
                                                                  format: "float32x2",
                                                                  offset: 0  }]        }
                              ]},
    fragment: {  module    : device.createShaderModule({ 
                             code : fragWGSL,     }),
                 entryPoint: 'main',
                 targets: [{  format : presentationFormat  }] },
    primitive: { topology  : 'triangle-list',
                 frontFace : "ccw",
                 cullMode  : 'back',
                 stripIndexFormat: undefined },
    depthStencil: {
                 depthWriteEnabled: true,
                 depthCompare     : 'less',
                 format           : 'depth24plus' }
});

  
this.meshData = { active:false, p:{x:0,y:0,z:0}, r:{x:0,y:0,z:0}, s:{x:1,y:1,z:1} };
  
}// end create

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

this.getMeshData = function ()
{
    return this.meshData;
}
  
// ------------------------------

function buildMatrix( p, r, s )
{
    // Create the matrix in Javascript (using matrix library)
    const modelMatrix          = mat4.create();

    // create the model transform with a rotation and translation
    let translateMat = mat4.create();   mat4.fromTranslation( translateMat, Object.values(p) );
    let rotateXMat   = mat4.create();   mat4.fromXRotation(rotateXMat, r.x);
    let rotateYMat   = mat4.create();   mat4.fromYRotation(rotateYMat, r.y);
    let rotateZMat   = mat4.create();   mat4.fromZRotation(rotateZMat, r.z);
    let scaleMat     = mat4.create();   mat4.fromScaling(scaleMat, Object.values(s) );

    mat4.multiply(modelMatrix, modelMatrix,   translateMat);
    mat4.multiply(modelMatrix, modelMatrix,   rotateXMat);
    mat4.multiply(modelMatrix, modelMatrix,   rotateYMat);
    mat4.multiply(modelMatrix, modelMatrix,   rotateZMat);
    mat4.multiply(modelMatrix, modelMatrix,   scaleMat);
    return modelMatrix;
}

this.draw = async function( device, context, depthTexture, viewMatrix, projectionMatrix ) 
{

  // --------------------------------------------------
  // Update uniform buffer 
  
  const modelMatrix = buildMatrix( this.meshData.p, this.meshData.r, this.meshData.s );
  device.queue.writeBuffer(this.mvpUniformBuffer, 0,    modelMatrix);
  device.queue.writeBuffer(this.mvpUniformBuffer, 64,  viewMatrix );
  device.queue.writeBuffer(this.mvpUniformBuffer, 128, projectionMatrix);

  
  this.timer[0] = this.timer[0] + 0.01;
  device.queue.writeBuffer(this.timerBuffer   , 0, this.timer    );

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

  // GPURenderPassDescriptor 
  this.renderPassDescriptor = { 
        colorAttachments:  [{    
             view     : undefined, // asign later in frame
             loadOp:   "load", clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
             storeOp  : 'store' }],
        depthStencilAttachment: {
             view:  depthTexture.createView(),
             depthLoadOp: "load", 
             depthClearValue: 1.0,
             depthStoreOp: 'store',
          } 
  };

  // -------------------------------------------------
  
  this.renderPassDescriptor.colorAttachments[0].view = context.getCurrentTexture().createView();

  const commandEncoder = device.createCommandEncoder();

  const renderPass = commandEncoder.beginRenderPass(this.renderPassDescriptor);
  renderPass.setPipeline(this.pipeline);
  renderPass.setBindGroup(0, this.uniformBindGroup);
  renderPass.setVertexBuffer(0, this.positionBuffer);
  renderPass.setVertexBuffer(1, this.normalBuffer);
  renderPass.setVertexBuffer(2, this.uvBuffer);
  renderPass.setIndexBuffer(this.indicesBuffer, 'uint32');
  renderPass.drawIndexed( this.numTris, 1, 0, 0);
  renderPass.end();
  await device.queue.submit([commandEncoder.finish()]);
  
}// end draw(..)
  
}// end sphere
















Resources and Links


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