diff --git a/TODO.txt b/TODO.txt index a622f38..987ce05 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,8 +1,6 @@ TODO: WebGPU - Render targets - Resize accordingly Blur: compute or render? What is workgroupBarrier in compute shaders? The other passes should be a breeze diff --git a/js/webgpu/main.js b/js/webgpu/main.js index 01fb342..0c53823 100644 --- a/js/webgpu/main.js +++ b/js/webgpu/main.js @@ -14,6 +14,9 @@ export default async (canvas, config) => { device, format: presentationFormat, size: [NaN, NaN], + usage: + // GPUTextureUsage.STORAGE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST, }; const timeLayout = std140(["f32", "i32"]); @@ -27,7 +30,7 @@ export default async (canvas, config) => { timeBuffer, }; - const pipeline = makePipeline([makeRain /*makeBloomPass, effects[effectName]*/], (p) => p.outputs, context); + const pipeline = makePipeline(context, [makeRain /*makeBloomPass, effects[effectName]*/]); await Promise.all(pipeline.map((step) => step.ready)); @@ -46,6 +49,7 @@ export default async (canvas, config) => { const encoder = device.createCommandEncoder(); pipeline.forEach((step) => step.execute(encoder)); + encoder.copyTextureToTexture({ texture: pipeline[pipeline.length - 1].getOutputs().primary }, { texture: canvasContext.getCurrentTexture() }, canvasSize); device.queue.submit([encoder.finish()]); requestAnimationFrame(renderLoop); }; diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index e6c721b..442fa09 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -1,5 +1,5 @@ import std140 from "./std140.js"; -import { loadTexture, loadShaderModule, makeUniformBuffer, makePass } from "./utils.js"; +import { createRenderTargetTexture, loadTexture, loadShaderModule, makeUniformBuffer, makePass } from "./utils.js"; const { mat4, vec3 } = glMatrix; @@ -15,23 +15,7 @@ const cycleStyles = { const numVerticesPerQuad = 2 * 3; -export default (context, inputs) => { - const { config, adapter, device, canvasContext, timeBuffer } = context; - - const assets = [loadTexture(device, config.glyphTexURL), loadShaderModule(device, "shaders/wgsl/rainPass.wgsl")]; - - // The volumetric mode multiplies the number of columns - // to reach the desired density, and then overlaps them - const volumetric = config.volumetric; - const density = volumetric && config.effect !== "none" ? config.density : 1; - const gridSize = [config.numColumns * density, config.numColumns]; - const numCells = gridSize[0] * gridSize[1]; - - // The volumetric mode requires us to create a grid of quads, - // rather than a single quad for our geometry - const numQuads = volumetric ? numCells : 1; - - // Various effect-related values +const makeConfigBuffer = (device, config, density, gridSize) => { const rippleType = config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1; const cycleStyle = config.cycleStyleName in cycleStyles ? cycleStyles[config.cycleStyleName] : 0; const slantVec = [Math.cos(config.slant), Math.sin(config.slant)]; @@ -73,16 +57,33 @@ export default (context, inputs) => { { name: "density", type: "f32", value: density }, { name: "slantScale", type: "f32", value: slantScale }, { name: "slantVec", type: "vec2", value: slantVec }, - { name: "volumetric", type: "i32", value: volumetric }, + { name: "volumetric", type: "i32", value: config.volumetric }, ]; console.table(configData); - const configLayout = std140(configData.map((field) => field.type)); - const configBuffer = makeUniformBuffer( + return makeUniformBuffer( device, - configLayout, + std140(configData.map((field) => field.type)), configData.map((field) => field.value) ); +}; + +export default (context, getInputs) => { + const { config, adapter, device, canvasContext, timeBuffer } = context; + + const assets = [loadTexture(device, config.glyphTexURL), loadShaderModule(device, "shaders/wgsl/rainPass.wgsl")]; + + // The volumetric mode multiplies the number of columns + // to reach the desired density, and then overlaps them + const density = config.volumetric && config.effect !== "none" ? config.density : 1; + const gridSize = [config.numColumns * density, config.numColumns]; + const numCells = gridSize[0] * gridSize[1]; + + // The volumetric mode requires us to create a grid of quads, + // rather than a single quad for our geometry + const numQuads = config.volumetric ? numCells : 1; + + const configBuffer = makeConfigBuffer(device, config, density, gridSize); const sceneLayout = std140(["vec2", "mat4x4", "mat4x4"]); const sceneBuffer = makeUniformBuffer(device, sceneLayout); @@ -101,11 +102,23 @@ export default (context, inputs) => { minFilter: "linear", }); + const renderPassConfig = { + colorAttachments: [ + { + view: null, + loadValue: { r: 0, g: 0, b: 0, a: 1 }, + storeOp: "store", + }, + ], + }; + + const presentationFormat = canvasContext.getPreferredFormat(adapter); + let rainComputePipeline; let rainRenderPipeline; let computeBindGroup; let renderBindGroup; - let renderPassConfig; + let renderTargetTexture; const ready = (async () => { const [msdfTexture, rainShaderModule] = await Promise.all(assets); @@ -123,8 +136,6 @@ export default (context, inputs) => { dstFactor: "one", }; - const presentationFormat = canvasContext.getPreferredFormat(adapter); - rainRenderPipeline = device.createRenderPipeline({ vertex: { module: rainShaderModule, @@ -164,23 +175,17 @@ export default (context, inputs) => { resource, })), }); - - renderPassConfig = { - colorAttachments: [ - { - view: null, - loadValue: { r: 0, g: 0, b: 0, a: 1 }, - storeOp: "store", - }, - ], - }; })(); const setSize = (width, height) => { + // Update scene buffer const aspectRatio = width / height; mat4.perspectiveZO(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000); const screenSize = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; device.queue.writeBuffer(sceneBuffer, 0, sceneLayout.build([screenSize, camera, transform])); + + // Update + renderTargetTexture = createRenderTargetTexture(device, width, height, presentationFormat); }; const execute = (encoder) => { @@ -190,7 +195,7 @@ export default (context, inputs) => { computePass.dispatch(Math.ceil(gridSize[0] / 32), gridSize[1], 1); computePass.endPass(); - renderPassConfig.colorAttachments[0].view = canvasContext.getCurrentTexture().createView(); + renderPassConfig.colorAttachments[0].view = renderTargetTexture.createView(); const renderPass = encoder.beginRenderPass(renderPassConfig); renderPass.setPipeline(rainRenderPipeline); renderPass.setBindGroup(0, renderBindGroup); @@ -198,7 +203,9 @@ export default (context, inputs) => { renderPass.endPass(); }; - const outputs = {}; // TODO + const getOutputs = () => ({ + primary: renderTargetTexture, + }); - return makePass(outputs, ready, setSize, execute); + return makePass(ready, setSize, getOutputs, execute); }; diff --git a/js/webgpu/utils.js b/js/webgpu/utils.js index 11fd30e..2977729 100644 --- a/js/webgpu/utils.js +++ b/js/webgpu/utils.js @@ -27,6 +27,14 @@ const loadTexture = async (device, url) => { return texture; }; +const createRenderTargetTexture = (device, width, height, format = "rgba8unorm") => + device.createTexture({ + size: [width, height, 1], + format, + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, + // TODO: whittle these down + }); + const loadShaderModule = async (device, url) => { const response = await fetch(url); const code = await response.text(); @@ -46,22 +54,14 @@ const makeUniformBuffer = (device, structLayout, values = null) => { return buffer; }; -const makePass = (outputs, ready, setSize, execute) => { - if (ready == null) { - ready = Promise.resolve(); - } else if (ready instanceof Array) { - ready = Promise.all(ready); - } +const makePass = (ready, setSize, getOutputs, execute) => ({ + ready: ready ?? Promise.resolve(), + setSize: setSize ?? (() => {}), + getOutputs: getOutputs ?? (() => ({})), + execute: execute ?? (() => {}), +}); - return { - outputs, - ready, - setSize, - execute, - }; -}; +const makePipeline = (context, steps) => + steps.filter((f) => f != null).reduce((pipeline, f, i) => [...pipeline, f(context, i == 0 ? null : pipeline[i - 1].getOutputs)], []); -const makePipeline = (steps, getInputs, context) => - steps.filter((f) => f != null).reduce((pipeline, f, i) => [...pipeline, f(context, i == 0 ? null : getInputs(pipeline[i - 1]))], []); - -export { getCanvasSize, loadTexture, loadShaderModule, makeUniformBuffer, makePass, makePipeline }; +export { getCanvasSize, createRenderTargetTexture, loadTexture, loadShaderModule, makeUniformBuffer, makePass, makePipeline };