diff --git a/js/regl/bloomPass.js b/js/regl/bloomPass.js index 8ac28c8..d8f1c00 100644 --- a/js/regl/bloomPass.js +++ b/js/regl/bloomPass.js @@ -1,4 +1,4 @@ -import { loadText, makePassFBO, makePyramid, resizePyramid, makePass } from "./utils.js"; +import { loadText, makePassFBO, makePass } from "./utils.js"; // The bloom pass is basically an added high-pass blur. // The blur approximation is the sum of a pyramid of downscaled textures. @@ -9,6 +9,16 @@ const levelStrengths = Array(pyramidHeight) .map((_, index) => Math.pow(index / (pyramidHeight * 2) + 0.5, 1 / 3).toPrecision(5)) .reverse(); +// A pyramid is just an array of FBOs, where each FBO is half the width +// and half the height of the FBO below it. +const makePyramid = (regl, height, halfFloat) => + Array(height) + .fill() + .map((_) => makePassFBO(regl, halfFloat)); + +const resizePyramid = (pyramid, vw, vh, scale) => + pyramid.forEach((fbo, index) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index))); + export default ({ regl, config }, inputs) => { const { bloomStrength, bloomSize, highPassThreshold } = config; const enabled = bloomSize > 0 && bloomStrength > 0; diff --git a/js/regl/utils.js b/js/regl/utils.js index 10c46a2..885abea 100644 --- a/js/regl/utils.js +++ b/js/regl/utils.js @@ -10,13 +10,6 @@ const makePassTexture = (regl, halfFloat) => const makePassFBO = (regl, halfFloat) => regl.framebuffer({ color: makePassTexture(regl, halfFloat) }); -// A pyramid is just an array of FBOs, where each FBO is half the width -// and half the height of the FBO below it. -const makePyramid = (regl, height, halfFloat) => - Array(height) - .fill() - .map((_) => makePassFBO(regl, halfFloat)); - const makeDoubleBuffer = (regl, props) => { const state = Array(2) .fill() @@ -32,9 +25,6 @@ const makeDoubleBuffer = (regl, props) => { }; }; -const resizePyramid = (pyramid, vw, vh, scale) => - pyramid.forEach((fbo, index) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index))); - const loadImage = (regl, url) => { let texture = regl.texture([[0]]); let loaded = false; @@ -139,16 +129,4 @@ const makePass = (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].outputs)], []); -export { - makePassTexture, - makePassFBO, - makeDoubleBuffer, - makePyramid, - resizePyramid, - loadImage, - loadText, - makeFullScreenQuad, - make1DTexture, - makePass, - makePipeline, -}; +export { makePassTexture, makePassFBO, makeDoubleBuffer, loadImage, loadText, makeFullScreenQuad, make1DTexture, makePass, makePipeline }; diff --git a/js/webgpu/bloomPass.js b/js/webgpu/bloomPass.js new file mode 100644 index 0000000..a79149c --- /dev/null +++ b/js/webgpu/bloomPass.js @@ -0,0 +1,125 @@ +import { structs } from "/lib/gpu-buffer.js"; +import { loadShader, makeUniformBuffer, makeBindGroup, makePassFBO, makePass } from "./utils.js"; + +export default (context, getInputs) => { + const { config, device, canvasFormat } = context; + const fbo = makePassFBO(device, 1, 1, canvasFormat); + return makePass(() => ({ ...getInputs(), bloom: fbo })); +}; + +/* +import { loadText, makePassFBO, makePass } from "./utils.js"; + +// The bloom pass is basically an added high-pass blur. +// The blur approximation is the sum of a pyramid of downscaled textures. + +const pyramidHeight = 5; +const levelStrengths = Array(pyramidHeight) + .fill() + .map((_, index) => Math.pow(index / (pyramidHeight * 2) + 0.5, 1 / 3).toPrecision(5)) + .reverse(); + +// A pyramid is just an array of FBOs, where each FBO is half the width +// and half the height of the FBO below it. +const makePyramid = (regl, height, halfFloat) => + Array(height) + .fill() + .map((_) => makePassFBO(regl, halfFloat)); + +const resizePyramid = (pyramid, vw, vh, scale) => + pyramid.forEach((fbo, index) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index))); + +export default ({ regl, config }, inputs) => { + const { bloomStrength, bloomSize, highPassThreshold } = config; + const enabled = bloomSize > 0 && bloomStrength > 0; + + // If there's no bloom to apply, return a no-op pass with an empty bloom texture + if (!enabled) { + return makePass({ + primary: inputs.primary, + bloom: makePassFBO(regl), + }); + } + + // Build three pyramids of FBOs, one for each step in the process + const highPassPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat); + const hBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat); + const vBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat); + const output = makePassFBO(regl, config.useHalfFloat); + + // The high pass restricts the blur to bright things in our input texture. + const highPassFrag = loadText("shaders/glsl/highPass.frag.glsl"); + const highPass = regl({ + frag: regl.prop("frag"), + uniforms: { + highPassThreshold, + tex: regl.prop("tex"), + }, + framebuffer: regl.prop("fbo"), + }); + + // A 2D gaussian blur is just a 1D blur done horizontally, then done vertically. + // The FBO pyramid's levels represent separate levels of detail; + // by blurring them all, this basic blur approximates a more complex gaussian: + // https://web.archive.org/web/20191124072602/https://software.intel.com/en-us/articles/compute-shader-hdr-and-bloom + + const blurFrag = loadText("shaders/glsl/blur.frag.glsl"); + const blur = regl({ + frag: regl.prop("frag"), + uniforms: { + tex: regl.prop("tex"), + direction: regl.prop("direction"), + height: regl.context("viewportWidth"), + width: regl.context("viewportHeight"), + }, + framebuffer: regl.prop("fbo"), + }); + + // The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture + const sumPyramid = regl({ + frag: ` + precision mediump float; + varying vec2 vUV; + ${vBlurPyramid.map((_, index) => `uniform sampler2D pyr_${index};`).join("\n")} + uniform float bloomStrength; + void main() { + vec4 total = vec4(0.); + ${vBlurPyramid.map((_, index) => `total += texture2D(pyr_${index}, vUV) * ${levelStrengths[index]};`).join("\n")} + gl_FragColor = total * bloomStrength; + } + `, + uniforms: { + bloomStrength, + ...Object.fromEntries(vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])), + }, + framebuffer: output, + }); + + return makePass( + { + primary: inputs.primary, + bloom: output, + }, + Promise.all([highPassFrag.loaded, blurFrag.loaded]), + (w, h) => { + // The blur pyramids can be lower resolution than the screen. + resizePyramid(highPassPyramid, w, h, bloomSize); + resizePyramid(hBlurPyramid, w, h, bloomSize); + resizePyramid(vBlurPyramid, w, h, bloomSize); + output.resize(w, h); + }, + () => { + for (let i = 0; i < pyramidHeight; i++) { + const highPassFBO = highPassPyramid[i]; + const hBlurFBO = hBlurPyramid[i]; + const vBlurFBO = vBlurPyramid[i]; + highPass({ fbo: highPassFBO, frag: highPassFrag.text(), tex: inputs.primary }); + blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] }); + blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] }); + } + + sumPyramid(); + } + ); +}; +*/ diff --git a/js/webgpu/imagePass.js b/js/webgpu/imagePass.js index b10c1b2..6db1531 100644 --- a/js/webgpu/imagePass.js +++ b/js/webgpu/imagePass.js @@ -6,7 +6,7 @@ const defaultBGURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/ const numVerticesPerQuad = 2 * 3; export default (context, getInputs) => { - const { config, adapter, device, canvasContext } = context; + const { config, device, canvasFormat } = context; const linearSampler = device.createSampler({ magFilter: "linear", @@ -23,8 +23,6 @@ export default (context, getInputs) => { ], }; - const presentationFormat = canvasContext.getPreferredFormat(adapter); - let renderPipeline; let output; let backgroundTex; @@ -47,7 +45,7 @@ export default (context, getInputs) => { entryPoint: "fragMain", targets: [ { - format: presentationFormat, + format: canvasFormat, }, ], }, @@ -56,7 +54,7 @@ export default (context, getInputs) => { const setSize = (width, height) => { output?.destroy(); - output = makePassFBO(device, width, height, presentationFormat); + output = makePassFBO(device, width, height, canvasFormat); }; const getOutputs = () => ({ @@ -66,7 +64,7 @@ export default (context, getInputs) => { const execute = (encoder) => { const inputs = getInputs(); const tex = inputs.primary; - const bloomTex = inputs.primary; // TODO: bloom + const bloomTex = inputs.bloom; // TODO: bloom const renderBindGroup = makeBindGroup(device, renderPipeline, 0, [linearSampler, tex.createView(), bloomTex.createView(), backgroundTex.createView()]); renderPassConfig.colorAttachments[0].view = output.createView(); const renderPass = encoder.beginRenderPass(renderPassConfig); diff --git a/js/webgpu/main.js b/js/webgpu/main.js index a5c1dfc..36c0014 100644 --- a/js/webgpu/main.js +++ b/js/webgpu/main.js @@ -2,7 +2,7 @@ import { structs } from "/lib/gpu-buffer.js"; import { getCanvasSize, makeUniformBuffer, makePipeline } from "./utils.js"; import makeRain from "./rainPass.js"; -// import makeBloomPass from "./bloomPass.js"; +import makeBloomPass from "./bloomPass.js"; import makePalettePass from "./palettePass.js"; import makeStripePass from "./stripePass.js"; import makeImagePass from "./imagePass.js"; @@ -25,13 +25,13 @@ export default async (canvas, config) => { const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const canvasContext = canvas.getContext("webgpu"); - const presentationFormat = canvasContext.getPreferredFormat(adapter); + const canvasFormat = canvasContext.getPreferredFormat(adapter); // console.table(device.limits); const canvasConfig = { device, - format: presentationFormat, + format: canvasFormat, size: [NaN, NaN], usage: // GPUTextureUsage.STORAGE_BINDING | @@ -47,10 +47,11 @@ export default async (canvas, config) => { device, canvasContext, timeBuffer, + canvasFormat, }; const effectName = config.effect in effects ? config.effect : "plain"; - const pipeline = makePipeline(context, [makeRain, /*makeBloomPass,*/ effects[effectName]]); + const pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName]]); await Promise.all(pipeline.map((step) => step.ready)); diff --git a/js/webgpu/palettePass.js b/js/webgpu/palettePass.js index c3c1535..a894ba2 100644 --- a/js/webgpu/palettePass.js +++ b/js/webgpu/palettePass.js @@ -78,7 +78,7 @@ const makePalette = (device, paletteUniforms, entries) => { // in screen space. export default (context, getInputs) => { - const { config, adapter, device, canvasContext, timeBuffer } = context; + const { config, device, timeBuffer, canvasFormat } = context; const linearSampler = device.createSampler({ magFilter: "linear", @@ -95,8 +95,6 @@ export default (context, getInputs) => { ], }; - const presentationFormat = canvasContext.getPreferredFormat(adapter); - let renderPipeline; let configBuffer; let paletteBuffer; @@ -117,7 +115,7 @@ export default (context, getInputs) => { entryPoint: "fragMain", targets: [ { - format: presentationFormat, + format: canvasFormat, }, ], }, @@ -133,7 +131,7 @@ export default (context, getInputs) => { const setSize = (width, height) => { output?.destroy(); - output = makePassFBO(device, width, height, presentationFormat); + output = makePassFBO(device, width, height, canvasFormat); }; const getOutputs = () => ({ @@ -143,7 +141,7 @@ export default (context, getInputs) => { const execute = (encoder) => { const inputs = getInputs(); const tex = inputs.primary; - const bloomTex = inputs.primary; // TODO: bloom + const bloomTex = inputs.bloom; // TODO: bloom const renderBindGroup = makeBindGroup(device, renderPipeline, 0, [ configBuffer, paletteBuffer, diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index 892d202..0c8333a 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -32,7 +32,7 @@ const makeConfigBuffer = (device, configUniforms, config, density, gridSize) => }; export default (context, getInputs) => { - const { config, adapter, device, canvasContext, timeBuffer } = context; + const { config, device, timeBuffer, canvasFormat } = context; const assets = [loadTexture(device, config.glyphTexURL), loadShader(device, "shaders/wgsl/rainPass.wgsl")]; @@ -82,8 +82,6 @@ export default (context, getInputs) => { ], }; - const presentationFormat = canvasContext.getPreferredFormat(adapter); - let configBuffer; let sceneUniforms; let sceneBuffer; @@ -126,14 +124,14 @@ export default (context, getInputs) => { entryPoint: "fragMain", targets: [ { - format: presentationFormat, + format: canvasFormat, blend: { color: additiveBlendComponent, alpha: additiveBlendComponent, }, }, { - format: presentationFormat, + format: canvasFormat, blend: { color: additiveBlendComponent, alpha: additiveBlendComponent, @@ -164,10 +162,10 @@ export default (context, getInputs) => { // Update output?.destroy(); - output = makePassFBO(device, width, height, presentationFormat); + output = makePassFBO(device, width, height, canvasFormat); highPassOutput?.destroy(); - highPassOutput = makePassFBO(device, width, height, presentationFormat); + highPassOutput = makePassFBO(device, width, height, canvasFormat); }; const getOutputs = () => ({ diff --git a/js/webgpu/resurrectionPass.js b/js/webgpu/resurrectionPass.js index 5b7b562..a54219f 100644 --- a/js/webgpu/resurrectionPass.js +++ b/js/webgpu/resurrectionPass.js @@ -12,7 +12,7 @@ import { loadShader, makeUniformBuffer, makePassFBO, makePass } from "./utils.js const numVerticesPerQuad = 2 * 3; export default (context, getInputs) => { - const { config, adapter, device, canvasContext, timeBuffer } = context; + const { config, device, timeBuffer, canvasFormat } = context; const linearSampler = device.createSampler({ magFilter: "linear", @@ -29,8 +29,6 @@ export default (context, getInputs) => { ], }; - const presentationFormat = canvasContext.getPreferredFormat(adapter); - let renderPipeline; let configBuffer; let output; @@ -50,7 +48,7 @@ export default (context, getInputs) => { entryPoint: "fragMain", targets: [ { - format: presentationFormat, + format: canvasFormat, }, ], }, @@ -62,7 +60,7 @@ export default (context, getInputs) => { const setSize = (width, height) => { output?.destroy(); - output = makePassFBO(device, width, height, presentationFormat); + output = makePassFBO(device, width, height, canvasFormat); }; const getOutputs = () => ({ @@ -72,7 +70,7 @@ export default (context, getInputs) => { const execute = (encoder) => { const inputs = getInputs(); const tex = inputs.primary; - const bloomTex = inputs.primary; // TODO: bloom + const bloomTex = inputs.bloom; // TODO: bloom const renderBindGroup = makeBindGroup(device, renderPipeline, 0, [configBuffer, timeBuffer, linearSampler, tex.createView(), bloomTex.createView()]); renderPassConfig.colorAttachments[0].view = output.createView(); diff --git a/js/webgpu/stripePass.js b/js/webgpu/stripePass.js index 93a0b59..ff606e5 100644 --- a/js/webgpu/stripePass.js +++ b/js/webgpu/stripePass.js @@ -38,7 +38,7 @@ const numVerticesPerQuad = 2 * 3; // in screen space. export default (context, getInputs) => { - const { config, adapter, device, canvasContext, timeBuffer } = context; + const { config, device, timeBuffer, canvasFormat } = context; // Expand and convert stripe colors into 1D texture data const stripeColors = @@ -64,8 +64,6 @@ export default (context, getInputs) => { ], }; - const presentationFormat = canvasContext.getPreferredFormat(adapter); - let renderPipeline; let configBuffer; let output; @@ -85,7 +83,7 @@ export default (context, getInputs) => { entryPoint: "fragMain", targets: [ { - format: presentationFormat, + format: canvasFormat, }, ], }, @@ -97,7 +95,7 @@ export default (context, getInputs) => { const setSize = (width, height) => { output?.destroy(); - output = makePassFBO(device, width, height, presentationFormat); + output = makePassFBO(device, width, height, canvasFormat); }; const getOutputs = () => ({ @@ -107,7 +105,7 @@ export default (context, getInputs) => { const execute = (encoder) => { const inputs = getInputs(); const tex = inputs.primary; - const bloomTex = inputs.primary; // TODO: bloom + const bloomTex = inputs.bloom; // TODO: bloom const renderBindGroup = makeBindGroup(device, renderPipeline, 0, [ configBuffer, timeBuffer,