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,5 +1,21 @@
TODO:
Improve forkability
Document every variable in config.js
Document every variable, method, and section of the main function in compute.frag
Maybe rewrite it? Make the time based stuff easier to read?
Document resurrectionPass
Label it a WIP
List intended characteristics
Comment makePalette
Write a document (and include images) that explains the underlying principle of the rain pass
Create interactive controls?
Is the pipeline stuff just too bizarre?
Resurrection
Modified glyph order?
@@ -14,6 +30,11 @@ Resurrection
New glyphs?
Experiment with varying the colors in the palette pass
Maybe a separate palette for the non-bloom
Maybe dim and widen the bloom

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);

View File

@@ -6,6 +6,9 @@ varying vec2 vUV;
void main() {
vec3 bgColor = texture2D(backgroundTex, vUV).rgb;
// Combine the texture and bloom, then blow it out to reveal more of the image
float brightness = pow(min(1., texture2D(tex, vUV).r * 2.) + texture2D(bloomTex, vUV).r, 1.5);
gl_FragColor = vec4(bgColor * brightness, 1.0);
}

View File

@@ -17,7 +17,13 @@ highp float rand( const in vec2 uv, const in float t ) {
void main() {
vec4 brightnessRGB = texture2D( tex, vUV ) + texture2D( bloomTex, vUV );
// Combine the texture and bloom
float brightness = brightnessRGB.r + brightnessRGB.g + brightnessRGB.b;
float at = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude;
gl_FragColor = texture2D( palette, vec2(at, 0.0)) + vec4(backgroundColor, 0.0);
// Dither: subtract a random value from the brightness
brightness = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude;
// Map the brightness to a position in the palette texture
gl_FragColor = texture2D( palette, vec2(brightness, 0.0)) + vec4(backgroundColor, 0.0);
}

View File

@@ -33,9 +33,10 @@ void main() {
vec2 uv = vUV;
// In normal mode, derives the current glyph and UV from vUV
if (!volumetric) {
if (isPolar) {
// Curves the UV space to make letters appear to radiate from up above
// Curved space that makes letters appear to radiate from up above
uv -= 0.5;
uv *= 0.5;
uv.y -= 0.5;
@@ -43,35 +44,29 @@ void main() {
float angle = atan(uv.y, uv.x) / (2. * PI) + 0.5;
uv = vec2(angle * 4. - 0.5, 1.5 - pow(radius, 0.5) * 1.5);
} else {
// Applies the slant, scaling the UV space
// to guarantee the viewport is still covered
// Applies the slant and scales space so the viewport is fully covered
uv = vec2(
(uv.x - 0.5) * slantVec.x + (uv.y - 0.5) * slantVec.y,
(uv.y - 0.5) * slantVec.x - (uv.x - 0.5) * slantVec.y
(uv.x - 0.5) * slantVec.x + (uv.y - 0.5) * slantVec.y,
(uv.y - 0.5) * slantVec.x - (uv.x - 0.5) * slantVec.y
) * slantScale + 0.5;
}
uv.y /= glyphHeightToWidth;
}
// Unpack the values from the data texture
vec4 glyph = volumetric ? vGlyph : texture2D(lastState, uv);
if (showComputationTexture) {
gl_FragColor = glyph;
return;
}
// Unpack the values from the font texture
float brightness = glyph.r;
float symbolIndex = getSymbolIndex(glyph.g);
float quadDepth = glyph.b;
float effect = glyph.a;
brightness = max(effect, brightness);
// In volumetric mode, distant glyphs are dimmer
if (volumetric) {
brightness = min(1.0, brightness * quadDepth * 1.25);
brightness = brightness * min(1.0, quadDepth);
}
// resolve UV to MSDF texture coord
// resolve UV to position of glyph in MSDF texture
vec2 symbolUV = vec2(mod(symbolIndex, glyphTextureColumns), floor(symbolIndex / glyphTextureColumns));
vec2 glyphUV = fract(uv * vec2(numColumns, numRows));
glyphUV -= 0.5;
@@ -79,10 +74,15 @@ void main() {
glyphUV += 0.5;
vec2 msdfUV = (glyphUV + symbolUV) / glyphTextureColumns;
// MSDF
// MSDF: calculate brightness of fragment based on distance to shape
vec3 dist = texture2D(glyphTex, msdfUV).rgb;
float sigDist = median3(dist) - 0.5;
float alpha = clamp(sigDist/fwidth(sigDist) + 0.5, 0.0, 1.0);
gl_FragColor = vec4(vChannel * brightness * alpha, 1.0);
if (showComputationTexture) {
gl_FragColor = vec4(glyph.rgb * alpha, 1.0);
} else {
gl_FragColor = vec4(vChannel * brightness * alpha, 1.0);
}
}

View File

@@ -26,6 +26,7 @@ void main() {
vUV = (aPosition + aCorner) * quadSize;
vGlyph = texture2D(lastState, aPosition * quadSize);
// Calculate the world space position
float quadDepth = 0.0;
if (volumetric && !showComputationTexture) {
quadDepth = fract(vGlyph.b + time * animationSpeed * forwardSpeed);
@@ -34,16 +35,17 @@ void main() {
vec2 position = (aPosition + aCorner * vec2(density, 1.)) * quadSize;
vec4 pos = vec4((position - 0.5) * 2.0, quadDepth, 1.0);
// "Resurrected" columns are in the green channel,
// and are vertically flipped (along with their glyphs)
vChannel = vec3(1.0, 0.0, 0.0);
if (volumetric && rand(vec2(aPosition.x, 0)) < resurrectingCodeRatio) {
pos.y = -pos.y;
vChannel = vec3(0.0, 1.0, 0.0);
}
// Convert the world space position to screen space
if (volumetric) {
if (rand(vec2(aPosition.x, 0)) < resurrectingCodeRatio) {
pos.y = -pos.y;
vChannel = vec3(0.0, 1.0, 0.0);
}
pos.x /= glyphHeightToWidth;
pos = camera * transform * pos;
} else {
pos.xy *= screenSize;

View File

@@ -17,7 +17,11 @@ highp float rand( const in vec2 uv, const in float t ) {
void main() {
vec3 color = texture2D(stripes, vUV).rgb;
// Combine the texture and bloom
float brightness = min(1., texture2D(tex, vUV).r * 2.) + texture2D(bloomTex, vUV).r;
float at = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude;
gl_FragColor = vec4(color * at + backgroundColor, 1.0);
// Dither: subtract a random value from the brightness
brightness = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude;
gl_FragColor = vec4(color * brightness + backgroundColor, 1.0);
}