Skip to content

Commit 5a73b14

Browse files
authored
Add Occlusion Query Sample (#431)
* Add Occlusion Query Sample This is the simplest thing I could think of to demonstrate how to use Occlusion Queries.
1 parent 464b104 commit 5a73b14

File tree

5 files changed

+426
-0
lines changed

5 files changed

+426
-0
lines changed

sample/occlusionQuery/index.html

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<title>webgpu-samples: wireframe</title>
7+
<style>
8+
:root {
9+
color-scheme: light dark;
10+
}
11+
html, body {
12+
margin: 0; /* remove default margin */
13+
height: 100%; /* make body fill the browser window */
14+
display: flex;
15+
place-content: center center;
16+
}
17+
canvas {
18+
width: 600px;
19+
height: 600px;
20+
max-width: 100%;
21+
display: block;
22+
}
23+
#info {
24+
position: absolute;
25+
left: 0;
26+
top: 0;
27+
padding: 1em;
28+
margin: 0;
29+
width: 12em;
30+
height: 1.25em;
31+
}
32+
</style>
33+
<script defer src="main.js" type="module"></script>
34+
<script defer type="module" src="../../js/iframe-helper.js"></script>
35+
</head>
36+
<body>
37+
<canvas></canvas>
38+
<pre id="info"></pre>
39+
</body>
40+
</html>

sample/occlusionQuery/main.ts

