My friend recommended a much simpler blur that doesn't build any image pyramids. Unfortunately my work towards an implementation isn't very promising— it doesn't pack the same wollop as the WebGL version— but I can built my pyramids on top of it.

This commit is contained in:
Rezmason
2021-11-13 21:22:15 -08:00
parent cebd6877bb
commit 7eecace634
5 changed files with 97 additions and 136 deletions

View File

@@ -1,7 +1,16 @@
TODO: TODO:
WebGPU 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 blur pass
Switch to rgba32float somehow?
Why isn't this straightforward?
Update links in issues Update links in issues
Get rid of end pass once it's possible to copy a bgra8unorm to a canvas texture Get rid of end pass once it's possible to copy a bgra8unorm to a canvas texture
@@ -26,6 +35,9 @@ Resurrection
New glyphs? New glyphs?
Good luck with that, champ Good luck with that, champ
Zion Control's matrix variant
From Reloaded
Audio Audio
Synthesize raindrop sound Synthesize raindrop sound
https://www.instagram.com/tv/CWGodRcoq7T/?utm_medium=copy_link https://www.instagram.com/tv/CWGodRcoq7T/?utm_medium=copy_link

View File

@@ -1,5 +1,5 @@
Reloaded/Revolutions: Reloaded/Revolutions:
モエヤキオカ7ケサスZ152ヨタワ4ネヌナ98ヒ0ホア3ウ セ¦:"꞊ミラリ╌ツテニハソ▪—<>0|+*コシマムメ モエヤキオカ7ケサスz152ヨタワ4ネヌナ98ヒ0ホア3ウ セ¦:"꞊ミラリ╌ツテニハソ▪—<>0|+*コシマムメ
Resurrection: Resurrections:
モエヤキオカ7ケサスZ152ヨタワ4ネヌナ98ヒ0ホア3ウ セ¦:"꞊ミラリ╌ツテニハソコ—<ム0|*▪メシマ>+ モエヤキオカ7ケサスz152ヨタワ4ネヌナ98ヒ0ホア3ウ セ¦:"꞊ミラリ╌ツテニハソコ—<ム0|*▪メシマ>+

View File

