diff --git a/README.md b/README.md
index c51b3c3..afe66e7 100644
--- a/README.md
+++ b/README.md
@@ -437,6 +437,7 @@ import { createTextureFromImage } from 'webgpu-utils';
* [cube-map](examples/cube-map.html)
* [instancing](examples/instancing.html)
* [primitives](examples/primitives.html)
+* [reverse-z](examples/reverse-z.html)
* [stencil](examples/stencil.html)
* [stencil-cube](examples/stencil-cube.html)
diff --git a/examples/reverse-z.html b/examples/reverse-z.html
new file mode 100644
index 0000000..bec9fc8
--- /dev/null
+++ b/examples/reverse-z.html
@@ -0,0 +1,44 @@
+
+
+
+ webgpu-utils - reverse-z
+
+
+
+
+
+
+
+
+
reverse-z perspective projection infinite z
+
+
standard perspective
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/reverse-z.js b/examples/reverse-z.js
new file mode 100644
index 0000000..f713429
--- /dev/null
+++ b/examples/reverse-z.js
@@ -0,0 +1,306 @@
+/* global GPUBufferUsage */
+/* global GPUTextureUsage */
+import { mat4, vec3 } from 'https://wgpu-matrix.org/dist/2.x/wgpu-matrix.module.js';
+import * as wgh from '../dist/1.x/webgpu-utils.module.js';
+
+async function main() {
+ const adapter = await navigator.gpu?.requestAdapter();
+ const device = await adapter?.requestDevice();
+ if (!device) {
+ fail('need a browser that supports WebGPU');
+ return;
+ }
+
+ const canvas = document.querySelector('canvas');
+ const context = canvas.getContext('webgpu');
+ const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
+ context.configure({
+ device,
+ format: presentationFormat,
+ alphaMode: 'premultiplied',
+ });
+
+ const code = `
+ struct Uniforms {
+ world: mat4x4f,
+ color: vec4f,
+ };
+
+ struct SharedUniforms {
+ viewProjection: mat4x4f,
+ lightDirection: vec3f,
+ };
+
+ @group(0) @binding(0) var uni: Uniforms;
+ @group(1) @binding(0) var sharedUni: SharedUniforms;
+
+ struct MyVSInput {
+ @location(0) position: vec4f,
+ @location(1) normal: vec3f,
+ @location(2) texcoord: vec2f,
+ };
+
+ struct MyVSOutput {
+ @builtin(position) position: vec4f,
+ @location(0) normal: vec3f,
+ @location(1) texcoord: vec2f,
+ };
+
+ @vertex
+ fn myVSMain(v: MyVSInput) -> MyVSOutput {
+ var vsOut: MyVSOutput;
+ vsOut.position = sharedUni.viewProjection * uni.world * v.position;
+ vsOut.normal = (uni.world * vec4f(v.normal, 0.0)).xyz;
+ vsOut.texcoord = v.texcoord;
+ return vsOut;
+ }
+
+ @fragment
+ fn myFSMain(v: MyVSOutput) -> @location(0) vec4f {
+ let diffuseColor = uni.color;
+ let a_normal = normalize(v.normal);
+ let l = dot(a_normal, sharedUni.lightDirection) * 0.5 + 0.5;
+ return vec4f(diffuseColor.rgb * l, diffuseColor.a);
+ }
+ `;
+ const defs = wgh.makeShaderDataDefinitions(code);
+
+ const descriptors = wgh.makeBindGroupLayoutDescriptors(defs, {vertex: {}, fragment: {}});
+ const bindGroupLayouts = descriptors.map(desc => device.createBindGroupLayout(desc));
+ const layout = device.createPipelineLayout({ bindGroupLayouts });
+
+ const numInstances = 50;
+ const geometries = [
+ wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createSphereVertices({subdivisionsAxis: 64, subdivisionsHeight: 32, radius: 0.5 })),
+ wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createCubeVertices()),
+ ];
+
+ function r(min, max) {
+ if (typeof max === 'undefined') {
+ max = min;
+ min = 0;
+ }
+ return Math.random() * (max - min) + min;
+ }
+
+ const module = device.createShaderModule({code});
+
+ const pipelinesAndRenderPassDescriptors = [
+ {
+ depthCompare: 'greater',
+ depthClearValue: 0,
+ perspective: (f, a, n) => mat4.perspectiveReverseZ(f, a, n),
+ clearValue: [ 0.2, 0.2, 0.2, 1.0 ],
+ loadOp: 'clear',
+ },
+ {
+ depthCompare: 'less',
+ depthClearValue: 1,
+ perspective: mat4.perspective,
+ loadOp: 'load',
+ },
+ ].map(settings => {
+ const pipeline = device.createRenderPipeline({
+ layout,
+ vertex: {
+ module,
+ entryPoint: 'myVSMain',
+ buffers: [
+ ...geometries[0].bufferLayouts,
+ ],
+ },
+ fragment: {
+ module,
+ entryPoint: 'myFSMain',
+ targets: [
+ {format: presentationFormat},
+ ],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ cullMode: 'back',
+ },
+ depthStencil: {
+ depthWriteEnabled: true,
+ ...settings,
+ format: 'depth32float',
+ },
+ });
+
+ const renderPassDescriptor = {
+ colorAttachments: [
+ {
+ // view: undefined, // Assigned later
+ ...settings,
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment: {
+ // view: undefined, // Assigned later
+ ...settings,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ },
+ };
+
+ const sharedUniformValues = wgh.makeStructuredView(defs.uniforms.sharedUni);
+
+ const sharedUniformBuffer = device.createBuffer({
+ size: sharedUniformValues.arrayBuffer.byteLength,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+
+ const sharedBindGroup = device.createBindGroup({
+ layout: bindGroupLayouts[1],
+ entries: [
+ { binding: 0, resource: { buffer: sharedUniformBuffer } },
+ ],
+ });
+
+ return {
+ pipeline,
+ renderPassDescriptor,
+ sharedUniformValues,
+ sharedUniformBuffer,
+ sharedBindGroup,
+ ...settings,
+ };
+ });
+
+ const objectInfos = [];
+ for (let i = 0; i < numInstances; ++i) {
+ const uniformView = wgh.makeStructuredView(defs.uniforms.uni);
+ const uniformBuffer = device.createBuffer({
+ size: uniformView.arrayBuffer.byteLength,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+ uniformView.views.color.set([r(1), r(1), r(1), 1]);
+
+ device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer);
+
+ const bindGroup = device.createBindGroup({
+ layout: bindGroupLayouts[0],
+ entries: [
+ { binding: 0, resource: { buffer: uniformBuffer } },
+ ],
+ });
+
+ objectInfos.push({
+ uniformView,
+ uniformBuffer,
+ bindGroup,
+ geometry: geometries[(i / 2 | 0) % geometries.length],
+ });
+ }
+
+ let depthTexture;
+
+ function render(time) {
+ time *= 0.001;
+
+ objectInfos.forEach(({
+ uniformBuffer,
+ uniformView,
+ }, i) => {
+ const p = i / 2 | 0;
+ const world = uniformView.views.world;
+ const size = 800;
+ mat4.identity(world);
+ mat4.translate(world, [0, 0, size * p * 1.1], world);
+ const s = i % 2 ? size : size - 0.1;
+ mat4.scale(world, [s, s, s], world);
+
+ device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer);
+ });
+
+ const encoder = device.createCommandEncoder();
+ pipelinesAndRenderPassDescriptors.forEach(({
+ pipeline,
+ renderPassDescriptor,
+ perspective,
+ sharedUniformBuffer,
+ sharedUniformValues,
+ sharedBindGroup,
+ }, pNdx) => {
+ const projection = perspective(90 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 10000);
+ const radius = 1000;
+ const t = time * 0.1;
+ const eye = [Math.cos(t) * radius, Math.sin(t) * radius, -600]; //[Math.cos(t) * radius, Math.sin(t) * radius, 1000];
+ const target = [0, 0, 2000];
+ const up = [0, 1, 0];
+
+ const view = mat4.lookAt(eye, target, up);
+ mat4.multiply(projection, view, sharedUniformValues.views.viewProjection);
+
+ sharedUniformValues.set({
+ lightDirection: vec3.normalize([1, 8, -10]),
+ });
+
+ device.queue.writeBuffer(sharedUniformBuffer, 0, sharedUniformValues.arrayBuffer);
+
+ 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 then 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: 'depth32float',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ }
+ renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
+
+ // TODO: Consider a render bundle
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ const h = canvasTexture.height / 2 | 0;
+ const y = pNdx * h;
+ pass.setScissorRect(0, y, canvasTexture.width, h);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(1, sharedBindGroup);
+ objectInfos.forEach(({
+ bindGroup,
+ geometry,
+ }) => {
+ pass.setBindGroup(0, bindGroup);
+ pass.setVertexBuffer(0, geometry.buffers[0]);
+ if (geometry.indexBuffer) {
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
+ pass.drawIndexed(geometry.numElements);
+ } else {
+ pass.draw(geometry.numElements);
+ }
+ });
+ pass.end();
+ });
+ device.queue.submit([encoder.finish()]);
+
+ requestAnimationFrame(render);
+ }
+ requestAnimationFrame(render);
+
+ const observer = new ResizeObserver(entries => {
+ for (const entry of entries) {
+ const canvas = entry.target;
+ const width = entry.contentBoxSize[0].inlineSize * 2;
+ const height = entry.contentBoxSize[0].blockSize * 2;
+ canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
+ canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
+ }
+ });
+ observer.observe(canvas);
+}
+
+function fail(msg) {
+ const elem = document.querySelector('#fail');
+ elem.style.display = '';
+ elem.children[0].textContent = msg;
+}
+
+main();