diff --git a/TODO.txt b/TODO.txt index cd76198..cceab9c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,7 +1,16 @@ TODO: WebGPU + + Figure out texture pyramid stuff + Idea: use mip levels to store stuff + Multiple draw calls, if level N must downsample level N - 1 + Does every level HAVE to be downsampled from the previous level? Or can they all be downsampled from a single source? + What about in my case in particular? I'm blurring everything anyway. + blur pass + Switch to rgba32float somehow? + Why isn't this straightforward? Update links in issues Get rid of end pass once it's possible to copy a bgra8unorm to a canvas texture @@ -26,6 +35,9 @@ Resurrection New glyphs? Good luck with that, champ +Zion Control's matrix variant + From Reloaded + Audio Synthesize raindrop sound https://www.instagram.com/tv/CWGodRcoq7T/?utm_medium=copy_link diff --git a/glyph order.txt b/glyph order.txt index 7ff5f74..c55a9de 100644 --- a/glyph order.txt +++ b/glyph order.txt @@ -1,5 +1,5 @@ Reloaded/Revolutions: -モエヤキオカ7ケサスZ152ヨタワ4ネヌナ98ヒ0ホア3ウ セ¦:"꞊ミラリ╌ツテニハソ▪—<>0|+*コシマムメ +モエヤキオカ7ケサスz152ヨタワ4ネヌナ98ヒ0ホア3ウ セ¦:"꞊ミラリ╌ツテニハソ▪—<>0|+*コシマムメ -Resurrection: -モエヤキオカ7ケサスZ152ヨタワ4ネヌナ98ヒ0ホア3ウ セ¦:"꞊ミラリ╌ツテニハソコ—<ム0|*▪メシマ>+ +Resurrections: +モエヤキオカ7ケサスz152ヨタワ4ネヌナ98ヒ0ホア3ウ セ¦:"꞊ミラリ╌ツテニハソコ—<ム0|*▪メシマ>+ diff --git a/js/config.js b/js/config.js index dd2b086..d722d89 100644 --- a/js/config.js +++ b/js/config.js @@ -26,6 +26,7 @@ const defaults = { animationSpeed: 1, // The global rate that all animations progress forwardSpeed: 0.25, // The speed volumetric rain approaches the eye bloomStrength: 1, // The intensity of the bloom + newBloomStrength: 1, // WGSL's bloom intensity has different math. TODO: investigate bloomSize: 0.6, // The amount the bloom calculation is scaled highPassThreshold: 0.1, // The minimum brightness that is still blurred cycleSpeed: 1, // The speed glyphs change @@ -70,6 +71,7 @@ const versions = { ...defaults, ...fonts.matrixcode, bloomStrength: 0.75, + newBloomStrength: 1.5, highPassThreshold: 0.0, cycleSpeed: 0.2, cycleFrameSkip: 8, @@ -111,6 +113,7 @@ const versions = { ...defaults, ...fonts.coptic, bloomStrength: 1, + newBloomStrength: 1, highPassThreshold: 0, cycleSpeed: 0.1, brightnessDecay: 0.05, diff --git a/js/webgpu/bloomPass.js b/js/webgpu/bloomPass.js index 5e83219..cc33148 100644 --- a/js/webgpu/bloomPass.js +++ b/js/webgpu/bloomPass.js @@ -1,19 +1,13 @@ import { structs } from "/lib/gpu-buffer.js"; -import { loadShader, makeUniformBuffer, makeBindGroup, makeComputeTarget, makePass } from "./utils.js"; - -// The bloom pass is basically an added blur of the high-pass rendered output. -// 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(); +import { makeComputeTarget, loadShader, makeUniformBuffer, makeBindGroup, makePass } from "./utils.js"; export default (context, getInputs) => { const { config, device } = context; - const enabled = false; // config.bloomSize > 0 && config.bloomStrength > 0; + const bloomSize = config.bloomSize; + const bloomStrength = config.newBloomStrength; + + 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) { @@ -24,142 +18,57 @@ export default (context, getInputs) => { const assets = [loadShader(device, "shaders/wgsl/blur1D.wgsl")]; - // TODO: generate sum shader code + const nearestSampler = device.createSampler({}); - const computeTarget = makeComputeTarget(device, 1, 1); - const getOutputs = () => ({ ...getInputs(), bloom: computeTarget }); // TODO + let computePipeline; + let configUniforms; + let horizontalConfigBuffer; + let verticalConfigBuffer; + let intermediate; + let output; + let screenSize; - let blurPipeline; - let sumPipeline; + const getOutputs = () => ({ + primary: getInputs().primary, + bloom: output, + }); const ready = (async () => { const [blurShader] = await Promise.all(assets); - // TODO: create sum shader - // TODO: create config buffer - // TODO: create blur render pipeline - // TODO: create sum render pipeline + computePipeline = device.createComputePipeline({ + compute: { + module: blurShader.module, + entryPoint: "computeMain", + }, + }); + + configUniforms = structs.from(blurShader.code).Config; })(); const setSize = (width, height) => { - // TODO: destroy output - // TODO: create output - // TODO: destroy pyramid textures - // TODO: create new pyramid textures - // TODO: create new pyramid bindings - // TODO: create new pyramid renderPassConfigs + intermediate?.destroy(); + intermediate = makeComputeTarget(device, Math.floor(width * bloomSize), height); + output?.destroy(); + output = makeComputeTarget(device, Math.floor(width * bloomSize), Math.floor(height * bloomSize)); + screenSize = [width, height]; + + horizontalConfigBuffer = makeUniformBuffer(device, configUniforms, { bloomStrength, direction: [0, bloomSize] }); + verticalConfigBuffer = makeUniformBuffer(device, configUniforms, { bloomStrength, direction: [1, 0] }); }; const execute = (encoder) => { const inputs = getInputs(); - - // TODO: set pipeline to blur pipeline - // TODO: bind config/source buffer group - // TODO: for every level, - // horizontally blur inputs.primary to horizontal blur output - // vertically blur the horizontal blur output to vertical blur output - - // TODO: set pipeline to the sum pipeline - // TODO: set bind group (vertical blur outputs) - // TODO: sum vertical blur into output + const tex = inputs.primary; + const intermediateView = intermediate.createView(); + const computePass = encoder.beginComputePass(); + computePass.setPipeline(computePipeline); + computePass.setBindGroup(0, makeBindGroup(device, computePipeline, 0, [horizontalConfigBuffer, nearestSampler, tex.createView(), intermediateView])); + computePass.dispatch(Math.ceil(Math.floor(screenSize[0] * bloomSize) / 32), screenSize[1], 1); + computePass.setBindGroup(0, makeBindGroup(device, computePipeline, 0, [verticalConfigBuffer, nearestSampler, intermediateView, output.createView()])); + computePass.dispatch(Math.ceil(Math.floor(screenSize[0] * bloomSize) / 32), Math.floor(screenSize[1] * bloomSize), 1); + computePass.endPass(); }; return makePass(getOutputs, ready, setSize, execute); }; - -/* - -// A pyramid is just an array of Targets, where each Target is half the width -// and half the height of the Target 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) => { - - // Build three pyramids of Targets, 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 Target 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 highPassTarget = highPassPyramid[i]; - const hBlurTarget = hBlurPyramid[i]; - const vBlurTarget = vBlurPyramid[i]; - highPass({ fbo: highPassTarget, frag: highPassFrag.text(), tex: inputs.primary }); - blur({ fbo: hBlurTarget, frag: blurFrag.text(), tex: highPassTarget, direction: [1, 0] }); - blur({ fbo: vBlurTarget, frag: blurFrag.text(), tex: hBlurTarget, direction: [0, 1] }); - } - - sumPyramid(); - } - ); -}; -*/ diff --git a/shaders/wgsl/blur1D.wgsl b/shaders/wgsl/blur1D.wgsl new file mode 100644 index 0000000..bc189ec --- /dev/null +++ b/shaders/wgsl/blur1D.wgsl @@ -0,0 +1,37 @@ +[[block]] struct Config { + bloomStrength : f32; + direction : vec2; +}; + +[[group(0), binding(0)]] var config : Config; +[[group(0), binding(1)]] var nearestSampler : sampler; +[[group(0), binding(2)]] var tex : texture_2d; +[[group(0), binding(3)]] var outputTex : texture_storage_2d; + +struct ComputeInput { + [[builtin(global_invocation_id)]] id : vec3; +}; + +[[stage(compute), workgroup_size(32, 1, 1)]] fn computeMain(input : ComputeInput) { + + var coord = vec2(input.id.xy); + var outputSize = textureDimensions(outputTex); + + if (coord.x >= outputSize.x) { + return; + } + + var uv = (vec2(coord) + 0.5) / vec2(outputSize); + var offset = config.direction / vec2(outputSize); + var sum = vec4(0.0); + + sum = sum + textureSampleLevel( tex, nearestSampler, uv + offset * 3.0, 0.0 ) * 0.006; + sum = sum + textureSampleLevel( tex, nearestSampler, uv + offset * 2.0, 0.0 ) * 0.061; + sum = sum + textureSampleLevel( tex, nearestSampler, uv + offset * 1.0, 0.0 ) * 0.242; + sum = sum + textureSampleLevel( tex, nearestSampler, uv + offset * 0.0, 0.0 ) * 0.383; + sum = sum + textureSampleLevel( tex, nearestSampler, uv + offset * -1.0, 0.0 ) * 0.242; + sum = sum + textureSampleLevel( tex, nearestSampler, uv + offset * -2.0, 0.0 ) * 0.061; + sum = sum + textureSampleLevel( tex, nearestSampler, uv + offset * -3.0, 0.0 ) * 0.006; + + textureStore(outputTex, coord, sum * config.bloomStrength); +}