 | Per Pixel Lighting |  |
Calculating the lighting in the fragment shader (simple directional light).
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]
|