Files
webgpufundamentals/webgpu/webgpu-camera-visualization.html
Gregg Tavares dcfd514975 Revert "Use direct attachment binding"
This reverts commit bdce233195.
2026-02-04 12:17:44 -08:00

719 lines
20 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 Perspective - perspective - Step 4</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%;
}
.fill-container {
width: 100%;
height: 100%;
}
.split-horizontal {
display: flex;
justify-content: center;
align-items: stretch;
}
.split-horizontal>* {
flex: 1 1 auto;
min-width: 0;
}
:root {
--bg-color: #fff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #000;
}
}
canvas {
background-color: var(--bg-color);
}
</style>
</head>
<body>
<div class="split-horizontal fill-container">
<canvas id="main"></canvas>
<canvas id="alt"></canvas>
</div>
</body>
<script type="module">
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#wgpu-matrix
import {mat4} from '../3rdparty/wgpu-matrix.module.js';
import GUI from '../3rdparty/muigui-0.x.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 quadColors = [
200, 70, 120, // left column front
200, 70, 120, // top rung front
200, 70, 120, // middle rung front
80, 70, 200, // left column back
80, 70, 200, // top rung back
80, 70, 200, // middle rung back
70, 200, 210, // top
160, 160, 220, // top rung right
90, 130, 110, // top rung bottom
200, 200, 70, // between top and middle rung
210, 100, 70, // middle rung top
210, 160, 70, // middle rung right
70, 180, 210, // middle rung bottom
100, 70, 210, // stem right
76, 210, 100, // bottom
140, 210, 80, // left
];
const numVertices = indices.length;
const vertexData = new Float32Array(numVertices * 4); // xyz + color
const colorData = new Uint8Array(vertexData.buffer);
for (let i = 0; i < indices.length; ++i) {
const positionNdx = indices[i] * 3;
const position = positions.slice(positionNdx, positionNdx + 3);
vertexData.set(position, i * 4);
const quadNdx = (i / 6 | 0) * 3;
const color = quadColors.slice(quadNdx, quadNdx + 3);
colorData.set(color, i * 16 + 12);
colorData[i * 16 + 15] = 255;
}
return {
vertexData,
numVertices,
};
}
function createCameraFrustumVertices() {
const positions = [
-1, -1, 0, // cube vertices
1, -1, 0,
-1, 1, 0,
1, 1, 0,
-1, -1, 1,
1, -1, 1,
-1, 1, 1,
1, 1, 1,
];
const indices = [
0, 1, 1, 3, 3, 2, 2, 0, // cube indices
4, 5, 5, 7, 7, 6, 6, 4,
0, 4, 1, 5, 3, 7, 2, 6,
];
return {
vertexData: new Float32Array(positions),
indexData: new Uint16Array(indices),
};
}
// create geometry for a camera
function createCameraVertices() {
// first let's add a cube. It goes from 1 to 3
// because cameras look down -Z so we want
// the camera to start at Z = 0.
// We'll put a cone in front of this cube opening
// toward -Z
const positions = [
-1, -1, 1, // cube vertices
1, -1, 1,
-1, 1, 1,
1, 1, 1,
-1, -1, 3,
1, -1, 3,
-1, 1, 3,
1, 1, 3,
0, 0, 1, // cone tip
];
const indices = [
0, 1, 1, 3, 3, 2, 2, 0, // cube indices
4, 5, 5, 7, 7, 6, 6, 4,
0, 4, 1, 5, 3, 7, 2, 6,
];
// add cone segments
const numSegments = 6;
const coneBaseIndex = positions.length / 3;
const coneTipIndex = coneBaseIndex - 1;
for (let i = 0; i < numSegments; ++i) {
const u = i / numSegments;
const angle = u * Math.PI * 2;
const x = Math.cos(angle);
const y = Math.sin(angle);
positions.push(x, y, 0);
// line from tip to edge
indices.push(coneTipIndex, coneBaseIndex + i);
// line from point on edge to next point on edge
indices.push(coneBaseIndex + i, coneBaseIndex + (i + 1) % numSegments);
}
return {
vertexData: new Float32Array(positions),
indexData: new Uint16Array(indices),
};
}
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
fail('need a browser that supports WebGPU');
return;
}
const degToRad = d => d * Math.PI / 180;
const settings = {
fieldOfView: degToRad(60),
camera: [-65, 100, 120],
target: [0, 0, 0],
near: 1,
far: 250,
};
const limitOptions = { min: 1, max: 500, minRange: 1, step: 1 };
const gui = new GUI();
GUI.setTheme('float');
gui.onChange(render);
gui.add(settings, 'fieldOfView', {min: 1, max: 179, converters: GUI.converters.radToDeg});
GUI.makeMinMaxPair(gui, settings, 'near', 'far', limitOptions);
gui.add(settings.camera, '0', -300, 300).name('camera.x');
gui.add(settings.camera, '1', -300, 300).name('camera.y');
gui.add(settings.camera, '2', -300, 300).name('camera.z');
gui.add(settings.target, '0', -300, 300).name('target.x');
gui.add(settings.target, '1', -300, 300).name('target.y');
gui.add(settings.target, '2', -300, 300).name('target.z');
const mainCanvas = document.querySelector('#main');
const altCanvas = document.querySelector('#alt');
const canvases = new Map([
[
mainCanvas,
{
clearValue: [0.3, 0.3, 0.3, 1.0],
drawCamera: false,
settings,
},
],
[
altCanvas,
{
clearValue: [0.5, 0.5, 0.5, 1.0],
drawCamera: true,
settings: {
fieldOfView: degToRad(60),
camera: [300, 400, 700],
target: [0, 75, 0],
near: 1,
far: 2000,
},
},
],
]);
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
// Get a WebGPU context from the canvas and configure it
canvases.forEach((info, canvas) => {
const context = canvas.getContext('webgpu');
context.configure({
device,
format: presentationFormat,
});
info.context = context;
});
const module = device.createShaderModule({
code: /* wgsl */ `
struct Uniforms {
matrix: mat4x4f,
};
struct Vertex {
@location(0) position: vec4f,
@location(1) color: vec4f,
};
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
@group(0) @binding(0) var<uniform> uni: Uniforms;
@vertex fn vs(vert: Vertex) -> VSOutput {
var vsOut: VSOutput;
vsOut.position = uni.matrix * vert.position;
vsOut.color = vert.color;
return vsOut;
}
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
return vsOut.color;
}
`,
});
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { } },
],
});
const layout = device.createPipelineLayout({
bindGroupLayouts: [
bindGroupLayout,
],
});
const pipeline = device.createRenderPipeline({
label: '2 attributes',
layout,
vertex: {
module,
buffers: [
{
arrayStride: (4) * 4, // (3) floats 4 bytes each + one 4 byte color
attributes: [
{shaderLocation: 0, offset: 0, format: 'float32x3'}, // position
{shaderLocation: 1, offset: 12, format: 'unorm8x4'}, // color
],
},
],
},
fragment: {
module,
targets: [{ format: presentationFormat }],
},
primitive: {
cullMode: 'back',
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
},
});
const linePipeline = device.createRenderPipeline({
label: '2 attributes line-list',
layout,
vertex: {
module,
buffers: [
{
arrayStride: (3) * 4, // (3) floats 4 bytes each
attributes: [
{shaderLocation: 0, offset: 0, format: 'float32x3'}, // position
],
},
{
arrayStride: 0, // repeat
attributes: [
{shaderLocation: 1, offset: 0, format: 'unorm8x4'}, // color
],
},
],
},
fragment: {
module,
targets: [{ format: presentationFormat }],
},
primitive: {
topology: 'line-list',
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
},
});
// matrix
const uniformBufferSize = (16) * 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 kMatrixOffset = 0;
const matrixValue = uniformValues.subarray(kMatrixOffset, kMatrixOffset + 16);
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 cameraResources = (() => {
const { vertexData, indexData } = createCameraVertices();
const vertexBuffer = device.createBuffer({
label: 'camera vertex buffer',
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
// A color we'll pass in as vertex colors but with arrayStride = 0
// so it will just use the same value for every vertex.
const colorBuffer = device.createBuffer({
label: 'camera vertex buffer',
size: 4,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(colorBuffer, 0, new Uint8Array([1, 1, 0, 1]));
const indexBuffer = device.createBuffer({
label: 'camera index buffer',
size: indexData.byteLength,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(indexBuffer, 0, indexData);
const uniformBufferSize = (16) * 4;
const uniformBuffer = device.createBuffer({
label: 'uniforms',
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const uniformValues = new Float32Array(uniformBufferSize / 4);
const matrixValue = uniformValues.subarray(kMatrixOffset, kMatrixOffset + 16);
const bindGroup = device.createBindGroup({
label: 'bind group for camera',
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: uniformBuffer },
],
});
return {
vertexBuffer,
colorBuffer,
indexBuffer,
indexFormat: 'uint16',
numElements: indexData.length,
uniformBuffer,
uniformValues,
matrixValue,
bindGroup,
};
})();
const frustumResources = (() => {
const { vertexData, indexData } = createCameraFrustumVertices();
const vertexBuffer = device.createBuffer({
label: 'camera vertex buffer',
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
// A color we'll pass in as vertex colors but with arrayStride = 0
// so it will just use the same value for every vertex.
const colorBuffer = device.createBuffer({
label: 'camera vertex buffer',
size: 4,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(colorBuffer, 0, new Uint8Array([1, 1, 0, 1]));
const indexBuffer = device.createBuffer({
label: 'camera index buffer',
size: indexData.byteLength,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(indexBuffer, 0, indexData);
const uniformBufferSize = (16) * 4;
const uniformBuffer = device.createBuffer({
label: 'uniforms',
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const uniformValues = new Float32Array(uniformBufferSize / 4);
const matrixValue = uniformValues.subarray(kMatrixOffset, kMatrixOffset + 16);
const bindGroup = device.createBindGroup({
label: 'bind group for camera',
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: uniformBuffer },
],
});
return {
vertexBuffer,
colorBuffer,
indexBuffer,
indexFormat: 'uint16',
numElements: indexData.length,
uniformBuffer,
uniformValues,
matrixValue,
bindGroup,
};
})();
const bindGroup = device.createBindGroup({
label: 'bind group for object',
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: uniformBuffer },
],
});
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',
},
};
let depthTexture;
function render() {
canvases.forEach((_, canvas) => renderCanvas(canvas));
}
function renderCanvas(canvas) {
const canvasInfo = canvases.get(canvas);
const {
context,
clearValue,
drawCamera,
settings,
} = canvasInfo;
// 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();
renderPassDescriptor.colorAttachments[0].clearValue = clearValue;
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(
settings.fieldOfView,
aspect,
settings.near,
settings.far
);
// Compute a view matrix
const up = [0, 1, 0];
const viewMatrix = mat4.lookAt(settings.camera, settings.target, up);
mat4.multiply(projection, viewMatrix, matrixValue);
canvasInfo.projection = projection;
canvasInfo.viewMatrix = viewMatrix;
// upload the uniform values to the uniform buffer
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
pass.setBindGroup(0, bindGroup);
pass.draw(numVertices);
if (drawCamera) {
{
const {
indexBuffer,
indexFormat,
vertexBuffer,
colorBuffer,
numElements,
uniformBuffer,
uniformValues,
bindGroup,
matrixValue,
} = cameraResources;
// use the first's camera's matrix as the matrix to position
// the camera's representative in the scene
const {viewMatrix: mainViewMatrix} = canvases.get(mainCanvas);
const mainCameraMatrix = mat4.inverse(mainViewMatrix);
mat4.multiply(projection, viewMatrix, matrixValue);
mat4.multiply(matrixValue, mainCameraMatrix, matrixValue);
mat4.scale(matrixValue, [20, 20, 20], matrixValue);
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
pass.setPipeline(linePipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setVertexBuffer(1, colorBuffer);
pass.setIndexBuffer(indexBuffer, indexFormat);
pass.setBindGroup(0, bindGroup);
pass.drawIndexed(numElements);
}
{
const {
indexBuffer,
indexFormat,
vertexBuffer,
colorBuffer,
numElements,
uniformBuffer,
uniformValues,
bindGroup,
matrixValue,
} = frustumResources;
// use the first's camera's matrix as the matrix to position
// the camera's representative in the scene
const {viewMatrix: mainViewMatrix, projection: mainProjection} = canvases.get(mainCanvas);
const mainCameraMatrix = mat4.inverse(mainViewMatrix);
mat4.multiply(projection, viewMatrix, matrixValue);
mat4.multiply(matrixValue, mainCameraMatrix, matrixValue);
mat4.multiply(matrixValue, mat4.inverse(mainProjection), matrixValue);
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
pass.setPipeline(linePipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setVertexBuffer(1, colorBuffer);
pass.setIndexBuffer(indexBuffer, indexFormat);
pass.setBindGroup(0, bindGroup);
pass.drawIndexed(numElements);
}
}
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
renderCanvas(canvas);
}
});
canvases.forEach((_, canvas) => {
observer.observe(canvas);
});
}
function fail(msg) {
alert(msg);
}
main();
</script>
</html>