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

721 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 Matrix Stack - Cube (no stack)</title>
<style>
@import url(resources/webgpu-lesson.css);
html, body {
margin: 0; /* remove the default margin */
height: 100%; /* make the html,body fill the page */
}
canvas {
display: block; /* make the canvas act like a block */
width: 100%; /* make the canvas fill its container */
height: 100%;
}
:root {
--bg-color: #fff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #000;
}
}
canvas {
background-color: var(--bg-color);
}
</style>
</head>
<body>
<canvas></canvas>
</body>
<script type="module">
import GUI from '../3rdparty/muigui-0.x.module.js';
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,
};
}
const vec3 = {
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;
},
};
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);
},
};
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 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 { vertexData, numVertices } = createCubeVertices();
const vertexBuffer = device.createBuffer({
label: 'vertex buffer vertices',
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
// view: <- to be filled out when we render
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
// view: <- to be filled out when we render
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
};
const degToRad = d => d * Math.PI / 180;
const settings = {
baseRotation: 0,
};
const radToDegOptions = { min: -360, max: 360, step: 1, converters: GUI.converters.radToDeg };
const gui = new GUI();
gui.onChange(render);
gui.add(settings, 'baseRotation', radToDegOptions);
let depthTexture;
let objectNdx = 0;
function drawObject(ctx, matrix, color) {
const { pass, viewProjectionMatrix } = ctx;
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.setBindGroup(0, bindGroup);
pass.draw(numVertices);
}
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
if (!depthTexture ||
depthTexture.width !== canvasTexture.width ||
depthTexture.height !== canvasTexture.height) {
if (depthTexture) {
depthTexture.destroy();
}
depthTexture = device.createTexture({
size: [canvasTexture.width, canvasTexture.height],
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
}
renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
const aspect = canvas.clientWidth / canvas.clientHeight;
const projection = mat4.perspective(
degToRad(60), // fieldOfView,
aspect,
1, // zNear
2000, // zFar
);
const eye = [0, 2, 3];
const target = [0, 1, 0];
const up = [0, 1, 0];
// Compute a view matrix
const viewMatrix = mat4.lookAt(eye, target, up);
// combine the view and projection matrixes
const viewProjectionMatrix = mat4.multiply(projection, viewMatrix);
objectNdx = 0;
const ctx = { pass, viewProjectionMatrix };
drawObject(ctx, mat4.rotationY(settings.baseRotation), [1, 1, 1, 1]);
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
const width = entry.contentBoxSize[0].inlineSize;
const height = entry.contentBoxSize[0].blockSize;
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
// re-render
render();
}
});
observer.observe(canvas);
}
function fail(msg) {
alert(msg);
}
main();
</script>
</html>