+346
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
import { GUI } from 'dat.gui';
2+
import { mat4 } from 'wgpu-matrix';
3+
import solidColorLitWGSL from './solidColorLit.wgsl';
4+
5+
const settings = {
6+
animate: true,
7+
};
8+
const gui = new GUI();
9+
gui.add(settings, 'animate');
10+
11+
type TypedArrayView =
12+
| Int8Array
13+
| Uint8Array
14+
| Int16Array
15+
| Uint16Array
16+
| Int32Array
17+
| Uint32Array
18+
| Float32Array
19+
| Float64Array;
20+
21+
export type TypedArrayConstructor =
22+
| Int8ArrayConstructor
23+
| Uint8ArrayConstructor
24+
| Int16ArrayConstructor
25+
| Uint16ArrayConstructor
26+
| Int32ArrayConstructor
27+
| Uint32ArrayConstructor
28+
| Float32ArrayConstructor
29+
| Float64ArrayConstructor;
30+
31+
const info = document.querySelector('#info');
32+
33+
const adapter = await navigator.gpu.requestAdapter();
34+
const device = await adapter.requestDevice();
35+
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
36+
const context = canvas.getContext('webgpu') as GPUCanvasContext;
37+
const devicePixelRatio = window.devicePixelRatio;
38+
canvas.width = canvas.clientWidth * devicePixelRatio;
39+
canvas.height = canvas.clientHeight * devicePixelRatio;
40+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
41+
context.configure({
42+
device,
43+
format: presentationFormat,
44+
alphaMode: 'premultiplied',
45+
});
46+
const depthFormat = 'depth24plus';
47+
48+
const module = device.createShaderModule({
49+
code: solidColorLitWGSL,
50+
});
51+
52+
const pipeline = device.createRenderPipeline({
53+
layout: 'auto',
54+
vertex: {
55+
module,
56+
buffers: [
57+
{
58+
arrayStride: 6 * 4, // 3x2 floats, 4 bytes each
59+
attributes: [
60+
{ shaderLocation: 0, offset: 0, format: 'float32x3' }, // position
61+
{ shaderLocation: 1, offset: 12, format: 'float32x3' }, // normal
62+
],
63+
},
64+
],
65+
},
66+
fragment: {
67+
module,
68+
targets: [{ format: presentationFormat }],
69+
},
70+
primitive: {
71+
topology: 'triangle-list',
72+
cullMode: 'back',
73+
},
74+
depthStencil: {
75+
depthWriteEnabled: true,
76+
depthCompare: 'less',
77+
format: depthFormat,
78+
},
79+
});
80+
81+
// prettier-ignore
82+
const cubePositions = [
83+
{ position: [-1, 0, 0], id: '🟥', color: [1, 0, 0, 1] },
84+
{ position: [ 1, 0, 0], id: '🟨', color: [1, 1, 0, 1] },
85+
{ position: [ 0, -1, 0], id: '🟩', color: [0, 0.5, 0, 1] },
86+
{ position: [ 0, 1, 0], id: '🟧', color: [1, 0.6, 0, 1] },
87+
{ position: [ 0, 0, -1], id: '🟦', color: [0, 0, 1, 1] },
88+
{ position: [ 0, 0, 1], id: '🟪', color: [0.5, 0, 0.5, 1] },
89+
];
90+
91+
const objectInfos = cubePositions.map(({ position, id, color }) => {
92+
const uniformBufferSize = (2 * 16 + 3 + 1 + 4) * 4;
93+
const uniformBuffer = device.createBuffer({
94+
size: uniformBufferSize,
95+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
96+
});
97+
const uniformValues = new Float32Array(uniformBufferSize / 4);
98+
const worldViewProjection = uniformValues.subarray(0, 16);
99+
const worldInverseTranspose = uniformValues.subarray(16, 32);
100+
const colorValue = uniformValues.subarray(32, 36);
101+
102+
colorValue.set(color);
103+
104+
const bindGroup = device.createBindGroup({
105+
layout: pipeline.getBindGroupLayout(0),
106+
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
107+
});
108+
109+
return {
110+
id,
111+
position: position.map((v) => v * 10),
112+
bindGroup,
113+
uniformBuffer,
114+
uniformValues,
115+
worldInverseTranspose,
116+
worldViewProjection,
117+
};
118+
});
119+
120+
const occlusionQuerySet = device.createQuerySet({
121+
type: 'occlusion',
122+
count: objectInfos.length,
123+
});
124+
125+
const resolveBuffer = device.createBuffer({
126+
label: 'resolveBuffer',
127+
// Query results are 64bit unsigned integers.
128+
size: objectInfos.length * BigUint64Array.BYTES_PER_ELEMENT,
129+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
130+
});
131+
132+
const resultBuffer = device.createBuffer({
133+
label: 'resultBuffer',
134+
size: resolveBuffer.size,
135+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
136+
});
137+
138+
function createBufferWithData(
139+
device: GPUDevice,
140+
data: TypedArrayView,
141+
usage: GPUBufferUsageFlags,
142+
label: string
143+
) {
144+
const buffer = device.createBuffer({
145+
label,
146+
size: data.byteLength,
147+
usage,
148+
mappedAtCreation: true,
149+
});
150+
const dst = new (data.constructor as TypedArrayConstructor)(
151+
buffer.getMappedRange()
152+
);
153+
dst.set(data);
154+
buffer.unmap();
155+
return buffer;
156+
}
157+
158+
// prettier-ignore
159+
const vertexData = new Float32Array([
160+
// position normal
161+
1, 1, -1, 1, 0, 0,
162+
1, 1, 1, 1, 0, 0,
163+
1, -1, 1, 1, 0, 0,
164+
1, -1, -1, 1, 0, 0,
165+
-1, 1, 1, -1, 0, 0,
166+
-1, 1, -1, -1, 0, 0,
167+
-1, -1, -1, -1, 0, 0,
168+
-1, -1, 1, -1, 0, 0,
169+
-1, 1, 1, 0, 1, 0,
170+
1, 1, 1, 0, 1, 0,
171+
1, 1, -1, 0, 1, 0,
172+
-1, 1, -1, 0, 1, 0,
173+
-1, -1, -1, 0, -1, 0,
174+
1, -1, -1, 0, -1, 0,
175+
1, -1, 1, 0, -1, 0,
176+
-1, -1, 1, 0, -1, 0,
177+
1, 1, 1, 0, 0, 1,
178+
-1, 1, 1, 0, 0, 1,
179+
-1, -1, 1, 0, 0, 1,
180+
1, -1, 1, 0, 0, 1,
181+
-1, 1, -1, 0, 0, -1,
182+
1, 1, -1, 0, 0, -1,
183+
1, -1, -1, 0, 0, -1,
184+
-1, -1, -1, 0, 0, -1,
185+
]);
186+
// prettier-ignore
187+
const indices = new Uint16Array([
188+
0, 1, 2, 0, 2, 3, // +x face
189+
4, 5, 6, 4, 6, 7, // -x face
190+
8, 9, 10, 8, 10, 11, // +y face
191+
12, 13, 14, 12, 14, 15, // -y face
192+
16, 17, 18, 16, 18, 19, // +z face
193+
20, 21, 22, 20, 22, 23, // -z face
194+
]);
195+
196+
const vertexBuffer = createBufferWithData(
197+
device,
198+
vertexData,
199+
GPUBufferUsage.VERTEX,
200+
'vertexBuffer'
201+
);
202+
const indicesBuffer = createBufferWithData(
203+
device,
204+
indices,
205+
GPUBufferUsage.INDEX,
206+
'indexBuffer'
207+
);
208+
209+
const renderPassDescriptor: GPURenderPassDescriptor = {
210+
colorAttachments: [
211+
{
212+
view: undefined, // Assigned later
213+
clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
214+
loadOp: 'clear',
215+
storeOp: 'store',
216+
},
217+
],
218+
depthStencilAttachment: {
219+
view: undefined, // Assigned later
220+
depthClearValue: 1.0,
221+
depthLoadOp: 'clear',
222+
depthStoreOp: 'store',
223+
},
224+
occlusionQuerySet,
225+
};
226+
227+
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
228+
const lerpV = (a: number[], b: number[], t: number) =>
229+
a.map((v, i) => lerp(v, b[i], t));
230+
const pingPongSine = (t: number) => Math.sin(t * Math.PI * 2) * 0.5 + 0.5;
231+
232+
let depthTexture: GPUTexture | undefined;
233+
234+
let time = 0;
235+
let then = 0;
236+
function render(now: number) {
237+
now *= 0.001; // convert to seconds
238+
const deltaTime = now - then;
239+
then = now;
240+
241+
if (settings.animate) {
242+
time += deltaTime;
243+
}
244+
245+
const projection = mat4.perspective(
246+
(30 * Math.PI) / 180,
247+
canvas.clientWidth / canvas.clientHeight,
248+
0.5,
249+
100
250+
);
251+
252+
const m = mat4.identity();
253+
mat4.rotateX(m, time, m);
254+
mat4.rotateY(m, time * 0.7, m);
255+
mat4.translate(m, lerpV([0, 0, 5], [0, 0, 40], pingPongSine(time * 0.2)), m);
256+
const view = mat4.inverse(m);
257+
const viewProjection = mat4.multiply(projection, view);
258+
259+
const canvasTexture = context.getCurrentTexture();
260+
if (
261+
!depthTexture ||
262+
depthTexture.width !== canvasTexture.width ||
263+
depthTexture.height !== canvasTexture.height
264+
) {
265+
if (depthTexture) {
266+
depthTexture.destroy();
267+
}
268+
269+
depthTexture = device.createTexture({
270+
size: canvasTexture, // canvasTexture has width, height, and depthOrArrayLayers properties
271+
format: depthFormat,
272+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
273+
});
274+
}
275+
276+
const colorTexture = context.getCurrentTexture();
277+
renderPassDescriptor.colorAttachments[0].view = colorTexture.createView();
278+
renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
279+
280+
const encoder = device.createCommandEncoder();
281+
const pass = encoder.beginRenderPass(renderPassDescriptor);
282+
pass.setPipeline(pipeline);
283+
284+
objectInfos.forEach(
285+
(
286+
{
287+
bindGroup,
288+
uniformBuffer,
289+
uniformValues,
290+
worldViewProjection,
291+
worldInverseTranspose,
292+
position,
293+
},
294+
i
295+
) => {
296+
const world = mat4.translation(position);
297+
mat4.transpose(mat4.inverse(world), worldInverseTranspose);
298+
mat4.multiply(viewProjection, world, worldViewProjection);
299+
300+
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
301+
302+
pass.setBindGroup(0, bindGroup);
303+
pass.setVertexBuffer(0, vertexBuffer);
304+
pass.setIndexBuffer(indicesBuffer, 'uint16');
305+
pass.beginOcclusionQuery(i);
306+
pass.drawIndexed(indices.length);
307+
pass.endOcclusionQuery();
308+
}
309+
);
310+
311+
pass.end();
312+
encoder.resolveQuerySet(
313+
occlusionQuerySet,
314+
0,
315+
objectInfos.length,
316+
resolveBuffer,
317+
0
318+
);
319+
if (resultBuffer.mapState === 'unmapped') {
320+
encoder.copyBufferToBuffer(
321+
resolveBuffer,
322+
0,
323+
resultBuffer,
324+
0,
325+
resultBuffer.size
326+
);
327+
}
328+
329+
device.queue.submit([encoder.finish()]);
330+
331+
if (resultBuffer.mapState === 'unmapped') {
332+
resultBuffer.mapAsync(GPUMapMode.READ).then(() => {
333+
const results = new BigUint64Array(resultBuffer.getMappedRange()).slice();
334+
resultBuffer.unmap();
335+
336+
const visible = objectInfos
337+
.filter((_, i) => results[i])
338+
.map(({ id }) => id)
339+
.join('');
340+
info.textContent = `visible: ${visible}`;
341+
});
342+
}
343+
344+
requestAnimationFrame(render);
345+
}
346+
requestAnimationFrame(render);

sample/occlusionQuery/meta.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
name: 'Occlusion Query',
3+
description: `
4+
This example demonstrates using Occlusion Queries.
5+
`,
6+
filename: __DIRNAME__,
7+
sources: [{ path: 'main.ts' }, { path: 'solidColorLit.wgsl' }],
8+
};

0 commit comments

Comments
 (0)