Normals
Normals are a vital concept in computer graphics - essentially the direction the surface is facing. It's used for culling and lighting.
Single triangle provides a point of references for our camera - as we move around we see the triangle moving.
Functions Used: setVertexBuffer(), setIndexBuffer(), drawIndexed(), createBuffer(), getMappedRange(), getContext(), requestAdapter(), getPreferredCanvasFormat(), createCommandEncoder(), beginRenderPass(), setPipeline(), draw(), end(), submit(), getCurrentTexture(), createView(), createShaderModule()
To visualize the normal we draw a line on the triangle surface - the line is in its own function object called 'lines'.
lines = function() { this . create = function( device , presentationFormat ) { const vertWGSL = ` struct Transforms { model : mat4x4<f32>, view : mat4x4<f32>, projection : mat4x4<f32>, }; @group(0) @binding(0) var<uniform> transforms : Transforms; struct VSOut { @builtin(position) Position: vec4<f32>, @location(0) color : vec3<f32>, }; @vertex fn main(@location(0) inPos : vec3<f32>, @location(1) inColor: vec3<f32>) -> VSOut { var mvp = transforms.projection * transforms.view * transforms.model; var vsOut: VSOut; vsOut.Position = mvp * vec4<f32>(inPos, 1.0); vsOut.color = inColor; return vsOut; } `; const fragWGSL = ` @fragment fn main(@location(0) inColor: vec3<f32>) -> @location(0) vec4<f32> { return vec4<f32>(inColor, 1.0); } `; const positions = new Float32Array ([ 0.0 , 0.0 , 1.0 , // Position Vertex Buffer Data 0.0 , 0.0 , - 1.0 ]); const colors = new Float32Array ([ 1.0 , 0.0 , 0.0 , // Color Vertex Buffer Data 0.0 , 1.0 , 0.0 ]); const indices = new Uint16Array ( [ 0 , 1 ]); // Index Buffer Data const createBuffer = ( arrData , usage ) => { const buffer = device . createBuffer ({ size : (( arrData . byteLength + 3 ) & ~ 3 ), usage : usage , mappedAtCreation : true }); if ( arrData instanceof Float32Array ) { (new Float32Array ( buffer . getMappedRange ())). set ( arrData ) } else { (new Uint16Array ( buffer . getMappedRange ())). set ( arrData ) } buffer . unmap (); return buffer ; } // Declare buffer handles (GPUBuffer) this . positionBuffer = createBuffer ( positions , GPUBufferUsage . VERTEX ); this . colorBuffer = createBuffer ( colors , GPUBufferUsage . VERTEX ); this . indexBuffer = createBuffer ( indices , GPUBufferUsage . INDEX ); // ---------------------------------------------------------------- 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" } } ] }); this . uniformBindGroup = device . createBindGroup ({ layout : this . sceneUniformBindGroupLayout , entries : [ { binding : 0 , resource : { buffer : this . mvpUniformBuffer } } ], }); // ---------------------------------------------------------------- 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 }] } ] }, fragment : { module : device . createShaderModule ({ code : fragWGSL }), entryPoint : 'main' , targets : [ { format : presentationFormat } ], }, primitive : { topology : "line-list" , frontFace : "cw" , cullMode : 'none' }, depthStencil : { format : "depth24plus" , depthWriteEnabled : true , depthCompare : "less" } }); } // ----------------------------------------------------- this . draw = function( device , context , depthTexture , modelMatrix , viewMatrix , projectionMatrix ) { device . queue . writeBuffer ( this . mvpUniformBuffer , 0 , modelMatrix ); device . queue . writeBuffer ( this . mvpUniformBuffer , 64 , viewMatrix ); device . queue . writeBuffer ( this . mvpUniformBuffer , 128 , projectionMatrix ); const renderPassDescription = { colorAttachments : [{ view : context . getCurrentTexture (). createView (), loadOp : 'load' , // (k==0 ? "clear":"load"), clearValue : [ 0 , 0.5 , 0.5 , 1 ], // clear screen to color storeOp : 'store' }], depthStencilAttachment : { view : depthTexture . createView (), depthLoadOp : 'load' , // (k==0 ? "clear":"load"), depthClearValue : 1 , depthStoreOp : "store" , } }; renderPassDescription . colorAttachments [ 0 ]. view = context . getCurrentTexture (). createView (); const commandEncoder = device . createCommandEncoder (); const renderPass = commandEncoder . beginRenderPass ( renderPassDescription ); renderPass . setBindGroup ( 0 , this . uniformBindGroup ); renderPass . setPipeline ( this . pipeline ); renderPass . setVertexBuffer ( 0 , this . positionBuffer ); renderPass . setVertexBuffer ( 1 , this . colorBuffer ); renderPass . setIndexBuffer ( this . indexBuffer , 'uint16' ); renderPass . drawIndexed ( 2 , 1 ); renderPass . end (); device . queue . submit ([ commandEncoder . finish ()]); } } // end lines
The body of the code that draws the rotating triangle:
let promise = await fetch ( 'https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.6.0/gl-matrix-min.js' ); let text = await promise . text (); let script = document . createElement ( 'script' ); script . type = 'text/javascript' ; script . async = false ; script . innerHTML = text ; document . body . appendChild ( script ); let canvas = document . createElement ( 'canvas' ); document . body . appendChild ( canvas ); canvas . width = canvas . height = 512 ; const adapter = await navigator . gpu . requestAdapter (); const device = await adapter . requestDevice (); const context = canvas . getContext ( 'webgpu' ); const presentationSize = [ canvas . width , canvas . height ] const presentationFormat = navigator . gpu . getPreferredCanvasFormat (); context . configure ({ device : device , compositingAlphaMode : "opaque" , compositingAlphaMode : "opaque" , format : presentationFormat , size : presentationSize }); const vertWGSL = ` struct Transforms { model : mat4x4<f32>, view : mat4x4<f32>, projection : mat4x4<f32>, }; @group(0) @binding(0) var<uniform> transforms : Transforms; struct VSOut { @builtin(position) Position: vec4<f32>, @location(0) color : vec3<f32>, }; @vertex fn main(@location(0) inPos : vec3<f32>, @location(1) inColor: vec3<f32>) -> VSOut { var mvp = transforms.projection * transforms.view * transforms.model; var vsOut: VSOut; vsOut.Position = mvp * vec4<f32>(inPos, 1.0); vsOut.color = inColor; return vsOut; }`; const fragWGSL = ` @fragment fn main(@location(0) inColor: vec3<f32>) -> @location(0) vec4<f32> { return vec4<f32>(inColor, 1.0); }`; const positions = new Float32Array ([- 1.0 , - 1.0 , 0.0 , // Position Vertex Buffer Data 1.0 , - 1.0 , 0.0 , 0.0 , 1.0 , 0.0 ]); const colors = new Float32Array ([ 1.0 , 0.0 , 0.0 , // Color Vertex Buffer Data 0.0 , 1.0 , 0.0 , 0.0 , 0.0 , 1.0 ]); const indices = new Uint16Array ( [ 0 , 1 , 2 ]); // Index Buffer Data const createBuffer = ( arrData , usage ) => { const buffer = device . createBuffer ({ size : (( arrData . byteLength + 3 ) & ~ 3 ), usage : usage , mappedAtCreation : true }); if ( arrData instanceof Float32Array ) { (new Float32Array ( buffer . getMappedRange ())). set ( arrData ) } else { (new Uint16Array ( buffer . getMappedRange ())). set ( arrData ) } buffer . unmap (); return buffer ; } // Declare buffer handles (GPUBuffer) var positionBuffer = createBuffer ( positions , GPUBufferUsage . VERTEX ); var colorBuffer = createBuffer ( colors , GPUBufferUsage . VERTEX ); var indexBuffer = createBuffer ( indices , GPUBufferUsage . INDEX ); // ---------------------------------------------------------------- function buildMatrix ( p , r , s ) // position, rotation, scale { // if not set fall back to default values if (! s ) s = { x : 1 , y : 1 , z : 1 }; if (! r ) r = { x : 0 , y : 0 , z : 0 }; if (! p ) p = { x : 0 , y : 0 , z : 0 }; // 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 ; } // build a model matrix (scale, rotate and position it wherever we want) let modelMatrix = buildMatrix (); // setup the projection let projectionMatrix = mat4 . create (); mat4 . perspective ( projectionMatrix , Math . PI / 2 , canvas . width / canvas . height , 0.001 , 5000.0 ); // default camera `lookat` - camera is at -4 units down the z-axis looking at '0,0,0' let viewMatrix = mat4 . create (); mat4 . lookAt ( viewMatrix , [ 0 , 0 ,- 4 ], [ 0 , 0 , 0 ], [ 0 , 1 , 0 ]); let mvpUniformBuffer = device . createBuffer ({ size : 64 * 3 , usage : GPUBufferUsage . UNIFORM | GPUBufferUsage . COPY_DST }); device . queue . writeBuffer ( mvpUniformBuffer , 0 , modelMatrix ); device . queue . writeBuffer ( mvpUniformBuffer , 64 , viewMatrix ); device . queue . writeBuffer ( mvpUniformBuffer , 128 , projectionMatrix ); // ---------------------------------------------------------------- let sceneUniformBindGroupLayout = device . createBindGroupLayout ({ entries : [ { binding : 0 , visibility : GPUShaderStage . VERTEX , buffer : { type : "uniform" } } ] }); let uniformBindGroup = device . createBindGroup ({ layout : sceneUniformBindGroupLayout , entries : [ { binding : 0 , resource : { buffer : mvpUniformBuffer } } ], }); // ---------------------------------------------------------------- const pipeline = device . createRenderPipeline ({ layout : device . createPipelineLayout ({ bindGroupLayouts : [ 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 }] } ] }, fragment : { module : device . createShaderModule ({ code : fragWGSL }), entryPoint : 'main' , targets : [ { format : presentationFormat } ], }, primitive : { topology : "triangle-list" , frontFace : "cw" , cullMode : 'none' }, depthStencil : { format : "depth24plus" , depthWriteEnabled : true , depthCompare : "less" } }); const depthTexture = device . createTexture ({ size : [ canvas . width , canvas . height , 1 ], format : "depth24plus" , usage : GPUTextureUsage . RENDER_ATTACHMENT }) // --------------- let line = new lines (); line . create ( device , presentationFormat ); let counter = 0.0 ; function frame () { // setup a transform for each triangle let tris = [ { p :{ x : 0 , y : 0 , z : 0 }, r :{ x : 0 , y : 0.0 , z : 0.0 }, s :{ x : 1.0 , y : 1.0 , z : 1.0 } } ]; // loop over each triangle and render it //tris.forEach( (t,k)=>{ let t = tris [ 0 ]; let k = 0 ; let modelMatrix = buildMatrix ( t . p , t . r , t . s ); // update the local matrix for each triangle draw differently device . queue . writeBuffer ( mvpUniformBuffer , 0 , modelMatrix ); // Rotate the camera around the origin in the circle let cameraEye = [ Math . cos ( counter )* 3.0 , 0.0 , Math . sin ( counter )* 3.0 ]; mat4 . lookAt ( viewMatrix , cameraEye , [ 0 , 0 , 0 ], [ 0 , 1 , 0 ]); device . queue . writeBuffer ( mvpUniformBuffer , 64 , viewMatrix ); // simple counter counter += 0.001 ; const renderPassDescription = { colorAttachments : [{ view : context . getCurrentTexture (). createView (), loadOp : ( k == 0 ? "clear" : "load" ), clearValue : [ 0 , 0.5 , 0.5 , 1 ], // clear screen to color storeOp : 'store' }], depthStencilAttachment : { view : depthTexture . createView (), depthLoadOp : ( k == 0 ? "clear" : "load" ), depthClearValue : 1 , depthStoreOp : "store" , } }; renderPassDescription . colorAttachments [ 0 ]. view = context . getCurrentTexture (). createView (); const commandEncoder = device . createCommandEncoder (); const renderPass = commandEncoder . beginRenderPass ( renderPassDescription ); renderPass . setBindGroup ( 0 , uniformBindGroup ); renderPass . setPipeline ( pipeline ); renderPass . setVertexBuffer ( 0 , positionBuffer ); renderPass . setVertexBuffer ( 1 , colorBuffer ); renderPass . setIndexBuffer ( indexBuffer , 'uint16' ); renderPass . drawIndexed ( 3 , 1 ); renderPass . end (); device . queue . submit ([ commandEncoder . finish ()]); //}); line . draw ( device , context , depthTexture , modelMatrix , viewMatrix , projectionMatrix ); // animate - keep updating requestAnimationFrame ( frame ); } frame (); console . log ( 'ready...' );
Resources and Links
• WebGPU Lab Example [LINK ]