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

1579 lines
43 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 OrbitCamera - no scene graph</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%;
touch-action: none;
}
:root {
--bg-color: #fff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #000;
}
}
canvas {
background-color: var(--bg-color);
}
#split {
display: flex;
height: 100%;
}
#ui {
border-left: 1px solid #888;
}
#ui.hide-ui {
right: 0;
position: absolute;
}
#split > :nth-child(1) {
flex: 1 1 auto;
min-width: 0;
}
</style>
</head>
<body>
<div id="split">
<canvas></canvas>
<div id="ui"></div>
</div>
</body>
<script type="module">
/* eslint-disable brace-style */
import GUI from '../3rdparty/muigui-0.x.module.js';
import { addButtonLeftJustified } from './resources/js/gui-helpers.js';
function createCubeVertices() {
const positions = [
// left
0, 0, 0,
0, 0, -1,
0, 1, 0,
0, 1, -1,
// right
1, 0, 0,
1, 0, -1,
1, 1, 0,
1, 1, -1,
];
const indices = [
0, 2, 1, 2, 3, 1, // left
4, 5, 6, 6, 5, 7, // right
0, 4, 2, 2, 4, 6, // front
1, 3, 5, 5, 3, 7, // back
0, 1, 4, 4, 1, 5, // bottom
2, 6, 3, 3, 6, 7, // top
];
const quadColors = [
200, 70, 120, // left column front
80, 70, 200, // left column 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
];
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,
aabb: {
min: [ 0, 0, -1],
max: [ 1, 1, 0],
},
};
}
const vec3 = {
create() {
return new Float32Array(3);
},
copy(src, dst) {
dst = dst || new Float32Array(3);
dst.set(src);
return dst;
},
cross(a, b, dst) {
dst = dst || new Float32Array(3);
const t0 = a[1] * b[2] - a[2] * b[1];
const t1 = a[2] * b[0] - a[0] * b[2];
const t2 = a[0] * b[1] - a[1] * b[0];
dst[0] = t0;
dst[1] = t1;
dst[2] = t2;
return dst;
},
subtract(a, b, dst) {
dst = dst || new Float32Array(3);
dst[0] = a[0] - b[0];
dst[1] = a[1] - b[1];
dst[2] = a[2] - b[2];
return dst;
},
normalize(v, dst) {
dst = dst || new Float32Array(3);
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
// make sure we don't divide by 0.
if (length > 0.00001) {
dst[0] = v[0] / length;
dst[1] = v[1] / length;
dst[2] = v[2] / length;
} else {
dst[0] = 0;
dst[1] = 0;
dst[2] = 0;
}
return dst;
},
getTranslation(m, dst) {
dst = dst || new Float32Array(3);
dst[0] = m[12];
dst[1] = m[13];
dst[2] = m[14];
return dst;
},
add(a, b, dst) {
dst = dst || new Float32Array(3);
dst[0] = a[0] + b[0];
dst[1] = a[1] + b[1];
dst[2] = a[2] + b[2];
return dst;
},
addScaled(a, b, scale, dst) {
dst = dst || new Float32Array(3);
dst[0] = a[0] + b[0] * scale;
dst[1] = a[1] + b[1] * scale;
dst[2] = a[2] + b[2] * scale;
return dst;
},
min(a, b, dst) {
dst = dst ?? new Float32Array(3);
dst[0] = Math.min(a[0], b[0]);
dst[1] = Math.min(a[1], b[1]);
dst[2] = Math.min(a[2], b[2]);
return dst;
},
max(a, b, dst) {
dst = dst ?? new Float32Array(3);
dst[0] = Math.max(a[0], b[0]);
dst[1] = Math.max(a[1], b[1]);
dst[2] = Math.max(a[2], b[2]);
return dst;
},
distance(a, b) {
const dx = a[0] - b[0];
const dy = a[1] - b[1];
const dz = a[2] - b[2];
return Math.sqrt(dx * dx + dy * dy + dz * dz);
},
transformMat3(v, m, dst) {
dst = dst ?? new Float32Array(3);
const x = v[0];
const y = v[1];
const z = v[2];
dst[0] = x * m[0] + y * m[4] + z * m[8];
dst[1] = x * m[1] + y * m[5] + z * m[9];
dst[2] = x * m[2] + y * m[6] + z * m[10];
return dst;
},
transformMat4(v, m, dst) {
dst = dst ?? new Float32Array(3);
const x = v[0];
const y = v[1];
const z = v[2];
const w = (m[3] * x + m[7] * y + m[11] * z + m[15]) || 1;
dst[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w;
dst[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w;
dst[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w;
return dst;
},
};
const mat4 = {
copy(src, dst) {
dst = dst || new Float32Array(16);
dst.set(src);
return dst;
},
projection(width, height, depth, dst) {
// Note: This matrix flips the Y axis so that 0 is at the top.
return mat4.ortho(0, width, height, 0, depth, -depth, dst);
},
perspective(fieldOfViewYInRadians, aspect, zNear, zFar, dst) {
dst = dst || new Float32Array(16);
const f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewYInRadians);
const rangeInv = 1 / (zNear - zFar);
dst[0] = f / aspect;
dst[1] = 0;
dst[2] = 0;
dst[3] = 0;
dst[4] = 0;
dst[5] = f;
dst[6] = 0;
dst[7] = 0;
dst[8] = 0;
dst[9] = 0;
dst[10] = zFar * rangeInv;
dst[11] = -1;
dst[12] = 0;
dst[13] = 0;
dst[14] = zNear * zFar * rangeInv;
dst[15] = 0;
return dst;
},
ortho(left, right, bottom, top, near, far, dst) {
dst = dst || new Float32Array(16);
dst[0] = 2 / (right - left);
dst[1] = 0;
dst[2] = 0;
dst[3] = 0;
dst[4] = 0;
dst[5] = 2 / (top - bottom);
dst[6] = 0;
dst[7] = 0;
dst[8] = 0;
dst[9] = 0;
dst[10] = 1 / (near - far);
dst[11] = 0;
dst[12] = (right + left) / (left - right);
dst[13] = (top + bottom) / (bottom - top);
dst[14] = near / (near - far);
dst[15] = 1;
return dst;
},
identity(dst) {
dst = dst || new Float32Array(16);
dst[ 0] = 1; dst[ 1] = 0; dst[ 2] = 0; dst[ 3] = 0;
dst[ 4] = 0; dst[ 5] = 1; dst[ 6] = 0; dst[ 7] = 0;
dst[ 8] = 0; dst[ 9] = 0; dst[10] = 1; dst[11] = 0;
dst[12] = 0; dst[13] = 0; dst[14] = 0; dst[15] = 1;
return dst;
},
multiply(a, b, dst) {
dst = dst || new Float32Array(16);
const b00 = b[0 * 4 + 0];
const b01 = b[0 * 4 + 1];
const b02 = b[0 * 4 + 2];
const b03 = b[0 * 4 + 3];
const b10 = b[1 * 4 + 0];
const b11 = b[1 * 4 + 1];
const b12 = b[1 * 4 + 2];
const b13 = b[1 * 4 + 3];
const b20 = b[2 * 4 + 0];
const b21 = b[2 * 4 + 1];
const b22 = b[2 * 4 + 2];
const b23 = b[2 * 4 + 3];
const b30 = b[3 * 4 + 0];
const b31 = b[3 * 4 + 1];
const b32 = b[3 * 4 + 2];
const b33 = b[3 * 4 + 3];
const a00 = a[0 * 4 + 0];
const a01 = a[0 * 4 + 1];
const a02 = a[0 * 4 + 2];
const a03 = a[0 * 4 + 3];
const a10 = a[1 * 4 + 0];
const a11 = a[1 * 4 + 1];
const a12 = a[1 * 4 + 2];
const a13 = a[1 * 4 + 3];
const a20 = a[2 * 4 + 0];
const a21 = a[2 * 4 + 1];
const a22 = a[2 * 4 + 2];
const a23 = a[2 * 4 + 3];
const a30 = a[3 * 4 + 0];
const a31 = a[3 * 4 + 1];
const a32 = a[3 * 4 + 2];
const a33 = a[3 * 4 + 3];
dst[0] = b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30;
dst[1] = b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31;
dst[2] = b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32;
dst[3] = b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33;
dst[4] = b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30;
dst[5] = b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31;
dst[6] = b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32;
dst[7] = b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33;
dst[8] = b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30;
dst[9] = b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31;
dst[10] = b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32;
dst[11] = b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33;
dst[12] = b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30;
dst[13] = b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31;
dst[14] = b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32;
dst[15] = b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33;
return dst;
},
inverse(m, dst) {
dst = dst || new Float32Array(16);
const m00 = m[0 * 4 + 0];
const m01 = m[0 * 4 + 1];
const m02 = m[0 * 4 + 2];
const m03 = m[0 * 4 + 3];
const m10 = m[1 * 4 + 0];
const m11 = m[1 * 4 + 1];
const m12 = m[1 * 4 + 2];
const m13 = m[1 * 4 + 3];
const m20 = m[2 * 4 + 0];
const m21 = m[2 * 4 + 1];
const m22 = m[2 * 4 + 2];
const m23 = m[2 * 4 + 3];
const m30 = m[3 * 4 + 0];
const m31 = m[3 * 4 + 1];
const m32 = m[3 * 4 + 2];
const m33 = m[3 * 4 + 3];
const tmp0 = m22 * m33;
const tmp1 = m32 * m23;
const tmp2 = m12 * m33;
const tmp3 = m32 * m13;
const tmp4 = m12 * m23;
const tmp5 = m22 * m13;
const tmp6 = m02 * m33;
const tmp7 = m32 * m03;
const tmp8 = m02 * m23;
const tmp9 = m22 * m03;
const tmp10 = m02 * m13;
const tmp11 = m12 * m03;
const tmp12 = m20 * m31;
const tmp13 = m30 * m21;
const tmp14 = m10 * m31;
const tmp15 = m30 * m11;
const tmp16 = m10 * m21;
const tmp17 = m20 * m11;
const tmp18 = m00 * m31;
const tmp19 = m30 * m01;
const tmp20 = m00 * m21;
const tmp21 = m20 * m01;
const tmp22 = m00 * m11;
const tmp23 = m10 * m01;
const t0 = (tmp0 * m11 + tmp3 * m21 + tmp4 * m31) -
(tmp1 * m11 + tmp2 * m21 + tmp5 * m31);
const t1 = (tmp1 * m01 + tmp6 * m21 + tmp9 * m31) -
(tmp0 * m01 + tmp7 * m21 + tmp8 * m31);
const t2 = (tmp2 * m01 + tmp7 * m11 + tmp10 * m31) -
(tmp3 * m01 + tmp6 * m11 + tmp11 * m31);
const t3 = (tmp5 * m01 + tmp8 * m11 + tmp11 * m21) -
(tmp4 * m01 + tmp9 * m11 + tmp10 * m21);
const d = 1 / (m00 * t0 + m10 * t1 + m20 * t2 + m30 * t3);
dst[0] = d * t0;
dst[1] = d * t1;
dst[2] = d * t2;
dst[3] = d * t3;
dst[4] = d * ((tmp1 * m10 + tmp2 * m20 + tmp5 * m30) -
(tmp0 * m10 + tmp3 * m20 + tmp4 * m30));
dst[5] = d * ((tmp0 * m00 + tmp7 * m20 + tmp8 * m30) -
(tmp1 * m00 + tmp6 * m20 + tmp9 * m30));
dst[6] = d * ((tmp3 * m00 + tmp6 * m10 + tmp11 * m30) -
(tmp2 * m00 + tmp7 * m10 + tmp10 * m30));
dst[7] = d * ((tmp4 * m00 + tmp9 * m10 + tmp10 * m20) -
(tmp5 * m00 + tmp8 * m10 + tmp11 * m20));
dst[8] = d * ((tmp12 * m13 + tmp15 * m23 + tmp16 * m33) -
(tmp13 * m13 + tmp14 * m23 + tmp17 * m33));
dst[9] = d * ((tmp13 * m03 + tmp18 * m23 + tmp21 * m33) -
(tmp12 * m03 + tmp19 * m23 + tmp20 * m33));
dst[10] = d * ((tmp14 * m03 + tmp19 * m13 + tmp22 * m33) -
(tmp15 * m03 + tmp18 * m13 + tmp23 * m33));
dst[11] = d * ((tmp17 * m03 + tmp20 * m13 + tmp23 * m23) -
(tmp16 * m03 + tmp21 * m13 + tmp22 * m23));
dst[12] = d * ((tmp14 * m22 + tmp17 * m32 + tmp13 * m12) -
(tmp16 * m32 + tmp12 * m12 + tmp15 * m22));
dst[13] = d * ((tmp20 * m32 + tmp12 * m02 + tmp19 * m22) -
(tmp18 * m22 + tmp21 * m32 + tmp13 * m02));
dst[14] = d * ((tmp18 * m12 + tmp23 * m32 + tmp15 * m02) -
(tmp22 * m32 + tmp14 * m02 + tmp19 * m12));
dst[15] = d * ((tmp22 * m22 + tmp16 * m02 + tmp21 * m12) -
(tmp20 * m12 + tmp23 * m22 + tmp17 * m02));
return dst;
},
aim(eye, target, up, dst) {
dst = dst || new Float32Array(16);
const zAxis = vec3.normalize(vec3.subtract(target, eye));
const xAxis = vec3.normalize(vec3.cross(up, zAxis));
const yAxis = vec3.normalize(vec3.cross(zAxis, xAxis));
dst[ 0] = xAxis[0]; dst[ 1] = xAxis[1]; dst[ 2] = xAxis[2]; dst[ 3] = 0;
dst[ 4] = yAxis[0]; dst[ 5] = yAxis[1]; dst[ 6] = yAxis[2]; dst[ 7] = 0;
dst[ 8] = zAxis[0]; dst[ 9] = zAxis[1]; dst[10] = zAxis[2]; dst[11] = 0;
dst[12] = eye[0]; dst[13] = eye[1]; dst[14] = eye[2]; dst[15] = 1;
return dst;
},
cameraAim(eye, target, up, dst) {
dst = dst || new Float32Array(16);
const zAxis = vec3.normalize(vec3.subtract(eye, target));
const xAxis = vec3.normalize(vec3.cross(up, zAxis));
const yAxis = vec3.normalize(vec3.cross(zAxis, xAxis));
dst[ 0] = xAxis[0]; dst[ 1] = xAxis[1]; dst[ 2] = xAxis[2]; dst[ 3] = 0;
dst[ 4] = yAxis[0]; dst[ 5] = yAxis[1]; dst[ 6] = yAxis[2]; dst[ 7] = 0;
dst[ 8] = zAxis[0]; dst[ 9] = zAxis[1]; dst[10] = zAxis[2]; dst[11] = 0;
dst[12] = eye[0]; dst[13] = eye[1]; dst[14] = eye[2]; dst[15] = 1;
return dst;
},
lookAt(eye, target, up, dst) {
return mat4.inverse(mat4.cameraAim(eye, target, up, dst), dst);
},
translation([tx, ty, tz], dst) {
dst = dst || new Float32Array(16);
dst[ 0] = 1; dst[ 1] = 0; dst[ 2] = 0; dst[ 3] = 0;
dst[ 4] = 0; dst[ 5] = 1; dst[ 6] = 0; dst[ 7] = 0;
dst[ 8] = 0; dst[ 9] = 0; dst[10] = 1; dst[11] = 0;
dst[12] = tx; dst[13] = ty; dst[14] = tz; dst[15] = 1;
return dst;
},
rotationX(angleInRadians, dst) {
const c = Math.cos(angleInRadians);
const s = Math.sin(angleInRadians);
dst = dst || new Float32Array(16);
dst[ 0] = 1; dst[ 1] = 0; dst[ 2] = 0; dst[ 3] = 0;
dst[ 4] = 0; dst[ 5] = c; dst[ 6] = s; dst[ 7] = 0;
dst[ 8] = 0; dst[ 9] = -s; dst[10] = c; dst[11] = 0;
dst[12] = 0; dst[13] = 0; dst[14] = 0; dst[15] = 1;
return dst;
},
rotationY(angleInRadians, dst) {
const c = Math.cos(angleInRadians);
const s = Math.sin(angleInRadians);
dst = dst || new Float32Array(16);
dst[ 0] = c; dst[ 1] = 0; dst[ 2] = -s; dst[ 3] = 0;
dst[ 4] = 0; dst[ 5] = 1; dst[ 6] = 0; dst[ 7] = 0;
dst[ 8] = s; dst[ 9] = 0; dst[10] = c; dst[11] = 0;
dst[12] = 0; dst[13] = 0; dst[14] = 0; dst[15] = 1;
return dst;
},
rotationZ(angleInRadians, dst) {
const c = Math.cos(angleInRadians);
const s = Math.sin(angleInRadians);
dst = dst || new Float32Array(16);
dst[ 0] = c; dst[ 1] = s; dst[ 2] = 0; dst[ 3] = 0;
dst[ 4] = -s; dst[ 5] = c; dst[ 6] = 0; dst[ 7] = 0;
dst[ 8] = 0; dst[ 9] = 0; dst[10] = 1; dst[11] = 0;
dst[12] = 0; dst[13] = 0; dst[14] = 0; dst[15] = 1;
return dst;
},
scaling([sx, sy, sz], dst) {
dst = dst || new Float32Array(16);
dst[ 0] = sx; dst[ 1] = 0; dst[ 2] = 0; dst[ 3] = 0;
dst[ 4] = 0; dst[ 5] = sy; dst[ 6] = 0; dst[ 7] = 0;
dst[ 8] = 0; dst[ 9] = 0; dst[10] = sz; dst[11] = 0;
dst[12] = 0; dst[13] = 0; dst[14] = 0; dst[15] = 1;
return dst;
},
translate(m, translation, dst) {
return mat4.multiply(m, mat4.translation(translation), dst);
},
rotateX(m, angleInRadians, dst) {
return mat4.multiply(m, mat4.rotationX(angleInRadians), dst);
},
rotateY(m, angleInRadians, dst) {
return mat4.multiply(m, mat4.rotationY(angleInRadians), dst);
},
rotateZ(m, angleInRadians, dst) {
return mat4.multiply(m, mat4.rotationZ(angleInRadians), dst);
},
scale(m, scale, dst) {
return mat4.multiply(m, mat4.scaling(scale), dst);
},
};
const degToRad = d => d * Math.PI / 180;
class SceneGraphNode {
constructor(name, source) {
this.name = name;
this.children = [];
this.localMatrix = mat4.identity();
this.worldMatrix = mat4.identity();
this.source = source;
}
addChild(child) {
child.setParent(this);
}
removeChild(child) {
child.setParent(null);
}
setParent(parent) {
// remove us from our parent
if (this.parent) {
const ndx = this.parent.children.indexOf(this);
if (ndx >= 0) {
this.parent.children.splice(ndx, 1);
}
}
// Add us to our new parent
if (parent) {
parent.children.push(this);
}
this.parent = parent;
}
updateWorldMatrix() {
// update the local matrix from its source if it has one.
this.source?.getMatrix(this.localMatrix);
if (this.parent) {
// we have a parent do the math
mat4.multiply(this.parent.worldMatrix, this.localMatrix, this.worldMatrix);
} else {
// we have no parent so just copy local to world
mat4.copy(this.localMatrix, this.worldMatrix);
}
// now process all the children
this.children.forEach(function(child) {
child.updateWorldMatrix();
});
}
}
class TRS {
constructor({
translation = [0, 0, 0],
rotation = [0, 0, 0],
scale = [1, 1, 1],
} = {}) {
this.translation = new Float32Array(translation);
this.rotation = new Float32Array(rotation);
this.scale = new Float32Array(scale);
}
getMatrix(dst) {
mat4.translation(this.translation, dst);
mat4.rotateX(dst, this.rotation[0], dst);
mat4.rotateY(dst, this.rotation[1], dst);
mat4.rotateZ(dst, this.rotation[2], dst);
mat4.scale(dst, this.scale, dst);
return dst;
}
}
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,
alphaMode: 'premultiplied',
});
const module = device.createShaderModule({
code: /* wgsl */ `
struct Uniforms {
matrix: mat4x4f,
color: vec4f,
};
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 * uni.color;
}
`,
});
const pipeline = device.createRenderPipeline({
label: '2 attributes with color',
layout: 'auto',
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 postProcessModule = device.createShaderModule({
code: /* wgsl */ `
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32,
) -> VSOutput {
var pos = array(
vec2f(-1.0, -1.0),
vec2f(-1.0, 3.0),
vec2f( 3.0, -1.0),
);
var vsOutput: VSOutput;
let xy = pos[vertexIndex];
vsOutput.position = vec4f(xy, 0.0, 1.0);
vsOutput.texcoord = xy * vec2f(0.5, -0.5) + vec2f(0.5);
return vsOutput;
}
@group(0) @binding(0) var mask: texture_2d<f32>;
fn isOnEdge(pos: vec2i) -> bool {
// Note: we need to make sure we don't use out of bounds
// texel coordinates with textureLoad as that returns
// different results on different GPUs
let size = vec2i(textureDimensions(mask, 0));
let start = max(pos - 2, vec2i(0));
let end = min(pos + 2, size);
for (var y = start.y; y <= end.y; y++) {
for (var x = start.x; x <= end.x; x++) {
let s = textureLoad(mask, vec2i(x, y), 0).a;
if (s > 0) {
return true;
}
}
}
return false;
};
@fragment fn fs2d(fsInput: VSOutput) -> @location(0) vec4f {
let pos = vec2i(fsInput.position.xy);
// get the current. If it's not 0 we're inside the selected objects
let s = textureLoad(mask, pos, 0).a;
if (s > 0) {
discard;
}
let hit = isOnEdge(pos);
if (!hit) {
discard;
}
return vec4f(1, 0.5, 0, 1);
}
`,
});
const postProcessPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module: postProcessModule },
fragment: {
module: postProcessModule,
targets: [ { format: presentationFormat }],
},
});
const postProcessRenderPassDescriptor = {
label: 'post process render pass',
colorAttachments: [
{ loadOp: 'load', storeOp: 'store' },
],
};
let postProcessBindGroup;
let lastPostProcessTexture;
function setupPostProcess(texture) {
if (!postProcessBindGroup || texture !== lastPostProcessTexture) {
lastPostProcessTexture = texture;
postProcessBindGroup = device.createBindGroup({
layout: postProcessPipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: texture },
],
});
}
}
function postProcess(encoder, srcTexture, dstTexture) {
postProcessRenderPassDescriptor.colorAttachments[0].view = dstTexture.createView();
const pass = encoder.beginRenderPass(postProcessRenderPassDescriptor);
pass.setPipeline(postProcessPipeline);
pass.setBindGroup(0, postProcessBindGroup);
pass.draw(3);
pass.end();
}
function addTRSSceneGraphNode(
name,
parent,
trs,
) {
const node = new SceneGraphNode(name, new TRS(trs));
if (parent) {
node.setParent(parent);
}
return node;
}
function addCubeNode(name, parent, trs, color) {
const node = addTRSSceneGraphNode(name, parent, trs);
return addMesh(node, cubeVertices, color);
}
const objectInfos = [];
function createObjectInfo() {
// matrix and color
const uniformBufferSize = (16 + 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 kMatrixOffset = 0;
const kColorOffset = 16;
const matrixValue = uniformValues.subarray(kMatrixOffset, kMatrixOffset + 16);
const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
const bindGroup = device.createBindGroup({
label: 'bind group for object',
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: uniformBuffer },
],
});
return {
uniformBuffer,
uniformValues,
colorValue,
matrixValue,
bindGroup,
};
}
const meshes = [];
function addMesh(node, vertices, color) {
const mesh = {
node,
vertices,
color,
};
meshes.push(mesh);
return mesh;
}
function createVertices({vertexData, numVertices, aabb}, name) {
const vertexBuffer = device.createBuffer({
label: `${name}: vertex buffer vertices`,
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
return {
vertexBuffer,
numVertices,
aabb,
};
}
const cubeVertices = createVertices(createCubeVertices(), 'cube');
const kHandleColor = [0.5, 0.5, 0.5, 1];
const kDrawerColor = [1, 1, 1, 1];
const kCabinetColor = [0.75, 0.75, 0.75, 0.75];
const kNumDrawersPerCabinet = 4;
const kNumCabinets = 5;
const kDrawerSize = [40, 30, 50];
const kHandleSize = [10, 2, 2];
const [kWidth, kHeight, kDepth] = [0, 1, 2];
const kHandlePosition = [
(kDrawerSize[kWidth] - kHandleSize[kWidth]) / 2,
kDrawerSize[kHeight] * 2 / 3,
kHandleSize[kDepth],
];
const kDrawerSpacing = kDrawerSize[kHeight] + 3;
const kCabinetSpacing = kDrawerSize[kWidth] + 10;
function addDrawer(parent, drawerNdx) {
const drawerName = `drawer${drawerNdx}`;
// add a node for the entire drawer
const drawer = addTRSSceneGraphNode(
drawerName, parent, {
translation: [3, drawerNdx * kDrawerSpacing + 5, 1],
});
// add a node with a cube for the drawer cube.
addCubeNode(`${drawerName}-drawer-mesh`, drawer, {
scale: kDrawerSize,
}, kDrawerColor);
// add a node with a cube for the handle
addCubeNode(`${drawerName}-handle-mesh`, drawer, {
translation: kHandlePosition,
scale: kHandleSize,
}, kHandleColor);
}
function addCabinet(parent, cabinetNdx) {
const cabinetName = `cabinet${cabinetNdx}`;
// add a node for the entire cabinet
const cabinet = addTRSSceneGraphNode(
cabinetName, parent, {
translation: [cabinetNdx * kCabinetSpacing, 0, 0],
});
// add a node with a cube for the cabinet
const kCabinetSize = [
kDrawerSize[kWidth] + 6,
kDrawerSpacing * kNumDrawersPerCabinet + 6,
kDrawerSize[kDepth] + 4,
];
addCubeNode(
`${cabinetName}-mesh`, cabinet, {
scale: kCabinetSize,
}, kCabinetColor);
// Add the drawers
for (let drawerNdx = 0; drawerNdx < kNumDrawersPerCabinet; ++drawerNdx) {
addDrawer(cabinet, drawerNdx);
}
}
const nodeToUISettings = new Map();
const root = new SceneGraphNode('root');
class OrbitCamera {
#target = vec3.create();
#pan = 0;
#tilt = 0;
#radius = 0;
constructor() {}
getCameraMatrix(parentMatrix) {
const mat = mat4.copy(parentMatrix ?? mat4.identity());
mat4.translate(mat, this.#target, mat);
mat4.rotateY(mat, this.#pan, mat);
mat4.rotateX(mat, this.#tilt, mat);
mat4.translate(mat, [0, 0, this.#radius], mat);
return mat;
}
getUpdateHelper() {
const startTilt = this.tilt;
const startPan = this.pan;
const startRadius = this.radius;
const startCameraMatrix = mat4.copy(this.getCameraMatrix());
const startTarget = vec3.copy(this.target);
return {
panAndTilt: (deltaPan, deltaTilt) => {
this.tilt = startTilt - deltaTilt;
this.pan = startPan - deltaPan;
},
track: (deltaX, deltaY, parentMatrix) => {
const worldDirection = vec3.transformMat3([deltaX, deltaY, 0], startCameraMatrix);
const inv = mat4.inverse(parentMatrix ?? mat4.identity());
const cameraDirection = vec3.transformMat3(worldDirection, inv);
this.target = vec3.add(startTarget, cameraDirection);
},
dolly: (delta) => {
this.radius = startRadius + delta;
},
};
}
get pan() { return this.#pan; }
set pan(v) { this.#pan = v; }
get tilt() { return this.#tilt; }
set tilt(v) { this.#tilt = v; }
get radius() { return this.#radius; }
set radius(v) { this.#radius = v; }
get target() { return vec3.copy(this.#target); }
setTarget(worldPosition, parentMatrix) {
const inv = mat4.inverse(parentMatrix ?? mat4.identity());
vec3.transformMat4(worldPosition, inv, this.#target);
}
}
const orbitCamera = new OrbitCamera();
orbitCamera.setTarget([120, 80, 0]);
orbitCamera.tilt = Math.PI * -0.2;
orbitCamera.radius = 300;
function addOrbitCameraEventListeners(cam, elem) {
let startX;
let startY;
let lastMode;
let camHelper;
let doubleTapMode;
let lastSingleTapTime;
let startPinchDistance;
const pointerToLastPosition = new Map();
const computePinchDistance = () => {
const pos = [...pointerToLastPosition.values()];
const dx = pos[0].x - pos[1].x;
const dy = pos[0].y - pos[1].y;
return Math.hypot(dx, dy);
};
const updateStartPosition = (e) => {
startX = e.clientX;
startY = e.clientY;
if (pointerToLastPosition.size === 2) {
startPinchDistance = computePinchDistance();
}
camHelper = cam.getUpdateHelper();
};
const onMove = (e) => {
if (!pointerToLastPosition.has(e.pointerId) ||
!canvas.hasPointerCapture(e.pointerId)) {
return;
}
pointerToLastPosition.set(e.pointerId, { x: e.clientX, y: e.clientY });
const mode = pointerToLastPosition.size === 2
? 'pinch'
: pointerToLastPosition.size > 2
? 'undefined'
: doubleTapMode
? 'doubleTapZoom'
: e.shiftKey
? 'track'
: 'panAndTilt';
if (mode !== lastMode) {
lastMode = mode;
updateStartPosition(e);
}
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
switch (mode) {
case 'pinch': {
const pinchDistance = computePinchDistance();
const delta = pinchDistance - startPinchDistance;
camHelper.dolly(cam.radius * 0.002 * -delta);
break;
}
case 'track': {
const s = cam.radius * 0.001;
camHelper.track(-deltaX * s, deltaY * s);
break;
}
case 'panAndTilt':
camHelper.panAndTilt(deltaX * 0.01, deltaY * 0.01);
break;
case 'doubleTapZoom':
camHelper.dolly(cam.radius * 0.002 * deltaY);
break;
}
render();
};
const onUp = (e) => {
pointerToLastPosition.delete(e.pointerId);
canvas.releasePointerCapture(e.pointerId);
if (pointerToLastPosition.size === 0) {
doubleTapMode = false;
}
};
const kDoubleClickTimeMS = 300;
const onDown = (e) => {
canvas.setPointerCapture(e.pointerId);
pointerToLastPosition.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (pointerToLastPosition.size === 1) {
if (!doubleTapMode) {
const now = performance.now();
const deltaTime = now - lastSingleTapTime;
if (deltaTime < kDoubleClickTimeMS) {
doubleTapMode = true;
}
lastSingleTapTime = now;
}
} else {
doubleTapMode = false;
}
updateStartPosition(e);
};
// Dolly when the user uses the wheel
const onWheel = (e) => {
e.preventDefault();
const helper = cam.getUpdateHelper();
helper.dolly(cam.radius * 0.001 * e.deltaY);
render();
};
elem.addEventListener('pointerup', onUp);
elem.addEventListener('pointercancel', onUp);
elem.addEventListener('lostpointercapture', onUp);
elem.addEventListener('pointerdown', onDown);
elem.addEventListener('pointermove', onMove);
elem.addEventListener('wheel', onWheel);
return () => {
elem.removeEventListener('pointerup', onUp);
elem.removeEventListener('pointercancel', onUp);
elem.removeEventListener('lostpointercapture', onUp);
elem.removeEventListener('pointerdown', onDown);
elem.removeEventListener('pointermove', onMove);
elem.removeEventListener('wheel', onWheel);
};
}
addOrbitCameraEventListeners(orbitCamera, canvas);
const cabinets = addTRSSceneGraphNode('cabinets', root);
// Add cabinets
for (let cabinetNdx = 0; cabinetNdx < kNumCabinets; ++cabinetNdx) {
addCabinet(cabinets, cabinetNdx);
}
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 selectedMeshes = [];
const settings = {
fieldOfView: degToRad(60),
showMeshNodes: false,
showAllTRS: false,
};
// Presents a TRS to the UI. Letting set which TRS
// is being edited.
class TRSUIHelper {
#trs = new TRS();
constructor() {}
setTRS(trs) {
this.#trs = trs;
}
get translationX() { return this.#trs.translation[0]; }
set translationX(x) { this.#trs.translation[0] = x; }
get translationY() { return this.#trs.translation[1]; }
set translationY(x) { this.#trs.translation[1] = x; }
get translationZ() { return this.#trs.translation[2]; }
set translationZ(x) { this.#trs.translation[2] = x; }
get rotationX() { return this.#trs.rotation[0]; }
set rotationX(x) { this.#trs.rotation[0] = x; }
get rotationY() { return this.#trs.rotation[1]; }
set rotationY(x) { this.#trs.rotation[1] = x; }
get rotationZ() { return this.#trs.rotation[2]; }
set rotationZ(x) { this.#trs.rotation[2] = x; }
get scaleX() { return this.#trs.scale[0]; }
set scaleX(x) { this.#trs.scale[0] = x; }
get scaleY() { return this.#trs.scale[1]; }
set scaleY(x) { this.#trs.scale[1] = x; }
get scaleZ() { return this.#trs.scale[2]; }
set scaleZ(x) { this.#trs.scale[2] = x; }
}
const trsUIHelper = new TRSUIHelper();
const radToDegOptions = { min: -180, max: 180, step: 1, converters: GUI.converters.radToDeg };
function meshUsesNode(mesh, node) {
if (!node) {
return false;
}
if (mesh.node === node) {
return true;
}
for (const child of node.children) {
if (meshUsesNode(mesh, child)) {
return true;
}
}
return false;
}
const kUnelected = '\u3000'; // full-width space
const kSelected = '➡️';
const prefixRE = new RegExp(`^(?:${kUnelected}|${kSelected})`);
let currentNode;
function setCurrentSceneGraphNode(node) {
currentNode = node;
trsUIHelper.setTRS(node.source);
trsFolder.name(`orientation: ${node.name}`);
trsFolder.updateDisplay();
showTRS();
// Mark which node is selected.
for (const b of nodeButtons) {
const name = b.button.getName().replace(prefixRE, '');
b.button.name(`${b.node === node ? kSelected : kUnelected}${name}`);
}
selectedMeshes = meshes.filter(mesh => meshUsesNode(mesh, node));
render();
}
// \u00a0 is non-breaking space.
const threeSpaces = '\u00a0\u00a0\u00a0';
const barTwoSpaces = '\u00a0|\u00a0';
const plusDash = '\u00a0+-';
// add a scene graph node to the GUI and adds the appropriate
// prefix so it looks something like
//
// +-root
// | +-child
// | | +-child
// | +-child
// +-child
function addSceneGraphNodeToGUI(gui, node, last, prefix) {
const nodes = [];
if (node.source instanceof TRS) {
const label = `${prefix === undefined ? '' : `${prefix}${plusDash}`}${node.name}`;
nodes.push({
button: addButtonLeftJustified(
gui, label, () => setCurrentSceneGraphNode(node)),
node,
});
}
const childPrefix = prefix === undefined
? ''
: `${prefix}${last ? threeSpaces : barTwoSpaces}`;
nodes.push(...node.children.map((child, i) => {
const childLast = i === node.children.length - 1;
return addSceneGraphNodeToGUI(gui, child, childLast, childPrefix);
}));
return nodes.flat();
}
const uiElem = document.querySelector('#ui');
const gui = new GUI({
parent: uiElem,
});
gui.onChange(() => {
uiElem.classList.toggle('hide-ui', !gui.isOpen());
render();
});
gui.add(settings, 'showMeshNodes').onChange(showMeshNodes);
gui.add(settings, 'showAllTRS').onChange(showTRS);
gui.addButton('frame selected', frameSelected);
const trsFolder = gui.addFolder('orientation').listen();
trsFolder.onChange(render);
const trsControls = [
trsFolder.add(trsUIHelper, 'translationX', -200, 200, 1),
trsFolder.add(trsUIHelper, 'translationY', -200, 200, 1),
trsFolder.add(trsUIHelper, 'translationZ', -200, 200, 1),
trsFolder.add(trsUIHelper, 'rotationX', radToDegOptions),
trsFolder.add(trsUIHelper, 'rotationY', radToDegOptions),
trsFolder.add(trsUIHelper, 'rotationZ', radToDegOptions),
trsFolder.add(trsUIHelper, 'scaleX', 0.1, 100),
trsFolder.add(trsUIHelper, 'scaleY', 0.1, 100),
trsFolder.add(trsUIHelper, 'scaleZ', 0.1, 100),
];
const nodesFolder = gui.addFolder('nodes');
const nodeButtons = addSceneGraphNodeToGUI(nodesFolder, root);
function showMeshNodes(show) {
for (const {node, button} of nodeButtons) {
if (node.name.includes('mesh')) {
button.show(show);
}
}
}
showMeshNodes(false);
const alwaysShow = new Set([0, 1, 2]);
function showTRS() {
const ui = nodeToUISettings.get(currentNode);
trsControls.forEach((trs, i) => {
const showThis = ui
? ui.trs?.indexOf(i) >= 0
: (settings.showAllTRS || alwaysShow.has(i));
trs.show(showThis);
});
}
let depthTexture;
let postTexture;
let objectNdx = 0;
setCurrentSceneGraphNode(cabinets.children[1]);
function drawObject(ctx, vertices, matrix, color) {
const { pass, viewProjectionMatrix } = ctx;
const { vertexBuffer, numVertices } = vertices;
if (objectNdx === objectInfos.length) {
objectInfos.push(createObjectInfo());
}
const {
matrixValue,
colorValue,
uniformBuffer,
uniformValues,
bindGroup,
} = objectInfos[objectNdx++];
mat4.multiply(viewProjectionMatrix, matrix, matrixValue);
colorValue.set(color);
// upload the uniform values to the uniform buffer
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
pass.setVertexBuffer(0, vertexBuffer);
pass.setBindGroup(0, bindGroup);
pass.draw(numVertices);
}
function makeNewTextureIfSizeDifferent(texture, size, format, usage) {
if (!texture ||
texture.width !== size.width ||
texture.height !== size.height) {
texture?.destroy();
texture = device.createTexture({
format,
size,
usage,
});
}
return texture;
}
function drawMesh(ctx, mesh) {
const { node, vertices, color } = mesh;
drawObject(ctx, vertices, node.worldMatrix, color);
}
function render() {
objectNdx = 0;
// 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
depthTexture = makeNewTextureIfSizeDifferent(
depthTexture,
canvasTexture, // for size
'depth24plus',
GPUTextureUsage.RENDER_ATTACHMENT,
);
renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
const aspect = canvas.clientWidth / canvas.clientHeight;
const projection = mat4.perspective(
settings.fieldOfView,
aspect,
1, // zNear
2000, // zFar
);
root.updateWorldMatrix();
// make a view matrix from the camera's
const viewMatrix = mat4.inverse(orbitCamera.getCameraMatrix());
// combine the view and projection matrixes
const viewProjectionMatrix = mat4.multiply(projection, viewMatrix);
const encoder = device.createCommandEncoder();
{
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
const ctx = { pass, viewProjectionMatrix };
for (const mesh of meshes) {
drawMesh(ctx, mesh);
}
pass.end();
}
// draw selected objects to postTexture
{
postTexture = makeNewTextureIfSizeDifferent(
postTexture,
canvasTexture, // for size
canvasTexture.format,
GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.TEXTURE_BINDING,
);
setupPostProcess(postTexture);
renderPassDescriptor.colorAttachments[0].view = postTexture.createView();
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
const ctx = { pass, viewProjectionMatrix };
for (const mesh of selectedMeshes) {
drawMesh(ctx, mesh);
}
pass.end();
// Draw outline based on alpha of postTexture
// on to the canvasTexture
postProcess(encoder, undefined, canvasTexture);
}
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 computeAABBForMesh(mesh) {
const mat = mesh.node.worldMatrix;
const p0 = mesh.vertices.aabb.min;
const p1 = mesh.vertices.aabb.max;
let min;
let max;
for (let i = 0; i < 8; ++i) {
const p = [
(i & 1) ? p0[0] : p1[0],
(i & 2) ? p0[1] : p1[1],
(i & 4) ? p0[2] : p1[2],
];
vec3.transformMat4(p, mat, p);
if (i === 0) {
min = p.slice();
max = p.slice();
} else {
vec3.min(min, p, min);
vec3.max(max, p, max);
}
}
return { min, max };
}
function expandAABBInPlace(aabb, otherAABB) {
vec3.min(aabb.min, otherAABB.min, aabb.min);
vec3.max(aabb.max, otherAABB.max, aabb.max);
}
function getAABBForSelectedMeshes() {
if (selectedMeshes.length === 0) {
return undefined;
}
const aabb = computeAABBForMesh(selectedMeshes[0]);
for (let i = 1; i < selectedMeshes.length; ++i) {
expandAABBInPlace(aabb, computeAABBForMesh(selectedMeshes[i]));
}
return aabb;
}
function frameSelected() {
if (selectedMeshes.length === 0) {
return;
}
// get aabb bounds for the selected objects.
const aabb = getAABBForSelectedMeshes();
const extent = vec3.subtract(aabb.max, aabb.min);
const diameter = vec3.distance(aabb.min, aabb.max);
// compute how far we need to set the radius for the selected
// objects to be framed.
const aspect = canvas.clientWidth / canvas.clientHeight;
const fieldOfViewH = 2 * Math.atan(Math.tan(settings.fieldOfView) * aspect);
const fov = Math.min(fieldOfViewH, settings.fieldOfView);
const zoomScale = 1.5; // make it 1.5 times as large for some padding.
const halfSize = diameter * zoomScale * 0.5;
const distance = halfSize / Math.tan(fov * 0.5);
orbitCamera.radius = distance;
// point the camera at the center
const center = vec3.addScaled(aabb.min, extent, 0.5);
orbitCamera.setTarget(center);
render();
}
}
function fail(msg) {
alert(msg);
}
main();
</script>
</html>