Per Vertex Lighting
Calculating the lighting in the vertex shader (simple directional light).
Per vertex 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 (vertex shader does the lighting calculation).
sphere = function() { // ------------------------------------------------ function geo () { let widthSegments = 10 , heightSegments = 6 , 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) uvs : vec2<f32>, @location(1) illum : 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; 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; var vsOut: VSOut; vsOut.Position = mvp * vec4<f32>( inPos, 1.0); vsOut.uvs = uvs; vsOut.illum = illum; 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) uvs : vec2<f32>, @location(1) illum : f32 ) -> @location(0) vec4<f32> { return vec4<f32>( illum, illum, 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 render(..) } // end sphere
Resources and Links
• WebGPU Lab Example [LINK ]