Rewrote the WebGPU bloom pass based on the classic Unreal solution of blurring and combining the levels of an image pyramid. Fixed the regl bloom pass to use the downscaled blurred mipmap levels to build the first pyramid.

This commit is contained in:
Rezmason
2021-11-14 22:01:56 -08:00
parent f907c1c91b
commit b0a4acdfdb
8 changed files with 170 additions and 78 deletions

View File

@@ -1,13 +1,15 @@
import { structs } from "/lib/gpu-buffer.js";
import { makeComputeTarget, loadShader, makeUniformBuffer, makeBindGroup, makePass } from "./utils.js";
import { makeComputeTarget, makePyramidView, loadShader, makeUniformBuffer, makeBindGroup, makePass } from "./utils.js";
export default (context, getInputs) => {
const { config, device } = context;
const pyramidHeight = 4;
const bloomSize = config.bloomSize;
const bloomStrength = config.newBloomStrength;
const bloomStrength = config.bloomStrength;
const bloomRadius = 2; // Looks better with more, but is more costly
const enabled = bloomSize > 0 && bloomStrength > 0;
const enabled = true;
// If there's no bloom to apply, return a no-op pass with an empty bloom texture
if (!enabled) {
@@ -16,17 +18,22 @@ export default (context, getInputs) => {
return makePass(getOutputs);
}
const assets = [loadShader(device, "shaders/wgsl/blur1D.wgsl")];
const assets = [loadShader(device, "shaders/wgsl/bloomBlur.wgsl"), loadShader(device, "shaders/wgsl/bloomCombine.wgsl")];
const nearestSampler = device.createSampler({});
const linearSampler = device.createSampler({
magFilter: "linear",
minFilter: "linear",
});
let computePipeline;
let configUniforms;
let horizontalConfigBuffer;
let verticalConfigBuffer;
let intermediate;
let blurPipeline;
let combinePipeline;
let hBlurPyramid;
let vBlurPyramid;
let hBlurBuffer;
let vBlurBuffer;
let combineBuffer;
let output;
let screenSize;
let scaledScreenSize;
const getOutputs = () => ({
primary: getInputs().primary,
@@ -34,39 +41,69 @@ export default (context, getInputs) => {
});
const ready = (async () => {
const [blurShader] = await Promise.all(assets);
const [blurShader, combineShader] = await Promise.all(assets);
computePipeline = device.createComputePipeline({
blurPipeline = device.createComputePipeline({
compute: {
module: blurShader.module,
entryPoint: "computeMain",
},
});
configUniforms = structs.from(blurShader.code).Config;
combinePipeline = device.createComputePipeline({
compute: {
module: combineShader.module,
entryPoint: "computeMain",
},
});
const blurUniforms = structs.from(blurShader.code).Config;
hBlurBuffer = makeUniformBuffer(device, blurUniforms, { bloomRadius, direction: [1, 0] });
vBlurBuffer = makeUniformBuffer(device, blurUniforms, { bloomRadius, direction: [0, 1] });
const combineUniforms = structs.from(combineShader.code).Config;
combineBuffer = makeUniformBuffer(device, combineUniforms, { bloomStrength, pyramidHeight });
})();
const setSize = (width, height) => {
intermediate?.destroy();
intermediate = makeComputeTarget(device, Math.floor(width * bloomSize), height);
hBlurPyramid?.destroy();
hBlurPyramid = makeComputeTarget(device, Math.floor(width * bloomSize), Math.floor(height * bloomSize), pyramidHeight);
vBlurPyramid?.destroy();
vBlurPyramid = makeComputeTarget(device, Math.floor(width * bloomSize), Math.floor(height * bloomSize), pyramidHeight);
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] });
scaledScreenSize = [Math.floor(width * bloomSize), Math.floor(height * bloomSize)];
};
const execute = (encoder) => {
const inputs = getInputs();
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.setPipeline(blurPipeline);
const hBlurPyramidViews = Array(pyramidHeight)
.fill()
.map((_, level) => makePyramidView(hBlurPyramid, level));
const vBlurPyramidViews = Array(pyramidHeight)
.fill()
.map((_, level) => makePyramidView(vBlurPyramid, level));
for (let i = 0; i < pyramidHeight; i++) {
const downsample = 2 ** -i;
const size = [Math.ceil(Math.floor(scaledScreenSize[0] * downsample) / 32), Math.floor(Math.floor(scaledScreenSize[1] * downsample)), 1];
const srcView = i === 0 ? tex.createView() : hBlurPyramidViews[i - 1];
computePass.setBindGroup(0, makeBindGroup(device, blurPipeline, 0, [hBlurBuffer, linearSampler, srcView, hBlurPyramidViews[i]]));
computePass.dispatch(...size);
computePass.setBindGroup(0, makeBindGroup(device, blurPipeline, 0, [vBlurBuffer, linearSampler, hBlurPyramidViews[i], vBlurPyramidViews[i]]));
computePass.dispatch(...size);
}
computePass.setPipeline(combinePipeline);
computePass.setBindGroup(0, makeBindGroup(device, combinePipeline, 0, [combineBuffer, linearSampler, vBlurPyramid.createView(), output.createView()]));
computePass.dispatch(Math.ceil(scaledScreenSize[0] / 32), scaledScreenSize[1], 1);
computePass.endPass();
};

View File

@@ -1,4 +1,4 @@
import { makeComputeTarget, loadTexture, loadShader, makeUniformBuffer, makeBindGroup, makePass } from "./utils.js";
import { makeComputeTarget, loadTexture, loadShader, makeBindGroup, makePass } from "./utils.js";
// Multiplies the rendered rain and bloom by a loaded in image

View File

@@ -27,16 +27,18 @@ const loadTexture = async (device, url) => {
return texture;
};
const makeRenderTarget = (device, width, height, format) =>
const makeRenderTarget = (device, width, height, format, mipLevelCount = 1) =>
device.createTexture({
size: [width, height, 1],
mipLevelCount,
format,
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
});
const makeComputeTarget = (device, width, height) =>
const makeComputeTarget = (device, width, height, mipLevelCount = 1) =>
device.createTexture({
size: [width, height, 1],
mipLevelCount,
format: "rgba8unorm",
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING,
});
@@ -76,6 +78,13 @@ const make1DTexture = (device, rgbas) => {
return texture;
};
const makePyramidView = (texture, level) =>
texture.createView({
baseMipLevel: level,
mipLevelCount: 1,
dimension: "2d",
});
const makeBindGroup = (device, pipeline, index, entries) =>
device.createBindGroup({
layout: pipeline.getBindGroupLayout(index),
@@ -97,4 +106,16 @@ const makePass = (getOutputs, 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)], []);
export { getCanvasSize, makeRenderTarget, makeComputeTarget, make1DTexture, loadTexture, loadShader, makeUniformBuffer, makePass, makePipeline, makeBindGroup };
export {
getCanvasSize,
makeRenderTarget,
makeComputeTarget,
make1DTexture,
makePyramidView,
loadTexture,
loadShader,
makeUniformBuffer,
makePass,
makePipeline,
makeBindGroup,
};