Added some documentation, cleaned up some code, fleshed out the remaining work to make the project a little easier for newcomers to approach

This commit is contained in:
Rezmason
2021-10-20 21:01:32 -07:00
parent b4bece1264
commit 4c6ff879fd
12 changed files with 156 additions and 100 deletions

View File

@@ -1,6 +1,7 @@
import { loadText, makePassFBO, makePyramid, resizePyramid, 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)
@@ -9,8 +10,10 @@ const levelStrengths = Array(pyramidHeight)
.reverse();
export default (regl, config, inputs) => {
const enabled = config.bloomSize > 0 && config.bloomStrength > 0;
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,
@@ -18,16 +21,14 @@ export default (regl, config, inputs) => {
});
}
const { bloomStrength, highPassThreshold } = config;
// 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);
const highPassFrag = loadText("shaders/highPass.frag");
// The high pass restricts the blur to bright things in our input texture.
const highPassFrag = loadText("shaders/highPass.frag");
const highPass = regl({
frag: regl.prop("frag"),
uniforms: {
@@ -39,7 +40,8 @@ export default (regl, config, inputs) => {
// 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 3x1 blur approximates a more complex gaussian.
// by blurring them all, this basic blur approximates a more complex gaussian:
// https://software.intel.com/en-us/articles/compute-shader-hdr-and-bloom
const blurFrag = loadText("shaders/blur.frag");
const blur = regl({
@@ -53,8 +55,8 @@ export default (regl, config, inputs) => {
framebuffer: regl.prop("fbo"),
});
// The pyramid of textures gets flattened onto the source texture.
const flattenPyramid = regl({
// The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture
const sumPyramid = regl({
frag: `
precision mediump float;
varying vec2 vUV;
@@ -88,15 +90,15 @@ export default (regl, config, inputs) => {
blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] });
}
flattenPyramid();
sumPyramid();
},
(w, h) => {
// The blur pyramids can be lower resolution than the screen.
resizePyramid(highPassPyramid, w, h, config.bloomSize);
resizePyramid(hBlurPyramid, w, h, config.bloomSize);
resizePyramid(vBlurPyramid, w, h, config.bloomSize);
resizePyramid(highPassPyramid, w, h, bloomSize);
resizePyramid(hBlurPyramid, w, h, bloomSize);
resizePyramid(vBlurPyramid, w, h, bloomSize);
output.resize(w, h);
},
[highPassFrag.laoded, blurFrag.loaded]
[highPassFrag.loaded, blurFrag.loaded]
);
};

View File

@@ -1,5 +1,7 @@
import { loadImage, loadText, makePassFBO, makePass } from "./utils.js";
// Multiplies the rendered rain and bloom by a loaded in image
const defaultBGURL = "https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg";
export default (regl, config, inputs) => {

View File

@@ -1,5 +1,7 @@
import { makeFullScreenQuad, makePipeline } from "./utils.js";
import makeConfig from "./config.js";
import makeConfig from "./config.js"; // The settings of the effect, specified in the URL query params
import makeRain from "./rainPass.js";
import makeBloomPass from "./bloomPass.js";
import makePalettePass from "./palettePass.js";
@@ -34,12 +36,10 @@ const effects = {
};
const config = makeConfig(window.location.search);
const resolution = config.resolution;
const effect = config.effect in effects ? config.effect : "plain";
const resize = () => {
canvas.width = Math.ceil(canvas.clientWidth * resolution);
canvas.height = Math.ceil(canvas.clientHeight * resolution);
canvas.width = Math.ceil(canvas.clientWidth * config.resolution);
canvas.height = Math.ceil(canvas.clientHeight * config.resolution);
};
window.onresize = resize;
resize();
@@ -49,12 +49,11 @@ const dimensions = { width: 1, height: 1 };
document.body.onload = async () => {
// All this takes place in a full screen quad.
const fullScreenQuad = makeFullScreenQuad(regl);
const bloomPass = effect === "none" ? null : makeBloomPass;
const pipeline = makePipeline([makeRain, bloomPass, effects[effect]], (p) => p.outputs, regl, config);
const uniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
const drawToScreen = regl({ uniforms });
await Promise.all(pipeline.map(({ ready }) => ready));
const effectName = config.effect in effects ? config.effect : "plain";
const pipeline = makePipeline([makeRain, makeBloomPass, effects[effectName]], (p) => p.outputs, regl, config);
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
const drawToScreen = regl({ uniforms: screenUniforms });
await Promise.all(pipeline.map((step) => step.ready));
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
// tick.cancel();
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {

View File

@@ -1,5 +1,10 @@
import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js";
// Maps the brightness of the rendered rain and bloom to colors
// in a 1D gradient palette texture generated from the passed-in color sequence
// This shader introduces noise into the renders, to avoid banding
const colorToRGB = ([hue, saturation, lightness]) => {
const a = saturation * Math.min(lightness, 1 - lightness);
const f = (n) => {

View File

@@ -20,64 +20,32 @@ const brVert = [1, 1];
const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert];
export default (regl, config) => {
// 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 [numRows, numColumns] = [config.numColumns, config.numColumns * density];
// The volumetric mode requires us to create a grid of quads,
// rather than a single quad for our geometry
const [numQuadRows, numQuadColumns] = volumetric ? [numRows, numColumns] : [1, 1];
const numQuads = numQuadRows * numQuadColumns;
const quadSize = [1 / numQuadColumns, 1 / numQuadRows];
// Various effect-related values
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)];
const slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1);
const showComputationTexture = config.effect === "none";
const uniforms = {
...extractEntries(config, [
// general
"glyphHeightToWidth",
"glyphTextureColumns",
// compute
"animationSpeed",
"brightnessMinimum",
"brightnessMix",
"brightnessMultiplier",
"brightnessOffset",
"cursorEffectThreshold",
"cycleSpeed",
"fallSpeed",
"glyphSequenceLength",
"hasSun",
"hasThunder",
"raindropLength",
"rippleScale",
"rippleSpeed",
"rippleThickness",
"resurrectingCodeRatio",
// render vertex
"forwardSpeed",
// render fragment
"glyphEdgeCrop",
"isPolar",
]),
density,
numRows,
const commonUniforms = {
...extractEntries(config, ["animationSpeed", "glyphHeightToWidth", "glyphSequenceLength", "glyphTextureColumns", "resurrectingCodeRatio"]),
numColumns,
numQuadRows,
numQuadColumns,
quadSize,
volumetric,
rippleType,
cycleStyle,
slantVec,
slantScale,
numRows,
showComputationTexture,
};
const msdf = loadImage(regl, config.glyphTexURL);
// These two framebuffers are used to compute the raining code.
// they take turns being the source and destination of the "compute" shader.
// The half float data type is crucial! It lets us store almost any real number,
@@ -91,14 +59,31 @@ export default (regl, config) => {
wrapT: "clamp",
type: "half float",
});
const output = makePassFBO(regl, config.useHalfFloat);
const updateFrag = loadText("shaders/compute.frag");
const update = regl({
const computeFrag = loadText("shaders/compute.frag");
const computeUniforms = {
...commonUniforms,
...extractEntries(config, [
"brightnessMinimum",
"brightnessMix",
"brightnessMultiplier",
"brightnessOffset",
"cursorEffectThreshold",
"cycleSpeed",
"fallSpeed",
"hasSun",
"hasThunder",
"raindropLength",
"rippleScale",
"rippleSpeed",
"rippleThickness",
]),
cycleStyle,
rippleType,
};
const compute = regl({
frag: regl.prop("frag"),
uniforms: {
...uniforms,
...computeUniforms,
lastState: doubleBuffer.back,
},
@@ -114,8 +99,27 @@ export default (regl, config) => {
);
// We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen
const msdf = loadImage(regl, config.glyphTexURL);
const renderVert = loadText("shaders/rain.vert");
const renderFrag = loadText("shaders/rain.frag");
const output = makePassFBO(regl, config.useHalfFloat);
const renderUniforms = {
...commonUniforms,
...extractEntries(config, [
// vertex
"forwardSpeed",
// fragment
"glyphEdgeCrop",
"isPolar",
]),
density,
numQuadColumns,
numQuadRows,
quadSize,
slantScale,
slantVec,
volumetric,
};
const render = regl({
blend: {
enable: true,
@@ -128,7 +132,7 @@ export default (regl, config) => {
frag: regl.prop("frag"),
uniforms: {
...uniforms,
...renderUniforms,
lastState: doubleBuffer.front,
glyphTex: msdf.texture,
@@ -147,6 +151,7 @@ export default (regl, config) => {
framebuffer: output,
});
// Camera and transform math for the volumetric mode
const screenSize = [1, 1];
const { mat4, vec3 } = glMatrix;
const camera = mat4.create();
@@ -161,7 +166,7 @@ export default (regl, config) => {
primary: output,
},
() => {
update({ frag: updateFrag.text() });
compute({ frag: computeFrag.text() });
regl.clear({
depth: 1,
color: [0, 0, 0, 1],
@@ -175,6 +180,6 @@ export default (regl, config) => {
glMatrix.mat4.perspective(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000);
[screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
},
[msdf.loaded, updateFrag.loaded, renderVert.loaded, renderFrag.loaded]
[msdf.loaded, computeFrag.loaded, renderVert.loaded, renderFrag.loaded]
);
};

View File

@@ -1,5 +1,10 @@
import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js";
// Multiplies the rendered rain and bloom by a 1D gradient texture
// generated from the passed-in color sequence
// This shader introduces noise into the renders, to avoid banding
const transPrideStripeColors = [
[0.3, 1.0, 1.0],
[0.3, 1.0, 1.0],
@@ -27,6 +32,8 @@ export default (regl, config, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat);
const { backgroundColor } = config;
// Expand and convert stripe colors into 1D texture data
const stripeColors =
"stripeColors" in config ? config.stripeColors.split(",").map(parseFloat) : config.effect === "pride" ? prideStripeColors : transPrideStripeColors;
const numStripeColors = Math.floor(stripeColors.length / 3);