diff --git a/sample/occlusionQuery/index.html b/sample/occlusionQuery/index.html
new file mode 100644
index 00000000..b6625444
--- /dev/null
+++ b/sample/occlusionQuery/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+ webgpu-samples: wireframe
+
+
+
+
+
+
+
+
+
diff --git a/sample/occlusionQuery/main.ts b/sample/occlusionQuery/main.ts
new file mode 100644
index 00000000..7c55e056
--- /dev/null
+++ b/sample/occlusionQuery/main.ts
@@ -0,0 +1,346 @@
+import { GUI } from 'dat.gui';
+import { mat4 } from 'wgpu-matrix';
+import solidColorLitWGSL from './solidColorLit.wgsl';
+
+const settings = {
+ animate: true,
+};
+const gui = new GUI();
+gui.add(settings, 'animate');
+
+type TypedArrayView =
+ | Int8Array
+ | Uint8Array
+ | Int16Array
+ | Uint16Array
+ | Int32Array
+ | Uint32Array
+ | Float32Array
+ | Float64Array;
+
+export type TypedArrayConstructor =
+ | Int8ArrayConstructor
+ | Uint8ArrayConstructor
+ | Int16ArrayConstructor
+ | Uint16ArrayConstructor
+ | Int32ArrayConstructor
+ | Uint32ArrayConstructor
+ | Float32ArrayConstructor
+ | Float64ArrayConstructor;
+
+const info = document.querySelector('#info');
+
+const adapter = await navigator.gpu.requestAdapter();
+const device = await adapter.requestDevice();
+const canvas = document.querySelector('canvas') as HTMLCanvasElement;
+const context = canvas.getContext('webgpu') as GPUCanvasContext;
+const devicePixelRatio = window.devicePixelRatio;
+canvas.width = canvas.clientWidth * devicePixelRatio;
+canvas.height = canvas.clientHeight * devicePixelRatio;
+const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
+context.configure({
+ device,
+ format: presentationFormat,
+ alphaMode: 'premultiplied',
+});
+const depthFormat = 'depth24plus';
+
+const module = device.createShaderModule({
+ code: solidColorLitWGSL,
+});
+
+const pipeline = device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module,
+ buffers: [
+ {
+ arrayStride: 6 * 4, // 3x2 floats, 4 bytes each
+ attributes: [
+ { shaderLocation: 0, offset: 0, format: 'float32x3' }, // position
+ { shaderLocation: 1, offset: 12, format: 'float32x3' }, // normal
+ ],
+ },
+ ],
+ },
+ fragment: {
+ module,
+ targets: [{ format: presentationFormat }],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ cullMode: 'back',
+ },
+ depthStencil: {
+ depthWriteEnabled: true,
+ depthCompare: 'less',
+ format: depthFormat,
+ },
+});
+
+// prettier-ignore
+const cubePositions = [
+ { position: [-1, 0, 0], id: '🟥', color: [1, 0, 0, 1] },
+ { position: [ 1, 0, 0], id: '🟨', color: [1, 1, 0, 1] },
+ { position: [ 0, -1, 0], id: '🟩', color: [0, 0.5, 0, 1] },
+ { position: [ 0, 1, 0], id: '🟧', color: [1, 0.6, 0, 1] },
+ { position: [ 0, 0, -1], id: '🟦', color: [0, 0, 1, 1] },
+ { position: [ 0, 0, 1], id: '🟪', color: [0.5, 0, 0.5, 1] },
+];
+
+const objectInfos = cubePositions.map(({ position, id, color }) => {
+ const uniformBufferSize = (2 * 16 + 3 + 1 + 4) * 4;
+ const uniformBuffer = device.createBuffer({
+ size: uniformBufferSize,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+ const uniformValues = new Float32Array(uniformBufferSize / 4);
+ const worldViewProjection = uniformValues.subarray(0, 16);
+ const worldInverseTranspose = uniformValues.subarray(16, 32);
+ const colorValue = uniformValues.subarray(32, 36);
+
+ colorValue.set(color);
+
+ const bindGroup = device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
+ });
+
+ return {
+ id,
+ position: position.map((v) => v * 10),
+ bindGroup,
+ uniformBuffer,
+ uniformValues,
+ worldInverseTranspose,
+ worldViewProjection,
+ };
+});
+
+const occlusionQuerySet = device.createQuerySet({
+ type: 'occlusion',
+ count: objectInfos.length,
+});
+
+const resolveBuffer = device.createBuffer({
+ label: 'resolveBuffer',
+ // Query results are 64bit unsigned integers.
+ size: objectInfos.length * BigUint64Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
+});
+
+const resultBuffer = device.createBuffer({
+ label: 'resultBuffer',
+ size: resolveBuffer.size,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+});
+
+function createBufferWithData(
+ device: GPUDevice,
+ data: TypedArrayView,
+ usage: GPUBufferUsageFlags,
+ label: string
+) {
+ const buffer = device.createBuffer({
+ label,
+ size: data.byteLength,
+ usage,
+ mappedAtCreation: true,
+ });
+ const dst = new (data.constructor as TypedArrayConstructor)(
+ buffer.getMappedRange()
+ );
+ dst.set(data);
+ buffer.unmap();
+ return buffer;
+}
+
+// prettier-ignore
+const vertexData = new Float32Array([
+// position normal
+ 1, 1, -1, 1, 0, 0,
+ 1, 1, 1, 1, 0, 0,
+ 1, -1, 1, 1, 0, 0,
+ 1, -1, -1, 1, 0, 0,
+ -1, 1, 1, -1, 0, 0,
+ -1, 1, -1, -1, 0, 0,
+ -1, -1, -1, -1, 0, 0,
+ -1, -1, 1, -1, 0, 0,
+ -1, 1, 1, 0, 1, 0,
+ 1, 1, 1, 0, 1, 0,
+ 1, 1, -1, 0, 1, 0,
+ -1, 1, -1, 0, 1, 0,
+ -1, -1, -1, 0, -1, 0,
+ 1, -1, -1, 0, -1, 0,
+ 1, -1, 1, 0, -1, 0,
+ -1, -1, 1, 0, -1, 0,
+ 1, 1, 1, 0, 0, 1,
+ -1, 1, 1, 0, 0, 1,
+ -1, -1, 1, 0, 0, 1,
+ 1, -1, 1, 0, 0, 1,
+ -1, 1, -1, 0, 0, -1,
+ 1, 1, -1, 0, 0, -1,
+ 1, -1, -1, 0, 0, -1,
+ -1, -1, -1, 0, 0, -1,
+]);
+// prettier-ignore
+const indices = new Uint16Array([
+ 0, 1, 2, 0, 2, 3, // +x face
+ 4, 5, 6, 4, 6, 7, // -x face
+ 8, 9, 10, 8, 10, 11, // +y face
+ 12, 13, 14, 12, 14, 15, // -y face
+ 16, 17, 18, 16, 18, 19, // +z face
+ 20, 21, 22, 20, 22, 23, // -z face
+]);
+
+const vertexBuffer = createBufferWithData(
+ device,
+ vertexData,
+ GPUBufferUsage.VERTEX,
+ 'vertexBuffer'
+);
+const indicesBuffer = createBufferWithData(
+ device,
+ indices,
+ GPUBufferUsage.INDEX,
+ 'indexBuffer'
+);
+
+const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: undefined, // Assigned later
+ clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment: {
+ view: undefined, // Assigned later
+ depthClearValue: 1.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ },
+ occlusionQuerySet,
+};
+
+const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
+const lerpV = (a: number[], b: number[], t: number) =>
+ a.map((v, i) => lerp(v, b[i], t));
+const pingPongSine = (t: number) => Math.sin(t * Math.PI * 2) * 0.5 + 0.5;
+
+let depthTexture: GPUTexture | undefined;
+
+let time = 0;
+let then = 0;
+function render(now: number) {
+ now *= 0.001; // convert to seconds
+ const deltaTime = now - then;
+ then = now;
+
+ if (settings.animate) {
+ time += deltaTime;
+ }
+
+ const projection = mat4.perspective(
+ (30 * Math.PI) / 180,
+ canvas.clientWidth / canvas.clientHeight,
+ 0.5,
+ 100
+ );
+
+ const m = mat4.identity();
+ mat4.rotateX(m, time, m);
+ mat4.rotateY(m, time * 0.7, m);
+ mat4.translate(m, lerpV([0, 0, 5], [0, 0, 40], pingPongSine(time * 0.2)), m);
+ const view = mat4.inverse(m);
+ const viewProjection = mat4.multiply(projection, view);
+
+ const canvasTexture = context.getCurrentTexture();
+ if (
+ !depthTexture ||
+ depthTexture.width !== canvasTexture.width ||
+ depthTexture.height !== canvasTexture.height
+ ) {
+ if (depthTexture) {
+ depthTexture.destroy();
+ }
+
+ depthTexture = device.createTexture({
+ size: canvasTexture, // canvasTexture has width, height, and depthOrArrayLayers properties
+ format: depthFormat,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ }
+
+ const colorTexture = context.getCurrentTexture();
+ renderPassDescriptor.colorAttachments[0].view = colorTexture.createView();
+ renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(pipeline);
+
+ objectInfos.forEach(
+ (
+ {
+ bindGroup,
+ uniformBuffer,
+ uniformValues,
+ worldViewProjection,
+ worldInverseTranspose,
+ position,
+ },
+ i
+ ) => {
+ const world = mat4.translation(position);
+ mat4.transpose(mat4.inverse(world), worldInverseTranspose);
+ mat4.multiply(viewProjection, world, worldViewProjection);
+
+ device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
+
+ pass.setBindGroup(0, bindGroup);
+ pass.setVertexBuffer(0, vertexBuffer);
+ pass.setIndexBuffer(indicesBuffer, 'uint16');
+ pass.beginOcclusionQuery(i);
+ pass.drawIndexed(indices.length);
+ pass.endOcclusionQuery();
+ }
+ );
+
+ pass.end();
+ encoder.resolveQuerySet(
+ occlusionQuerySet,
+ 0,
+ objectInfos.length,
+ resolveBuffer,
+ 0
+ );
+ if (resultBuffer.mapState === 'unmapped') {
+ encoder.copyBufferToBuffer(
+ resolveBuffer,
+ 0,
+ resultBuffer,
+ 0,
+ resultBuffer.size
+ );
+ }
+
+ device.queue.submit([encoder.finish()]);
+
+ if (resultBuffer.mapState === 'unmapped') {
+ resultBuffer.mapAsync(GPUMapMode.READ).then(() => {
+ const results = new BigUint64Array(resultBuffer.getMappedRange()).slice();
+ resultBuffer.unmap();
+
+ const visible = objectInfos
+ .filter((_, i) => results[i])
+ .map(({ id }) => id)
+ .join('');
+ info.textContent = `visible: ${visible}`;
+ });
+ }
+
+ requestAnimationFrame(render);
+}
+requestAnimationFrame(render);
diff --git a/sample/occlusionQuery/meta.ts b/sample/occlusionQuery/meta.ts
new file mode 100644
index 00000000..bf23a730
--- /dev/null
+++ b/sample/occlusionQuery/meta.ts
@@ -0,0 +1,8 @@
+export default {
+ name: 'Occlusion Query',
+ description: `
+ This example demonstrates using Occlusion Queries.
+ `,
+ filename: __DIRNAME__,
+ sources: [{ path: 'main.ts' }, { path: 'solidColorLit.wgsl' }],
+};
diff --git a/sample/occlusionQuery/solidColorLit.wgsl b/sample/occlusionQuery/solidColorLit.wgsl
new file mode 100644
index 00000000..c29c3f6c
--- /dev/null
+++ b/sample/occlusionQuery/solidColorLit.wgsl
@@ -0,0 +1,30 @@
+struct Uniforms {
+ worldViewProjectionMatrix: mat4x4f,
+ worldMatrix: mat4x4f,
+ color: vec4f,
+};
+
+struct Vertex {
+ @location(0) position: vec4f,
+ @location(1) normal: vec3f,
+};
+
+struct VSOut {
+ @builtin(position) position: vec4f,
+ @location(0) normal: vec3f,
+};
+
+@group(0) @binding(0) var uni: Uniforms;
+
+@vertex fn vs(vin: Vertex) -> VSOut {
+ var vOut: VSOut;
+ vOut.position = uni.worldViewProjectionMatrix * vin.position;
+ vOut.normal = (uni.worldMatrix * vec4f(vin.normal, 0)).xyz;
+ return vOut;
+}
+
+@fragment fn fs(vin: VSOut) -> @location(0) vec4f {
+ let lightDirection = normalize(vec3f(4, 10, 6));
+ let light = dot(normalize(vin.normal), lightDirection) * 0.5 + 0.5;
+ return vec4f(uni.color.rgb * light, uni.color.a);
+}
diff --git a/src/samples.ts b/src/samples.ts
index 45db7cb2..639bf3a2 100644
--- a/src/samples.ts
+++ b/src/samples.ts
@@ -17,6 +17,7 @@ import instancedCube from '../sample/instancedCube/meta';
import metaballs from '../sample/metaballs/meta';
import multipleCanvases from '../sample/multipleCanvases/meta';
import normalMap from '../sample/normalMap/meta';
+import occlusionQuery from '../sample/occlusionQuery/meta';
import particles from '../sample/particles/meta';
import points from '../sample/points/meta';
import pristineGrid from '../sample/pristineGrid/meta';
@@ -88,6 +89,7 @@ export const pageCategories: PageCategory[] = [
samplerParameters,
reversedZ,
renderBundles,
+ occlusionQuery,
},
},