mirror of
https://github.com/webgpu/webgpufundamentals.git
synced 2026-05-16 06:50:37 -04:00
1579 lines
43 KiB
HTML
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>
|