mirror of
https://github.com/webgpu/webgpufundamentals.git
synced 2026-05-16 06:50:37 -04:00
474 lines
14 KiB
HTML
474 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
|
<title>WebGPU Lighting - Spot with smoothstep falloff</title>
|
|
<style>
|
|
@import url(resources/webgpu-lesson.css);
|
|
html, body {
|
|
margin: 0; /* remove the default margin */
|
|
height: 100%; /* make the html,body fill the page */
|
|
}
|
|
canvas {
|
|
display: block; /* make the canvas act like a block */
|
|
width: 100%; /* make the canvas fill its container */
|
|
height: 100%;
|
|
}
|
|
:root {
|
|
--bg-color: #fff;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg-color: #000;
|
|
}
|
|
}
|
|
canvas {
|
|
background-color: var(--bg-color);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas></canvas>
|
|
</body>
|
|
<script type="module">
|
|
import GUI from '../3rdparty/muigui-0.x.module.js';
|
|
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#wgpu-matrix
|
|
import {mat3, mat4} from '../3rdparty/wgpu-matrix.module.js';
|
|
|
|
function createFVertices() {
|
|
const positions = [
|
|
// left column
|
|
-50, 75, 15,
|
|
-20, 75, 15,
|
|
-50, -75, 15,
|
|
-20, -75, 15,
|
|
|
|
// top rung
|
|
-20, 75, 15,
|
|
50, 75, 15,
|
|
-20, 45, 15,
|
|
50, 45, 15,
|
|
|
|
// middle rung
|
|
-20, 15, 15,
|
|
20, 15, 15,
|
|
-20, -15, 15,
|
|
20, -15, 15,
|
|
|
|
// left column back
|
|
-50, 75, -15,
|
|
-20, 75, -15,
|
|
-50, -75, -15,
|
|
-20, -75, -15,
|
|
|
|
// top rung back
|
|
-20, 75, -15,
|
|
50, 75, -15,
|
|
-20, 45, -15,
|
|
50, 45, -15,
|
|
|
|
// middle rung back
|
|
-20, 15, -15,
|
|
20, 15, -15,
|
|
-20, -15, -15,
|
|
20, -15, -15,
|
|
];
|
|
|
|
const indices = [
|
|
0, 2, 1, 2, 3, 1, // left column
|
|
4, 6, 5, 6, 7, 5, // top run
|
|
8, 10, 9, 10, 11, 9, // middle run
|
|
|
|
12, 13, 14, 14, 13, 15, // left column back
|
|
16, 17, 18, 18, 17, 19, // top run back
|
|
20, 21, 22, 22, 21, 23, // middle run back
|
|
|
|
0, 5, 12, 12, 5, 17, // top
|
|
5, 7, 17, 17, 7, 19, // top rung right
|
|
6, 18, 7, 18, 19, 7, // top rung bottom
|
|
6, 8, 18, 18, 8, 20, // between top and middle rung
|
|
8, 9, 20, 20, 9, 21, // middle rung top
|
|
9, 11, 21, 21, 11, 23, // middle rung right
|
|
10, 22, 11, 22, 23, 11, // middle rung bottom
|
|
10, 3, 22, 22, 3, 15, // stem right
|
|
2, 14, 3, 14, 15, 3, // bottom
|
|
0, 12, 2, 12, 14, 2, // left
|
|
];
|
|
|
|
const normals = [
|
|
0, 0, 1, // left column front
|
|
0, 0, 1, // top rung front
|
|
0, 0, 1, // middle rung front
|
|
|
|
0, 0, -1, // left column back
|
|
0, 0, -1, // top rung back
|
|
0, 0, -1, // middle rung back
|
|
|
|
0, 1, 0, // top
|
|
1, 0, 0, // top rung right
|
|
0, -1, 0, // top rung bottom
|
|
1, 0, 0, // between top and middle rung
|
|
0, 1, 0, // middle rung top
|
|
1, 0, 0, // middle rung right
|
|
0, -1, 0, // middle rung bottom
|
|
1, 0, 0, // stem right
|
|
0, -1, 0, // bottom
|
|
-1, 0, 0, // left
|
|
];
|
|
|
|
const numVertices = indices.length;
|
|
const vertexData = new Float32Array(numVertices * 6); // xyz + normal
|
|
|
|
for (let i = 0; i < indices.length; ++i) {
|
|
const positionNdx = indices[i] * 3;
|
|
const position = positions.slice(positionNdx, positionNdx + 3);
|
|
vertexData.set(position, i * 6);
|
|
|
|
const quadNdx = (i / 6 | 0) * 3;
|
|
const normal = normals.slice(quadNdx, quadNdx + 3);
|
|
vertexData.set(normal, i * 6 + 3);
|
|
}
|
|
|
|
return {
|
|
vertexData,
|
|
numVertices,
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const adapter = await navigator.gpu?.requestAdapter();
|
|
const device = await adapter?.requestDevice();
|
|
if (!device) {
|
|
fail('need a browser that supports WebGPU');
|
|
return;
|
|
}
|
|
|
|
// Get a WebGPU context from the canvas and configure it
|
|
const canvas = document.querySelector('canvas');
|
|
const context = canvas.getContext('webgpu');
|
|
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
|
context.configure({
|
|
device,
|
|
format: presentationFormat,
|
|
});
|
|
|
|
const module = device.createShaderModule({
|
|
code: /* wgsl */ `
|
|
struct Uniforms {
|
|
normalMatrix: mat3x3f,
|
|
worldViewProjection: mat4x4f,
|
|
world: mat4x4f,
|
|
color: vec4f,
|
|
lightWorldPosition: vec3f,
|
|
viewWorldPosition: vec3f,
|
|
shininess: f32,
|
|
lightDirection: vec3f,
|
|
innerLimit: f32,
|
|
outerLimit: f32,
|
|
};
|
|
|
|
struct Vertex {
|
|
@location(0) position: vec4f,
|
|
@location(1) normal: vec3f,
|
|
};
|
|
|
|
struct VSOutput {
|
|
@builtin(position) position: vec4f,
|
|
@location(0) normal: vec3f,
|
|
@location(1) surfaceToLight: vec3f,
|
|
@location(2) surfaceToView: vec3f,
|
|
};
|
|
|
|
@group(0) @binding(0) var<uniform> uni: Uniforms;
|
|
|
|
@vertex fn vs(vert: Vertex) -> VSOutput {
|
|
var vsOut: VSOutput;
|
|
vsOut.position = uni.worldViewProjection * vert.position;
|
|
|
|
// Orient the normals and pass to the fragment shader
|
|
vsOut.normal = uni.normalMatrix * vert.normal;
|
|
|
|
// Compute the world position of the surface
|
|
let surfaceWorldPosition = (uni.world * vert.position).xyz;
|
|
|
|
// Compute the vector of the surface to the light
|
|
// and pass it to the fragment shader
|
|
vsOut.surfaceToLight = uni.lightWorldPosition - surfaceWorldPosition;
|
|
|
|
// Compute the vector of the surface to the light
|
|
// and pass it to the fragment shader
|
|
vsOut.surfaceToView = uni.viewWorldPosition - surfaceWorldPosition;
|
|
|
|
return vsOut;
|
|
}
|
|
|
|
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
|
|
// Because vsOut.normal is an inter-stage variable
|
|
// it's interpolated so it will not be a unit vector.
|
|
// Normalizing it will make it a unit vector again
|
|
let normal = normalize(vsOut.normal);
|
|
|
|
let surfaceToLightDirection = normalize(vsOut.surfaceToLight);
|
|
let surfaceToViewDirection = normalize(vsOut.surfaceToView);
|
|
let halfVector = normalize(
|
|
surfaceToLightDirection + surfaceToViewDirection);
|
|
|
|
let dotFromDirection = dot(surfaceToLightDirection, -uni.lightDirection);
|
|
let inLight = smoothstep(uni.outerLimit, uni.innerLimit, dotFromDirection);
|
|
|
|
// Compute the light by taking the dot product
|
|
// of the normal with the direction to the light
|
|
let light = inLight * dot(normal, surfaceToLightDirection);
|
|
|
|
var specular = dot(normal, halfVector);
|
|
specular = inLight * select(
|
|
0.0, // value if condition false
|
|
pow(specular, uni.shininess), // value if condition is true
|
|
specular > 0.0); // condition
|
|
|
|
// Lets multiply just the color portion (not the alpha)
|
|
// by the light
|
|
let color = uni.color.rgb * light + specular;
|
|
return vec4f(color, uni.color.a);
|
|
}
|
|
`,
|
|
});
|
|
|
|
const pipeline = device.createRenderPipeline({
|
|
label: '2 attributes',
|
|
layout: 'auto',
|
|
vertex: {
|
|
module,
|
|
buffers: [
|
|
{
|
|
arrayStride: (3 + 3) * 4, // (3+3) floats 4 bytes each
|
|
attributes: [
|
|
{shaderLocation: 0, offset: 0, format: 'float32x3'}, // position
|
|
{shaderLocation: 1, offset: 12, format: 'float32x3'}, // normal
|
|
],
|
|
},
|
|
],
|
|
},
|
|
fragment: {
|
|
module,
|
|
targets: [{ format: presentationFormat }],
|
|
},
|
|
primitive: {
|
|
cullMode: 'back',
|
|
},
|
|
depthStencil: {
|
|
depthWriteEnabled: true,
|
|
depthCompare: 'less',
|
|
format: 'depth24plus',
|
|
},
|
|
});
|
|
|
|
const uniformBufferSize = (12 + 16 + 16 + 4 + 4 + 4 + 4 + 4) * 4;
|
|
const uniformBuffer = device.createBuffer({
|
|
label: 'uniforms',
|
|
size: uniformBufferSize,
|
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
});
|
|
|
|
const uniformValues = new Float32Array(uniformBufferSize / 4);
|
|
|
|
// offsets to the various uniform values in float32 indices
|
|
const kNormalMatrixOffset = 0;
|
|
const kWorldViewProjectionOffset = 12;
|
|
const kWorldOffset = 28;
|
|
const kColorOffset = 44;
|
|
const kLightWorldPositionOffset = 48;
|
|
const kViewWorldPositionOffset = 52;
|
|
const kShininessOffset = 55;
|
|
const kLightDirectionOffset = 56;
|
|
const kInnerLimitOffset = 59;
|
|
const kOuterLimitOffset = 60;
|
|
|
|
const normalMatrixValue = uniformValues.subarray(
|
|
kNormalMatrixOffset, kNormalMatrixOffset + 12);
|
|
const worldViewProjectionValue = uniformValues.subarray(
|
|
kWorldViewProjectionOffset, kWorldViewProjectionOffset + 16);
|
|
const worldValue = uniformValues.subarray(
|
|
kWorldOffset, kWorldOffset + 16);
|
|
const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
|
|
const lightWorldPositionValue = uniformValues.subarray(
|
|
kLightWorldPositionOffset, kLightWorldPositionOffset + 3);
|
|
const viewWorldPositionValue = uniformValues.subarray(
|
|
kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
|
|
const shininessValue = uniformValues.subarray(
|
|
kShininessOffset, kShininessOffset + 1);
|
|
const lightDirectionValue = uniformValues.subarray(
|
|
kLightDirectionOffset, kLightDirectionOffset + 3);
|
|
const innerLimitValue = uniformValues.subarray(
|
|
kInnerLimitOffset, kInnerLimitOffset + 1);
|
|
const outerLimitValue = uniformValues.subarray(
|
|
kOuterLimitOffset, kOuterLimitOffset + 1);
|
|
|
|
const bindGroup = device.createBindGroup({
|
|
label: 'bind group for object',
|
|
layout: pipeline.getBindGroupLayout(0),
|
|
entries: [
|
|
{ binding: 0, resource: uniformBuffer },
|
|
],
|
|
});
|
|
|
|
const { vertexData, numVertices } = createFVertices();
|
|
const vertexBuffer = device.createBuffer({
|
|
label: 'vertex buffer vertices',
|
|
size: vertexData.byteLength,
|
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
});
|
|
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
|
|
|
|
const renderPassDescriptor = {
|
|
label: 'our basic canvas renderPass',
|
|
colorAttachments: [
|
|
{
|
|
// view: <- to be filled out when we render
|
|
loadOp: 'clear',
|
|
storeOp: 'store',
|
|
},
|
|
],
|
|
depthStencilAttachment: {
|
|
// view: <- to be filled out when we render
|
|
depthClearValue: 1.0,
|
|
depthLoadOp: 'clear',
|
|
depthStoreOp: 'store',
|
|
},
|
|
};
|
|
|
|
const degToRad = d => d * Math.PI / 180;
|
|
|
|
const settings = {
|
|
rotation: degToRad(0),
|
|
shininess: 30,
|
|
innerLimit: degToRad(15),
|
|
outerLimit: degToRad(70),
|
|
aimOffsetX: -10,
|
|
aimOffsetY: 10,
|
|
};
|
|
|
|
const radToDegOptions = { min: -360, max: 360, step: 1, converters: GUI.converters.radToDeg };
|
|
const limitOptions = { min: 0, max: 90, minRange: 1, step: 1, converters: GUI.converters.radToDeg };
|
|
|
|
const gui = new GUI();
|
|
gui.onChange(render);
|
|
gui.add(settings, 'rotation', radToDegOptions);
|
|
gui.add(settings, 'shininess', { min: 1, max: 250 });
|
|
GUI.makeMinMaxPair(gui, settings, 'innerLimit', 'outerLimit', limitOptions);
|
|
gui.add(settings, 'aimOffsetX', -50, 50);
|
|
gui.add(settings, 'aimOffsetY', -50, 50);
|
|
|
|
let depthTexture;
|
|
|
|
function render() {
|
|
// Get the current texture from the canvas context and
|
|
// set it as the texture to render to.
|
|
const canvasTexture = context.getCurrentTexture();
|
|
renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView();
|
|
|
|
// If we don't have a depth texture OR if its size is different
|
|
// from the canvasTexture when make a new depth texture
|
|
if (!depthTexture ||
|
|
depthTexture.width !== canvasTexture.width ||
|
|
depthTexture.height !== canvasTexture.height) {
|
|
if (depthTexture) {
|
|
depthTexture.destroy();
|
|
}
|
|
depthTexture = device.createTexture({
|
|
size: [canvasTexture.width, canvasTexture.height],
|
|
format: 'depth24plus',
|
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
});
|
|
}
|
|
renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
|
|
|
|
const encoder = device.createCommandEncoder();
|
|
const pass = encoder.beginRenderPass(renderPassDescriptor);
|
|
pass.setPipeline(pipeline);
|
|
pass.setVertexBuffer(0, vertexBuffer);
|
|
|
|
const aspect = canvas.clientWidth / canvas.clientHeight;
|
|
const projection = mat4.perspective(
|
|
degToRad(60),
|
|
aspect,
|
|
1, // zNear
|
|
2000, // zFar
|
|
);
|
|
|
|
const eye = [100, 150, 200];
|
|
const target = [0, 35, 0];
|
|
const up = [0, 1, 0];
|
|
|
|
// Compute a view matrix
|
|
const viewMatrix = mat4.lookAt(eye, target, up);
|
|
|
|
// Combine the view and projection matrixes
|
|
const viewProjectionMatrix = mat4.multiply(projection, viewMatrix);
|
|
|
|
// Compute a world matrix
|
|
const world = mat4.rotationY(settings.rotation, worldValue);
|
|
|
|
// Combine the viewProjection and world matrices
|
|
mat4.multiply(viewProjectionMatrix, world, worldViewProjectionValue);
|
|
|
|
// Inverse and transpose it into the worldInverseTranspose value
|
|
mat3.fromMat4(mat4.transpose(mat4.inverse(world)), normalMatrixValue);
|
|
|
|
colorValue.set([0.2, 1, 0.2, 1]); // green
|
|
lightWorldPositionValue.set([-10, 30, 100]);
|
|
viewWorldPositionValue.set(eye);
|
|
shininessValue[0] = settings.shininess;
|
|
innerLimitValue[0] = Math.cos(settings.innerLimit);
|
|
outerLimitValue[0] = Math.cos(settings.outerLimit);
|
|
|
|
// Since we don't have a plane like most spotlight examples
|
|
// let's point the spot light at the F
|
|
{
|
|
const mat = mat4.aim(
|
|
lightWorldPositionValue,
|
|
[
|
|
target[0] + settings.aimOffsetX,
|
|
target[1] + settings.aimOffsetY,
|
|
0,
|
|
],
|
|
up);
|
|
// get the zAxis from the matrix
|
|
// negate it because lookAt looks down the -Z axis
|
|
lightDirectionValue.set(mat.slice(8, 11));
|
|
}
|
|
|
|
// upload the uniform values to the uniform buffer
|
|
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
|
|
|
|
pass.setBindGroup(0, bindGroup);
|
|
pass.draw(numVertices);
|
|
pass.end();
|
|
|
|
const commandBuffer = encoder.finish();
|
|
device.queue.submit([commandBuffer]);
|
|
}
|
|
|
|
const observer = new ResizeObserver(entries => {
|
|
for (const entry of entries) {
|
|
const canvas = entry.target;
|
|
const width = entry.contentBoxSize[0].inlineSize;
|
|
const height = entry.contentBoxSize[0].blockSize;
|
|
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
|
|
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
|
|
// re-render
|
|
render();
|
|
}
|
|
});
|
|
observer.observe(canvas);
|
|
}
|
|
|
|
function fail(msg) {
|
|
alert(msg);
|
|
}
|
|
|
|
main();
|
|
</script>
|
|
</html>
|