@@ -26,6 +26,7 @@ const defaults = {
animationSpeed: 1, // The global rate that all animations progress animationSpeed: 1, // The global rate that all animations progress
forwardSpeed: 0.25, // The speed volumetric rain approaches the eye forwardSpeed: 0.25, // The speed volumetric rain approaches the eye
bloomStrength: 1, // The intensity of the bloom 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 bloomSize: 0.6, // The amount the bloom calculation is scaled
highPassThreshold: 0.1, // The minimum brightness that is still blurred highPassThreshold: 0.1, // The minimum brightness that is still blurred
cycleSpeed: 1, // The speed glyphs change cycleSpeed: 1, // The speed glyphs change
@@ -70,6 +71,7 @@ const versions = {
...defaults, ...defaults,
...fonts.matrixcode, ...fonts.matrixcode,
bloomStrength: 0.75, bloomStrength: 0.75,
newBloomStrength: 1.5,
highPassThreshold: 0.0, highPassThreshold: 0.0,
cycleSpeed: 0.2, cycleSpeed: 0.2,
cycleFrameSkip: 8, cycleFrameSkip: 8,
@@ -111,6 +113,7 @@ const versions = {
...defaults, ...defaults,
...fonts.coptic, ...fonts.coptic,
bloomStrength: 1, bloomStrength: 1,
newBloomStrength: 1,
highPassThreshold: 0, highPassThreshold: 0,
cycleSpeed: 0.1, cycleSpeed: 0.1,
brightnessDecay: 0.05, brightnessDecay: 0.05,

View File

@@ -1,19 +1,13 @@
import { structs } from "/lib/gpu-buffer.js"; import { structs } from "/lib/gpu-buffer.js";
import { loadShader, makeUniformBuffer, makeBindGroup, makeComputeTarget, makePass } from "./utils.js"; import { makeComputeTarget, loadShader, makeUniformBuffer, makeBindGroup, 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();
export default (context, getInputs) => { export default (context, getInputs) => {
const { config, device } = context; 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 there's no bloom to apply, return a no-op pass with an empty bloom texture
if (!enabled) { if (!enabled) {
@@ -24,142 +18,57 @@ export default (context, getInputs) => {
const assets = [loadShader(device, "shaders/wgsl/blur1D.wgsl")]; const assets = [loadShader(device, "shaders/wgsl/blur1D.wgsl")];
// TODO: generate sum shader code const nearestSampler = device.createSampler({});
const computeTarget = makeComputeTarget(device, 1, 1); let computePipeline;
const getOutputs = () => ({ ...getInputs(), bloom: computeTarget }); // TODO let configUniforms;
let horizontalConfigBuffer;
let verticalConfigBuffer;
let intermediate;
let output;
let screenSize;
let blurPipeline; const getOutputs = () => ({
let sumPipeline; primary: getInputs().primary,
bloom: output,
});
const ready = (async () => { const ready = (async () => {
const [blurShader] = await Promise.all(assets); const [blurShader] = await Promise.all(assets);
// TODO: create sum shader
// TODO: create config buffer
// TODO: create blur render pipeline computePipeline = device.createComputePipeline({
// TODO: create sum render pipeline compute: {
module: blurShader.module,
entryPoint: "computeMain",
},
});
configUniforms = structs.from(blurShader.code).Config;
})(); })();
const setSize = (width, height) => { const setSize = (width, height) => {
// TODO: destroy output intermediate?.destroy();
// TODO: create output intermediate = makeComputeTarget(device, Math.floor(width * bloomSize), height);
// TODO: destroy pyramid textures output?.destroy();
// TODO: create new pyramid textures output = makeComputeTarget(device, Math.floor(width * bloomSize), Math.floor(height * bloomSize));
// TODO: create new pyramid bindings screenSize = [width, height];
// TODO: create new pyramid renderPassConfigs
horizontalConfigBuffer = makeUniformBuffer(device, configUniforms, { bloomStrength, direction: [0, bloomSize] });
verticalConfigBuffer = makeUniformBuffer(device, configUniforms, { bloomStrength, direction: [1, 0] });
}; };
const execute = (encoder) => { const execute = (encoder) => {
const inputs = getInputs(); const inputs = getInputs();
const tex = inputs.primary;
// TODO: set pipeline to blur pipeline const intermediateView = intermediate.createView();
// TODO: bind config/source buffer group const computePass = encoder.beginComputePass();
// TODO: for every level, computePass.setPipeline(computePipeline);
// horizontally blur inputs.primary to horizontal blur output computePass.setBindGroup(0, makeBindGroup(device, computePipeline, 0, [horizontalConfigBuffer, nearestSampler, tex.createView(), intermediateView]));
// vertically blur the horizontal blur output to vertical blur output 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()]));
// TODO: set pipeline to the sum pipeline computePass.dispatch(Math.ceil(Math.floor(screenSize[0] * bloomSize) / 32), Math.floor(screenSize[1] * bloomSize), 1);
// TODO: set bind group (vertical blur outputs) computePass.endPass();
// TODO: sum vertical blur into output
}; };
return makePass(getOutputs, ready, setSize, execute); 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();
}
);
};
*/

37
shaders/wgsl/blur1D.wgsl Normal file
View File

@@ -0,0 +1,37 @@
[[block]] struct Config {
bloomStrength : f32;
direction : vec2<f32>;
};
[[group(0), binding(0)]] var<uniform> config : Config;
[[group(0), binding(1)]] var nearestSampler : sampler;
[[group(0), binding(2)]] var tex : texture_2d<f32>;
[[group(0), binding(3)]] var outputTex : texture_storage_2d<rgba8unorm, write>;
struct ComputeInput {
[[builtin(global_invocation_id)]] id : vec3<u32>;
};
[[stage(compute), workgroup_size(32, 1, 1)]] fn computeMain(input : ComputeInput) {
var coord = vec2<i32>(input.id.xy);
var outputSize = textureDimensions(outputTex);
if (coord.x >= outputSize.x) {
return;
}
var uv = (vec2<f32>(coord) + 0.5) / vec2<f32>(outputSize);
var offset = config.direction / vec2<f32>(outputSize);
var sum = vec4<f32>(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);
}