mirror of
https://github.com/Rezmason/matrix.git
synced 2026-04-16 21:39:29 -07:00
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:
@@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
21
js/main.js
21
js/main.js
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
105
js/rainPass.js
105
js/rainPass.js
@@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user