mirror of
https://github.com/webgpu/webgpufundamentals.git
synced 2026-05-16 10:20:57 -04:00
631 lines
20 KiB
HTML
631 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 Optimization - None</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);
|
|
}
|
|
#info {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
padding: 0.5em;
|
|
margin: 0;
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
min-width: 8em;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas></canvas>
|
|
<pre id="info"></pre>
|
|
</body>
|
|
<script type="module">
|
|
import GUI from '../3rdparty/muigui-0.x.module.js';
|
|
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#webgpu-utils
|
|
import {createTextureFromSource} from '../3rdparty/webgpu-utils-1.x.module.js';
|
|
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-matrix-math.html
|
|
import {mat4, mat3, vec3} from '../3rdparty/wgpu-matrix.module.js';
|
|
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
|
|
import TimingHelper from './resources/js/timing-helper.js';
|
|
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
|
|
import NonNegativeRollingAverage from './resources/js/non-negative-rolling-average.js';
|
|
|
|
const fpsAverage = new NonNegativeRollingAverage();
|
|
const jsAverage = new NonNegativeRollingAverage();
|
|
const gpuAverage = new NonNegativeRollingAverage();
|
|
const mathAverage = new NonNegativeRollingAverage();
|
|
|
|
/** Given a css color string, return an array of 4 values from 0 to 255 */
|
|
const cssColorToRGBA8 = (() => {
|
|
const canvas = new OffscreenCanvas(1, 1);
|
|
const ctx = canvas.getContext('2d', {willReadFrequently: true});
|
|
return cssColor => {
|
|
ctx.clearRect(0, 0, 1, 1);
|
|
ctx.fillStyle = cssColor;
|
|
ctx.fillRect(0, 0, 1, 1);
|
|
return Array.from(ctx.getImageData(0, 0, 1, 1).data);
|
|
};
|
|
})();
|
|
|
|
/** Given a css color string, return an array of 4 values from 0 to 1 */
|
|
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
|
|
|
|
/**
|
|
* Given hue, saturation, and luminance values in the range of 0 to 1
|
|
* return the corresponding CSS hsl string
|
|
*/
|
|
const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
|
|
|
|
/**
|
|
* Given hue, saturation, and luminance values in the range of 0 to 1
|
|
* returns an array of 4 values from 0 to 1
|
|
*/
|
|
const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
|
|
|
|
/**
|
|
* Returns a random number between min and max.
|
|
* If min and max are not specified, returns 0 to 1
|
|
* If max is not specified, return 0 to min.
|
|
*/
|
|
function rand(min, max) {
|
|
if (min === undefined) {
|
|
max = 1;
|
|
min = 0;
|
|
} else if (max === undefined) {
|
|
max = min;
|
|
min = 0;
|
|
}
|
|
return Math.random() * (max - min) + min;
|
|
}
|
|
|
|
/** Selects a random array element */
|
|
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
|
|
|
|
/** Rounds up v to a multiple of alignment */
|
|
const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
|
|
|
|
async function main() {
|
|
const adapter = await navigator.gpu?.requestAdapter({powerPreference: 'high-performance'});
|
|
const canTimestamp = adapter.features.has('timestamp-query');
|
|
const device = await adapter?.requestDevice({
|
|
requiredFeatures: [
|
|
...(canTimestamp ? ['timestamp-query'] : []),
|
|
],
|
|
});
|
|
if (!device) {
|
|
fail('could not init WebGPU');
|
|
}
|
|
|
|
const timingHelper = new TimingHelper(device);
|
|
const infoElem = document.querySelector('#info');
|
|
|
|
// 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 GlobalUniforms {
|
|
viewProjection: mat4x4f,
|
|
lightWorldPosition: vec3f,
|
|
viewWorldPosition: vec3f,
|
|
};
|
|
|
|
struct MaterialUniforms {
|
|
color: vec4f,
|
|
shininess: f32,
|
|
};
|
|
|
|
struct PerObjectUniforms {
|
|
normalMatrix: mat3x3f,
|
|
world: mat4x4f,
|
|
};
|
|
|
|
struct Vertex {
|
|
@location(0) position: vec4f,
|
|
@location(1) normal: vec3f,
|
|
@location(2) texcoord: vec2f,
|
|
};
|
|
|
|
struct VSOutput {
|
|
@builtin(position) position: vec4f,
|
|
@location(0) normal: vec3f,
|
|
@location(1) surfaceToLight: vec3f,
|
|
@location(2) surfaceToView: vec3f,
|
|
@location(3) texcoord: vec2f,
|
|
};
|
|
|
|
@group(0) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
@group(0) @binding(1) var diffuseSampler: sampler;
|
|
@group(0) @binding(2) var<uniform> obj: PerObjectUniforms;
|
|
@group(0) @binding(3) var<uniform> glb: GlobalUniforms;
|
|
@group(0) @binding(4) var<uniform> material: MaterialUniforms;
|
|
|
|
@vertex fn vs(vert: Vertex) -> VSOutput {
|
|
var vsOut: VSOutput;
|
|
vsOut.position = glb.viewProjection * obj.world * vert.position;
|
|
|
|
// Orient the normals and pass to the fragment shader
|
|
vsOut.normal = obj.normalMatrix * vert.normal;
|
|
|
|
// Compute the world position of the surface
|
|
let surfaceWorldPosition = (obj.world * vert.position).xyz;
|
|
|
|
// Compute the vector of the surface to the light
|
|
// and pass it to the fragment shader
|
|
vsOut.surfaceToLight = glb.lightWorldPosition - surfaceWorldPosition;
|
|
|
|
// Compute the vector of the surface to the light
|
|
// and pass it to the fragment shader
|
|
vsOut.surfaceToView = glb.viewWorldPosition - surfaceWorldPosition;
|
|
|
|
// Pass the texture coord on to the fragment shader
|
|
vsOut.texcoord = vert.texcoord;
|
|
|
|
return vsOut;
|
|
}
|
|
|
|
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
|
|
// Because vsOut.normal is an inter-stage variable
|
|
// it's interpolated so it will not be a unit vector.
|
|
// Normalizing it will make it a unit vector again
|
|
let normal = normalize(vsOut.normal);
|
|
|
|
let surfaceToLightDirection = normalize(vsOut.surfaceToLight);
|
|
let surfaceToViewDirection = normalize(vsOut.surfaceToView);
|
|
let halfVector = normalize(
|
|
surfaceToLightDirection + surfaceToViewDirection);
|
|
|
|
// Compute the light by taking the dot product
|
|
// of the normal with the direction to the light
|
|
let light = dot(normal, surfaceToLightDirection);
|
|
|
|
var specular = dot(normal, halfVector);
|
|
specular = select(
|
|
0.0, // value if condition is false
|
|
pow(specular, material.shininess), // value if condition is true
|
|
specular > 0.0); // condition
|
|
|
|
let diffuse = material.color * textureSample(diffuseTexture, diffuseSampler, vsOut.texcoord);
|
|
// Lets multiply just the color portion (not the alpha)
|
|
// by the light
|
|
let color = diffuse.rgb * light + specular;
|
|
return vec4f(color, diffuse.a);
|
|
}
|
|
`,
|
|
});
|
|
|
|
function createBufferWithData(device, data, usage) {
|
|
const buffer = device.createBuffer({
|
|
size: data.byteLength,
|
|
usage: usage,
|
|
mappedAtCreation: true,
|
|
});
|
|
const dst = new Uint8Array(buffer.getMappedRange());
|
|
dst.set(new Uint8Array(data.buffer));
|
|
buffer.unmap();
|
|
return buffer;
|
|
}
|
|
|
|
const vertexData = new Float32Array([
|
|
// position normal texcoord
|
|
1, 1, -1, 1, 0, 0, 1, 0,
|
|
1, 1, 1, 1, 0, 0, 0, 0,
|
|
1, -1, 1, 1, 0, 0, 0, 1,
|
|
1, -1, -1, 1, 0, 0, 1, 1,
|
|
-1, 1, 1, -1, 0, 0, 1, 0,
|
|
-1, 1, -1, -1, 0, 0, 0, 0,
|
|
-1, -1, -1, -1, 0, 0, 0, 1,
|
|
-1, -1, 1, -1, 0, 0, 1, 1,
|
|
-1, 1, 1, 0, 1, 0, 1, 0,
|
|
1, 1, 1, 0, 1, 0, 0, 0,
|
|
1, 1, -1, 0, 1, 0, 0, 1,
|
|
-1, 1, -1, 0, 1, 0, 1, 1,
|
|
-1, -1, -1, 0, -1, 0, 1, 0,
|
|
1, -1, -1, 0, -1, 0, 0, 0,
|
|
1, -1, 1, 0, -1, 0, 0, 1,
|
|
-1, -1, 1, 0, -1, 0, 1, 1,
|
|
1, 1, 1, 0, 0, 1, 1, 0,
|
|
-1, 1, 1, 0, 0, 1, 0, 0,
|
|
-1, -1, 1, 0, 0, 1, 0, 1,
|
|
1, -1, 1, 0, 0, 1, 1, 1,
|
|
-1, 1, -1, 0, 0, -1, 1, 0,
|
|
1, 1, -1, 0, 0, -1, 0, 0,
|
|
1, -1, -1, 0, 0, -1, 0, 1,
|
|
-1, -1, -1, 0, 0, -1, 1, 1,
|
|
]);
|
|
const indices = new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]);
|
|
|
|
const vertexBuffer = createBufferWithData(device, vertexData, GPUBufferUsage.VERTEX);
|
|
const indicesBuffer = createBufferWithData(device, indices, GPUBufferUsage.INDEX);
|
|
const numVertices = indices.length;
|
|
|
|
const pipeline = device.createRenderPipeline({
|
|
label: 'textured model with point light w/specular highlight',
|
|
layout: 'auto',
|
|
vertex: {
|
|
module,
|
|
buffers: [
|
|
{
|
|
arrayStride: (3 + 3 + 2) * 4, // 8 floats
|
|
attributes: [
|
|
{shaderLocation: 0, offset: 0 * 4, format: 'float32x3'}, // position
|
|
{shaderLocation: 1, offset: 3 * 4, format: 'float32x3'}, // normal
|
|
{shaderLocation: 2, offset: 6 * 4, format: 'float32x2'}, // texcoord
|
|
],
|
|
},
|
|
],
|
|
},
|
|
fragment: {
|
|
module,
|
|
targets: [{ format: presentationFormat }],
|
|
},
|
|
primitive: {
|
|
cullMode: 'back',
|
|
},
|
|
depthStencil: {
|
|
depthWriteEnabled: true,
|
|
depthCompare: 'less',
|
|
format: 'depth24plus',
|
|
},
|
|
});
|
|
|
|
const textures = [
|
|
'😂', '👾', '👍', '👀', '🌞', '🛟',
|
|
].map(s => {
|
|
const size = 128;
|
|
const ctx = new OffscreenCanvas(size, size).getContext('2d');
|
|
ctx.fillStyle = '#fff';
|
|
ctx.fillRect(0, 0, size, size);
|
|
ctx.font = `${size * 0.9}px sans-serif`;
|
|
ctx.textAlign = 'left';
|
|
ctx.textBaseline = 'top';
|
|
const m = ctx.measureText(s);
|
|
ctx.fillText(
|
|
s,
|
|
(size - m.actualBoundingBoxRight + m.actualBoundingBoxLeft) / 2,
|
|
(size - m.actualBoundingBoxDescent + m.actualBoundingBoxAscent) / 2
|
|
);
|
|
return createTextureFromSource(device, ctx.canvas, {mips: true});
|
|
});
|
|
|
|
const sampler = device.createSampler({
|
|
magFilter: 'linear',
|
|
minFilter: 'linear',
|
|
mipmapFilter: 'nearest',
|
|
});
|
|
|
|
const numMaterials = 20;
|
|
const materials = [];
|
|
for (let i = 0; i < numMaterials; ++i) {
|
|
const color = hslToRGBA(rand(), rand(0.5, 0.8), rand(0.5, 0.7));
|
|
const shininess = rand(10, 120);
|
|
|
|
const materialValues = new Float32Array([
|
|
...color,
|
|
shininess,
|
|
0, 0, 0, // padding
|
|
]);
|
|
const materialUniformBuffer = createBufferWithData(
|
|
device,
|
|
materialValues,
|
|
GPUBufferUsage.UNIFORM,
|
|
);
|
|
|
|
materials.push({
|
|
materialUniformBuffer,
|
|
texture: randomArrayElement(textures),
|
|
sampler,
|
|
});
|
|
}
|
|
|
|
const globalUniformBufferSize = (16 + 4 + 4) * 4;
|
|
const globalUniformBuffer = device.createBuffer({
|
|
label: 'global uniforms',
|
|
size: globalUniformBufferSize,
|
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
});
|
|
|
|
const globalUniformValues = new Float32Array(globalUniformBufferSize / 4);
|
|
|
|
const kViewProjectionOffset = 0;
|
|
const kLightWorldPositionOffset = 16;
|
|
const kViewWorldPositionOffset = 20;
|
|
|
|
const viewProjectionValue = globalUniformValues.subarray(
|
|
kViewProjectionOffset, kViewProjectionOffset + 16);
|
|
const lightWorldPositionValue = globalUniformValues.subarray(
|
|
kLightWorldPositionOffset, kLightWorldPositionOffset + 3);
|
|
const viewWorldPositionValue = globalUniformValues.subarray(
|
|
kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
|
|
|
|
const maxObjects = 30000;
|
|
const objectInfos = [];
|
|
|
|
const uniformBufferSize = (12 + 16) * 4;
|
|
const uniformBufferSpace = roundUp(uniformBufferSize, device.limits.minUniformBufferOffsetAlignment);
|
|
const uniformBuffers = [0, 1].map(() => device.createBuffer({
|
|
label: 'uniforms',
|
|
size: uniformBufferSpace * maxObjects,
|
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
}));
|
|
|
|
const mappedTransferBuffers = [];
|
|
const getMappedTransferBuffer = () => {
|
|
return mappedTransferBuffers.pop() || device.createBuffer({
|
|
label: 'transfer buffer',
|
|
size: uniformBufferSpace * maxObjects,
|
|
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
|
|
mappedAtCreation: true,
|
|
});
|
|
};
|
|
// offsets to the various uniform values in float32 indices
|
|
const kNormalMatrixOffset = 0;
|
|
const kWorldOffset = 12;
|
|
|
|
for (let i = 0; i < maxObjects; ++i) {
|
|
const uniformBufferOffset = i * uniformBufferSpace;
|
|
|
|
const material = randomArrayElement(materials);
|
|
|
|
const bindGroups = [0, 1].map(i => device.createBindGroup({
|
|
label: 'bind group for object',
|
|
layout: pipeline.getBindGroupLayout(0),
|
|
entries: [
|
|
{ binding: 0, resource: material.texture.createView() },
|
|
{ binding: 1, resource: material.sampler },
|
|
{ binding: 2, resource: { buffer: uniformBuffers[i], offset: uniformBufferOffset, size: uniformBufferSize }},
|
|
{ binding: 3, resource: globalUniformBuffer },
|
|
{ binding: 4, resource: { buffer: material.materialUniformBuffer }},
|
|
],
|
|
}));
|
|
|
|
const axis = vec3.normalize([rand(-1, 1), rand(-1, 1), rand(-1, 1)]);
|
|
const radius = rand(10, 100);
|
|
const speed = rand(0.1, 0.4);
|
|
const rotationSpeed = rand(-1, 1);
|
|
const scale = rand(2, 10);
|
|
|
|
objectInfos.push({
|
|
bindGroups,
|
|
|
|
axis,
|
|
radius,
|
|
speed,
|
|
rotationSpeed,
|
|
scale,
|
|
});
|
|
}
|
|
|
|
const renderPassDescriptor = {
|
|
label: 'our basic canvas renderPass',
|
|
colorAttachments: [
|
|
{
|
|
// view: <- to be filled out when we render
|
|
clearValue: [0.3, 0.3, 0.3, 1],
|
|
loadOp: 'clear',
|
|
storeOp: 'store',
|
|
},
|
|
],
|
|
depthStencilAttachment: {
|
|
// view: <- to be filled out when we render
|
|
depthClearValue: 1.0,
|
|
depthLoadOp: 'clear',
|
|
depthStoreOp: 'store',
|
|
},
|
|
};
|
|
|
|
const canvasToSizeMap = new WeakMap();
|
|
const degToRad = d => d * Math.PI / 180;
|
|
|
|
const settings = {
|
|
numObjects: 1000,
|
|
render: true,
|
|
};
|
|
|
|
const gui = new GUI();
|
|
gui.add(settings, 'numObjects', { min: 0, max: maxObjects, step: 1});
|
|
gui.add(settings, 'render');
|
|
|
|
let depthTexture;
|
|
let then = 0;
|
|
let frameCount = 0;
|
|
|
|
function render(time) {
|
|
time *= 0.001; // convert to seconds
|
|
const deltaTime = time - then;
|
|
then = time;
|
|
++frameCount;
|
|
|
|
const startTimeMs = performance.now();
|
|
|
|
const {width, height} = settings.render
|
|
? canvasToSizeMap.get(canvas) ?? canvas
|
|
: { width: 1, height: 1 };
|
|
|
|
// Don't set the canvas size if it's already that size as it may be slow.
|
|
if (canvas.width !== width || canvas.height !== height) {
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
}
|
|
|
|
// 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();
|
|
|
|
let mathElapsedTimeMs = 0;
|
|
|
|
const transferBuffer = getMappedTransferBuffer();
|
|
const uniformValues = new Float32Array(transferBuffer.getMappedRange());
|
|
|
|
for (let i = 0; i < settings.numObjects; ++i) {
|
|
const {
|
|
axis,
|
|
radius,
|
|
speed,
|
|
rotationSpeed,
|
|
scale,
|
|
} = objectInfos[i];
|
|
const mathTimeStartMs = performance.now();
|
|
|
|
const uniformBufferOffset = i * uniformBufferSpace;
|
|
const f32Offset = uniformBufferOffset / 4;
|
|
const normalMatrixValue = uniformValues.subarray(
|
|
f32Offset + kNormalMatrixOffset, f32Offset + kNormalMatrixOffset + 12);
|
|
const worldValue = uniformValues.subarray(
|
|
f32Offset + kWorldOffset, f32Offset + kWorldOffset + 16);
|
|
|
|
// Compute a world matrix
|
|
mat4.identity(worldValue);
|
|
mat4.axisRotate(worldValue, axis, i + time * speed, worldValue);
|
|
mat4.translate(worldValue, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], worldValue);
|
|
mat4.translate(worldValue, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], worldValue);
|
|
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
|
|
mat4.scale(worldValue, [scale, scale, scale], worldValue);
|
|
|
|
// Inverse and transpose it into the normalMatrix value
|
|
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
|
|
|
|
mathElapsedTimeMs += performance.now() - mathTimeStartMs;
|
|
}
|
|
transferBuffer.unmap();
|
|
|
|
const resourceIndex = frameCount % 2;
|
|
const uniformBuffer = uniformBuffers[resourceIndex];
|
|
|
|
// copy the uniform values from the transfer buffer to the uniform buffer
|
|
if (settings.numObjects) {
|
|
const size = (settings.numObjects - 1) * uniformBufferSpace + uniformBufferSize;
|
|
encoder.copyBufferToBuffer(transferBuffer, 0, uniformBuffer, 0, size);
|
|
}
|
|
|
|
const aspect = canvas.clientWidth / canvas.clientHeight;
|
|
const projection = mat4.perspective(
|
|
degToRad(60),
|
|
aspect,
|
|
1, // zNear
|
|
2000, // zFar
|
|
);
|
|
|
|
const eye = [100, 150, 200];
|
|
const target = [0, 0, 0];
|
|
const up = [0, 1, 0];
|
|
|
|
// Compute a view matrix
|
|
const viewMatrix = mat4.lookAt(eye, target, up);
|
|
|
|
// Combine the view and projection matrixes
|
|
mat4.multiply(projection, viewMatrix, viewProjectionValue);
|
|
|
|
lightWorldPositionValue.set([-10, 30, 300]);
|
|
viewWorldPositionValue.set(eye);
|
|
|
|
device.queue.writeBuffer(globalUniformBuffer, 0, globalUniformValues);
|
|
|
|
const pass = timingHelper.beginRenderPass(encoder, renderPassDescriptor);
|
|
pass.setPipeline(pipeline);
|
|
pass.setVertexBuffer(0, vertexBuffer);
|
|
pass.setIndexBuffer(indicesBuffer, 'uint16');
|
|
|
|
for (let i = 0; i < settings.numObjects; ++i) {
|
|
const { bindGroups } = objectInfos[i];
|
|
pass.setBindGroup(0, bindGroups[resourceIndex]);
|
|
pass.drawIndexed(numVertices);
|
|
}
|
|
|
|
pass.end();
|
|
|
|
const commandBuffer = encoder.finish();
|
|
device.queue.submit([commandBuffer]);
|
|
|
|
transferBuffer.mapAsync(GPUMapMode.WRITE).then(() => {
|
|
mappedTransferBuffers.push(transferBuffer);
|
|
});
|
|
|
|
timingHelper.getResult().then(gpuTime => {
|
|
gpuAverage.addSample(gpuTime / 1000);
|
|
});
|
|
|
|
const elapsedTimeMs = performance.now() - startTimeMs;
|
|
fpsAverage.addSample(1 / deltaTime);
|
|
jsAverage.addSample(elapsedTimeMs);
|
|
mathAverage.addSample(mathElapsedTimeMs);
|
|
|
|
infoElem.textContent = `\
|
|
js : ${jsAverage.get().toFixed(1)}ms
|
|
math: ${mathAverage.get().toFixed(1)}ms
|
|
fps : ${fpsAverage.get().toFixed(0)}
|
|
gpu : ${canTimestamp ? `${(gpuAverage.get() / 1000).toFixed(1)}ms` : 'N/A'}
|
|
`;
|
|
|
|
requestAnimationFrame(render);
|
|
}
|
|
requestAnimationFrame(render);
|
|
|
|
const observer = new ResizeObserver(entries => {
|
|
entries.forEach(entry => {
|
|
canvasToSizeMap.set(entry.target, {
|
|
width: Math.max(1, Math.min(entry.contentBoxSize[0].inlineSize, device.limits.maxTextureDimension2D)),
|
|
height: Math.max(1, Math.min(entry.contentBoxSize[0].blockSize, device.limits.maxTextureDimension2D)),
|
|
});
|
|
});
|
|
});
|
|
observer.observe(canvas);
|
|
}
|
|
|
|
function fail(msg) {
|
|
alert(msg);
|
|
}
|
|
|
|
main();
|
|
</script>
|
|
</html>
|