import { loadTriangleRenderer } from "./triangle.js" import { loadTriangleTexturedRenderer } from "./triangleTextured.js" if (!navigator.gpu) { throw new Error("WebGPU not supported on this browser."); } const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { throw new Error("No WebGPU adapter"); } // The next few lines, being nitpicky over bgra8unorm vs rgba8unorm is // needed for using the canvas framebuffer as a storage texture, which // is mostly only relevant in the specific case that you want to // directly write to the canvas framebuffer from a compute shader. // // I very highly doubt this has any negative performance impact // whatsoever on any device that has a WebGPU implementation, and // certainly no desktop/laptop GPU is affected by this. Nevertheless, // the WebGPU developers did deliberately decide to make // 'bgra8unorm-storage' an optional feature, so perhaps there is some // obscure device somewhere that actually cares about this. const hasBgraStorage = adapter.features.has("bgra8unorm-storage"); const device = await adapter.requestDevice({ requiredFeatures: hasBgraStorage ? ["bgra8unorm-storage"] : [] }); const canvas = document.querySelector("canvas"); const context = canvas.getContext("webgpu"); const canvasFormat = hasBgraStorage ? navigator.gpu.getPreferredCanvasFormat() : "rgba8unorm"; context.configure({ device: device, format: canvasFormat, alphaMode: 'premultiplied', usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT }); // render targets, initialized in handleResize var depthTexture = undefined; function handleResize(canvasTexture) { // The internal color buffer returned by context.getCurrentTexture() // is automatically resized with the canvas. All other render // targets (e.g depth buffers) are not. // // If there were other off-screen buffers (deferred lighting, // geometry buffers, etc..) those would also be recreated as needed here. const depthResized = depthTexture === undefined || canvasTexture.width != depthTexture.width || canvasTexture.height != depthTexture.height; if (depthResized) { if (depthTexture !== undefined) { depthTexture.destroy(); } depthTexture = device.createTexture({ format: 'depth24plus', usage: GPUTextureUsage.RENDER_ATTACHMENT, size: [canvasTexture.width, canvasTexture.height], }); } } const triangleRenderer = await loadTriangleRenderer(device, canvasFormat); const triangleTexturedRenderer = await loadTriangleTexturedRenderer(device, canvasFormat); function render() { // `canvas.clientWidth` and `canvas.clientHeight` are the current // size of the canvas element. The "canvas element" in this demo is // sized by the `canvas` CSS rule, which scales the canvas element // to 100% the width and height of the viewport. // // Web is a shit platform. The reported sizes are in "logical pixel" // units, which are not the same size as the physical pixels of the // device's screen. // // On the devices I've tested, multiplying clientWidth and // clientHeight by window.devicePixelRatio does give the real // dimensions of the canvas in physical screen pixels, and does // result in sharper font rendering. // // On devices like Nico's phone though, devicePixelRatio // multiplication results in ~12x more pixels being rendered per // frame, which can be significantly slower for things like // raymarching/raytracing. // // -- // // This causes the canvas color buffer to be resized to the canvas // element as sized by CSS, including the browser window being // resized. canvas.width = canvas.clientWidth * window.devicePixelRatio; canvas.height = canvas.clientHeight * window.devicePixelRatio; // possibly recreate any textures that depend on the size of the canvas // changing. const canvasTexture = context.getCurrentTexture(); handleResize(canvasTexture); const clearValue = { r: 0.2, g: 0.2, b: 0.4, a: 1.0 }; const colorAttachments = [ { view: canvasTexture.createView(), loadOp: "clear", clearValue: clearValue, storeOp: "store", } ]; const encoder = device.createCommandEncoder(); const renderPass = encoder.beginRenderPass({ colorAttachments: colorAttachments, depthStencilAttachment: { view: depthTexture.createView(), depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store', }, }); // Every time you want to use a different shader, WebGPU also // requires you use a different GPURenderPipeline. It seems like a // decent pattern to wrap each collection of // buffers/pipelines/shaders in its own object: // uncomment either of these to see the result of each render // pipeline / shader //triangleRenderer.render(renderPass); triangleTexturedRenderer.render(renderPass); renderPass.end(); const commandBuffer = encoder.finish(); device.queue.submit([commandBuffer]); } requestAnimationFrame(render);