Crushed down the config, removed a ton more inessential stuff

This commit is contained in:
Rezmason
2023-08-23 11:28:29 -07:00
parent 2d97f764f5
commit d1f00e7e42
15 changed files with 323 additions and 673 deletions

View File

@@ -1,9 +1,6 @@
TODO: TODO:
Simplify! Simplify!
Pare down config
Get rid of everything inessential
Remove features
Remove subsystems
Get as much into one file as you possibly can Get as much into one file as you possibly can
Remove regl Remove regl
Record WebGL debug calls

View File

@@ -27,83 +27,7 @@
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
} }
p {
color: hsl(108, 90%, 70%);
text-shadow: hsl(108, 90%, 40%) 1px 0 10px;
}
.notice {
margin-top: 10em;
animation: fadeInAnimation ease 3s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
}
@keyframes fadeInAnimation {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.pill {
display: inline-block;
background: gray;
border: 0.3em solid lightgray;
font-size: 1rem;
font-family: monospace;
color: white;
padding: 0.5em 1em;
border-radius: 2em;
min-width: 6rem;
margin: 3em;
text-decoration: none;
cursor: pointer;
text-transform: uppercase;
font-weight: bold;
}
.blue {
background: linear-gradient(skyblue, blue, black, black, darkblue);
border-color: darkblue;
color: lightblue;
}
.blue:hover {
border-color: blue;
color: white;
}
.red {
background: linear-gradient(lightpink, crimson, black, black, darkred);
border-color: darkred;
color: lightpink;
}
.red:hover {
border-color: crimson;
color: white;
}
</style> </style>
</head> </head>
<body> <body><script type="module" src="js/main.js"></script></body>
<!--
This is an implementation of the green code seen in The Matrix film and video game franchise.
This project demonstrates five concepts:
1. Drawing to floating point frame buffer objects, or 'FBO's,
for performing computation and post-processing
2. GPU-side computation, with fragment shaders
updating two alternating FBOs
3. Rendering crisp "vector" graphics, with a multiple-channel
signed distance field (or 'MSDF')
4. Creating a blur/bloom effect from a texture pyramid
5. Color mapping with noise, to hide banding
For more information, please visit: https://github.com/Rezmason/matrix
-->
<script type="module" src="js/main.js"></script>
</body>
</html> </html>

View File

@@ -1,12 +1,7 @@
import { loadText, makePassFBO, makePass } from "./utils.js"; import { loadText, makePassFBO, 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, blurred textures.
const pyramidHeight = 5; const pyramidHeight = 5;
// A pyramid is just an array of FBOs, where each FBO is half the width
// and half the height of the FBO below it.
const makePyramid = (regl, height, halfFloat) => const makePyramid = (regl, height, halfFloat) =>
Array(height) Array(height)
.fill() .fill()
@@ -15,28 +10,33 @@ const makePyramid = (regl, height, halfFloat) =>
const resizePyramid = (pyramid, vw, vh, scale) => const resizePyramid = (pyramid, vw, vh, scale) =>
pyramid.forEach((fbo, index) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index))); pyramid.forEach((fbo, index) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index)));
export default ({ regl, config }, inputs) => { export default ({ regl }, inputs) => {
const { bloomStrength, bloomSize, highPassThreshold } = config; const bloomStrength = 0.7; // The intensity of the bloom
const enabled = bloomSize > 0 && bloomStrength > 0; const bloomSize = 0.4; // The amount the bloom calculation is scaled
const highPassThreshold = 0.1; // The minimum brightness that is still blurred
// If there's no bloom to apply, return a no-op pass with an empty bloom texture const highPassPyramid = makePyramid(regl, pyramidHeight);
if (!enabled) { const hBlurPyramid = makePyramid(regl, pyramidHeight);
return makePass({ const vBlurPyramid = makePyramid(regl, pyramidHeight);
primary: inputs.primary, const output = makePassFBO(regl);
bloom: makePassFBO(regl),
});
}
// 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);
// The high pass restricts the blur to bright things in our input texture.
const highPassFrag = loadText("shaders/glsl/bloomPass.highPass.frag.glsl");
const highPass = regl({ const highPass = regl({
frag: regl.prop("frag"), frag: `
precision mediump float;
uniform sampler2D tex;
uniform float highPassThreshold;
varying vec2 vUV;
void main() {
vec4 color = texture2D(tex, vUV);
if (color.r < highPassThreshold) color.r = 0.0;
if (color.g < highPassThreshold) color.g = 0.0;
if (color.b < highPassThreshold) color.b = 0.0;
gl_FragColor = color;
}
`,
uniforms: { uniforms: {
highPassThreshold, highPassThreshold,
tex: regl.prop("tex"), tex: regl.prop("tex"),
@@ -44,14 +44,26 @@ export default ({ regl, config }, inputs) => {
framebuffer: regl.prop("fbo"), framebuffer: regl.prop("fbo"),
}); });
// 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 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/bloomPass.blur.frag.glsl");
const blur = regl({ const blur = regl({
frag: regl.prop("frag"), frag: `
precision mediump float;
uniform float width, height;
uniform sampler2D tex;
uniform vec2 direction;
varying vec2 vUV;
void main() {
vec2 size = width > height ? vec2(width / height, 1.) : vec2(1., height / width);
gl_FragColor =
texture2D(tex, vUV) * 0.442 +
(
texture2D(tex, vUV + direction / max(width, height) * size) +
texture2D(tex, vUV - direction / max(width, height) * size)
) * 0.279;
}
`,
uniforms: { uniforms: {
tex: regl.prop("tex"), tex: regl.prop("tex"),
direction: regl.prop("direction"), direction: regl.prop("direction"),
@@ -62,9 +74,24 @@ export default ({ regl, config }, inputs) => {
}); });
// The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture // The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture
const combineFrag = loadText("shaders/glsl/bloomPass.combine.frag.glsl");
const combine = regl({ const combine = regl({
frag: regl.prop("frag"), frag: `
precision mediump float;
uniform sampler2D pyr_0, pyr_1, pyr_2, pyr_3, pyr_4;
uniform float bloomStrength;
varying vec2 vUV;
void main() {
vec4 total = vec4(0.);
total += texture2D(pyr_0, vUV) * 0.96549;
total += texture2D(pyr_1, vUV) * 0.92832;
total += texture2D(pyr_2, vUV) * 0.88790;
total += texture2D(pyr_3, vUV) * 0.84343;
total += texture2D(pyr_4, vUV) * 0.79370;
gl_FragColor = total * bloomStrength;
}
`,
uniforms: { uniforms: {
bloomStrength, bloomStrength,
...Object.fromEntries(vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])), ...Object.fromEntries(vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])),
@@ -77,7 +104,7 @@ export default ({ regl, config }, inputs) => {
primary: inputs.primary, primary: inputs.primary,
bloom: output, bloom: output,
}, },
Promise.all([highPassFrag.loaded, blurFrag.loaded]), null,
(w, h) => { (w, h) => {
// The blur pyramids can be lower resolution than the screen. // The blur pyramids can be lower resolution than the screen.
resizePyramid(highPassPyramid, w, h, bloomSize); resizePyramid(highPassPyramid, w, h, bloomSize);
@@ -85,21 +112,17 @@ export default ({ regl, config }, inputs) => {
resizePyramid(vBlurPyramid, w, h, bloomSize); resizePyramid(vBlurPyramid, w, h, bloomSize);
output.resize(w, h); output.resize(w, h);
}, },
(shouldRender) => { () => {
if (!shouldRender) {
return;
}
for (let i = 0; i < pyramidHeight; i++) { for (let i = 0; i < pyramidHeight; i++) {
const highPassFBO = highPassPyramid[i]; const highPassFBO = highPassPyramid[i];
const hBlurFBO = hBlurPyramid[i]; const hBlurFBO = hBlurPyramid[i];
const vBlurFBO = vBlurPyramid[i]; const vBlurFBO = vBlurPyramid[i];
highPass({ fbo: highPassFBO, frag: highPassFrag.text(), tex: i === 0 ? inputs.primary : highPassPyramid[i - 1] }); highPass({ fbo: highPassFBO, tex: i === 0 ? inputs.primary : highPassPyramid[i - 1] });
blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] }); blur({ fbo: hBlurFBO, tex: highPassFBO, direction: [1, 0] });
blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] }); blur({ fbo: vBlurFBO, tex: hBlurFBO, direction: [0, 1] });
} }
combine({ frag: combineFrag.text() }); combine();
} }
); );
}; };

View File

@@ -1,12 +0,0 @@
export default ({ space, values }) => {
if (space === "rgb") {
return values;
}
const [hue, saturation, lightness] = values;
const a = saturation * Math.min(lightness, 1 - lightness);
const f = (n) => {
const k = (n + hue * 12) % 12;
return lightness - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
};
return [f(0), f(8), f(4)];
};

View File

@@ -1,47 +1,3 @@
const hsl = (...values) => ({ space: "hsl", values });
const config = {
glyphMSDFURL: "assets/matrixcode_msdf.png",
glyphSequenceLength: 57,
glyphTextureGridSize: [8, 8],
backgroundColor: hsl(0, 0, 0), // The color "behind" the glyphs
isolateCursor: true, // Whether the "cursor"— the brightest glyph at the bottom of a raindrop— has its own color
cursorColor: hsl(0.242, 1, 0.73), // The color of the cursor
cursorIntensity: 2, // The intensity of the cursor
glintColor: hsl(0, 0, 1), // The color of the glint
glintIntensity: 1, // The intensity of the glint
animationSpeed: 1, // The global rate that all animations progress
fps: 60, // The target frame rate (frames per second) of the effect
forwardSpeed: 0.25, // The speed volumetric rain approaches the eye
bloomStrength: 0.7, // The intensity of the bloom
bloomSize: 0.4, // The amount the bloom calculation is scaled
highPassThreshold: 0.1, // The minimum brightness that is still blurred
cycleSpeed: 0.03, // The speed glyphs change
cycleFrameSkip: 1, // The global minimum number of frames between glyphs cycling
baseBrightness: -0.5, // The brightness of the glyphs, before any effects are applied
baseContrast: 1.1, // The contrast of the glyphs, before any effects are applied
glintBrightness: -1.5, // The brightness of the glints, before any effects are applied
glintContrast: 2.5, // The contrast of the glints, before any effects are applied
brightnessOverride: 0.0, // A global override to the brightness of displayed glyphs. Only used if it is > 0.
brightnessThreshold: 0, // The minimum brightness for a glyph to still be considered visible
ditherMagnitude: 0.05, // The magnitude of the random per-pixel dimming
fallSpeed: 0.3, // The speed the raindrops progress downwards
glyphEdgeCrop: 0.0, // The border around a glyph in a font texture that should be cropped out
glyphHeightToWidth: 1, // The aspect ratio of glyphs
glyphVerticalSpacing: 1, // The ratio of the vertical distance between glyphs to their height
numColumns: 80, // The maximum dimension of the glyph grid
palette: [
// The color palette that glyph brightness is color mapped to
{ color: hsl(0.3, 0.9, 0.0), at: 0.0 },
{ color: hsl(0.3, 0.9, 0.2), at: 0.2 },
{ color: hsl(0.3, 0.9, 0.7), at: 0.7 },
{ color: hsl(0.3, 0.9, 0.8), at: 0.8 },
],
raindropLength: 0.75, // Adjusts the frequency of raindrops (and their length) in a column
resolution: 0.75, // An overall scale multiplier
useHalfFloat: false,
};
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
document.body.appendChild(canvas); document.body.appendChild(canvas);
document.addEventListener("touchmove", (e) => e.preventDefault(), { document.addEventListener("touchmove", (e) => e.preventDefault(), {
@@ -70,8 +26,8 @@ const init = async () => {
const resize = () => { const resize = () => {
const devicePixelRatio = window.devicePixelRatio ?? 1; const devicePixelRatio = window.devicePixelRatio ?? 1;
canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * config.resolution); canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * 0.75);
canvas.height = Math.ceil(canvas.clientHeight * devicePixelRatio * config.resolution); canvas.height = Math.ceil(canvas.clientHeight * devicePixelRatio * 0.75);
}; };
window.onresize = resize; window.onresize = resize;
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) { if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
@@ -97,37 +53,14 @@ const init = async () => {
// All this takes place in a full screen quad. // All this takes place in a full screen quad.
const fullScreenQuad = makeFullScreenQuad(regl); const fullScreenQuad = makeFullScreenQuad(regl);
const context = { regl, config }; const pipeline = makePipeline({ regl }, [makeRain, makeBloomPass, makePalettePass]);
const pipeline = makePipeline(context, [makeRain, makeBloomPass, makePalettePass]);
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary }; const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
const drawToScreen = regl({ uniforms: screenUniforms }); const drawToScreen = regl({ uniforms: screenUniforms });
await Promise.all(pipeline.map((step) => step.ready)); await Promise.all(pipeline.map((step) => step.ready));
const targetFrameTimeMilliseconds = 1000 / config.fps;
let last = NaN;
const render = ({ viewportWidth, viewportHeight }) => { const render = ({ viewportWidth, viewportHeight }) => {
if (config.once) {
tick.cancel();
}
const now = regl.now() * 1000; const now = regl.now() * 1000;
if (isNaN(last)) {
last = now;
}
const shouldRender = config.fps >= 60 || now - last >= targetFrameTimeMilliseconds || config.once == true;
if (shouldRender) {
while (now - targetFrameTimeMilliseconds > last) {
last += targetFrameTimeMilliseconds;
}
}
if (config.useCamera) {
cameraTex(cameraCanvas);
}
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) { if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {
dimensions.width = viewportWidth; dimensions.width = viewportWidth;
dimensions.height = viewportHeight; dimensions.height = viewportHeight;
@@ -137,17 +70,17 @@ const init = async () => {
} }
fullScreenQuad(() => { fullScreenQuad(() => {
for (const step of pipeline) { for (const step of pipeline) {
step.execute(shouldRender); step.execute();
} }
drawToScreen(); drawToScreen();
}); });
}; };
render({viewportWidth: 1, viewportHeight: 1}); render({ viewportWidth: 1, viewportHeight: 1 });
const tick = regl.frame(render); const tick = regl.frame(render);
}; };
document.body.onload = () => { document.body.onload = () => {
init(); init();
} };

View File

@@ -1,79 +1,54 @@
import colorToRGB from "./colorToRGB.js"; import { make1DTexture, makePassFBO, makePass } from "./utils.js";
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 makePalette = (regl, entries) => {
const PALETTE_SIZE = 2048;
const paletteColors = Array(PALETTE_SIZE);
// Convert HSL gradient into sorted RGB gradient, capping the ends
const sortedEntries = entries
.slice()
.sort((e1, e2) => e1.at - e2.at)
.map((entry) => ({
rgb: colorToRGB(entry.color),
arrayIndex: Math.floor(Math.max(Math.min(1, entry.at), 0) * (PALETTE_SIZE - 1)),
}));
sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 });
sortedEntries.push({
rgb: sortedEntries[sortedEntries.length - 1].rgb,
arrayIndex: PALETTE_SIZE - 1,
});
// Interpolate between the sorted RGB entries to generate
// the palette texture data
sortedEntries.forEach((entry, index) => {
paletteColors[entry.arrayIndex] = entry.rgb.slice();
if (index + 1 < sortedEntries.length) {
const nextEntry = sortedEntries[index + 1];
const diff = nextEntry.arrayIndex - entry.arrayIndex;
for (let i = 0; i < diff; i++) {
const ratio = i / diff;
paletteColors[entry.arrayIndex + i] = [
entry.rgb[0] * (1 - ratio) + nextEntry.rgb[0] * ratio,
entry.rgb[1] * (1 - ratio) + nextEntry.rgb[1] * ratio,
entry.rgb[2] * (1 - ratio) + nextEntry.rgb[2] * ratio,
];
}
}
});
return make1DTexture(
regl,
paletteColors.map((rgb) => [...rgb, 1])
);
};
// The rendered texture's values are mapped to colors in a palette texture.
// A little noise is introduced, to hide the banding that appears
// in subtle gradients. The noise is also time-driven, so its grain
// won't persist across subsequent frames. This is a safe trick
// in screen space.
export default ({ regl, config }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat);
const paletteTex = makePalette(regl, config.palette);
const { backgroundColor, cursorColor, glintColor, cursorIntensity, glintIntensity, ditherMagnitude } = config;
const palettePassFrag = loadText("shaders/glsl/palettePass.frag.glsl");
export default ({ regl }, inputs) => {
const output = makePassFBO(regl);
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: `
precision mediump float;
#define PI 3.14159265359
uniform sampler2D tex, bloomTex, paletteTex;
uniform float time;
varying vec2 vUV;
highp float rand( const in vec2 uv, const in float t ) {
const highp float a = 12.9898, b = 78.233, c = 43758.5453;
highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
return fract(sin(sn) * c + t);
}
void main() {
vec4 primary = texture2D(tex, vUV);
vec4 bloom = texture2D(bloomTex, vUV);
vec4 brightness = primary + bloom - rand( gl_FragCoord.xy, time ) * 0.0167;
gl_FragColor = vec4(
texture2D( paletteTex, vec2(brightness.r, 0.0)).rgb
+ min(vec3(0.756, 1.0, 0.46) * brightness.g * 2.0, vec3(1.0)),
1.0
);
}
`,
uniforms: { uniforms: {
backgroundColor: colorToRGB(backgroundColor),
cursorColor: colorToRGB(cursorColor),
glintColor: colorToRGB(glintColor),
cursorIntensity,
glintIntensity,
ditherMagnitude,
tex: inputs.primary, tex: inputs.primary,
bloomTex: inputs.bloom, bloomTex: inputs.bloom,
paletteTex, paletteTex: make1DTexture(regl, [
[0.0, 0.0, 0.0, 1.0],
[0.03, 0.13, 0.0, 1.0],
[0.06, 0.25, 0.01, 1.0],
[0.09, 0.38, 0.02, 1.0],
[0.15, 0.46, 0.07, 1.0],
[0.21, 0.54, 0.13, 1.0],
[0.28, 0.63, 0.19, 1.0],
[0.34, 0.71, 0.25, 1.0],
[0.41, 0.8, 0.31, 1.0],
[0.47, 0.88, 0.37, 1.0],
[0.53, 0.97, 0.43, 1.0],
[0.61, 0.97, 0.52, 1.0],
[0.69, 0.98, 0.62, 1.0],
[0.69, 0.98, 0.62, 1.0],
[0.69, 0.98, 0.62, 1.0],
[0.69, 0.98, 0.62, 1.0],
]),
}, },
framebuffer: output, framebuffer: output,
}); });
@@ -82,12 +57,8 @@ export default ({ regl, config }, inputs) => {
{ {
primary: output, primary: output,
}, },
palettePassFrag.loaded, null,
(w, h) => output.resize(w, h), (w, h) => output.resize(w, h),
(shouldRender) => { () => render()
if (shouldRender) {
render({ frag: palettePassFrag.text() });
}
}
); );
}; };

View File

@@ -1,21 +1,4 @@
import { loadImage, loadText, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js"; import { loadImage, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js";
const extractEntries = (src, keys) => Object.fromEntries(Array.from(Object.entries(src)).filter(([key]) => keys.includes(key)));
// These compute buffers are used to compute the properties of cells in the grid.
// They take turns being the source and destination of a "compute" shader.
// The half float data type is crucial! It lets us store almost any real number,
// whereas the default type limits us to integers between 0 and 255.
// These double buffers are smaller than the screen, because their pixels correspond
// with cells in the grid, and the cells' glyphs are much larger than a pixel.
const makeComputeDoubleBuffer = (regl, height, width) =>
makeDoubleBuffer(regl, {
width,
height,
wrapT: "clamp",
type: "half float",
});
const numVerticesPerQuad = 2 * 3; const numVerticesPerQuad = 2 * 3;
const tlVert = [0, 0]; const tlVert = [0, 0];
@@ -24,26 +7,108 @@ const blVert = [1, 0];
const brVert = [1, 1]; const brVert = [1, 1];
const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert]; const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert];
export default ({ regl, config }) => { export default ({ regl }) => {
const [numRows, numColumns] = [config.numColumns, config.numColumns]; const size = 80; // The maximum dimension of the glyph grid
const commonUniforms = { const commonUniforms = {
...extractEntries(config, ["animationSpeed", "glyphHeightToWidth", "glyphSequenceLength", "glyphTextureGridSize"]), glyphSequenceLength: 57,
numColumns, glyphTextureGridSize: [8, 8],
numRows, numColumns: size,
numRows: size,
}; };
const computeDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); const computeDoubleBuffer = makeDoubleBuffer(regl, {
const rainPassCompute = loadText("shaders/glsl/rainPass.compute.frag.glsl"); width: size,
const computeUniforms = { height: size,
...commonUniforms, wrapT: "clamp",
...extractEntries(config, ["fallSpeed", "raindropLength"]), type: "half float",
...extractEntries(config, ["cycleSpeed", "cycleFrameSkip"]), });
};
const compute = regl({ const compute = regl({
frag: regl.prop("frag"), frag: `
precision highp float;
#define PI 3.14159265359
#define SQRT_2 1.4142135623730951
#define SQRT_5 2.23606797749979
uniform sampler2D previousComputeState;
uniform float numColumns, numRows;
uniform float time, tick;
uniform float fallSpeed, cycleSpeed;
uniform float glyphSequenceLength;
uniform float raindropLength;
// Helper functions for generating randomness, borrowed from elsewhere
highp float randomFloat( const in vec2 uv ) {
const highp float a = 12.9898, b = 78.233, c = 43758.5453;
highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
return fract(sin(sn) * c);
}
float wobble(float x) {
return x + 0.3 * sin(SQRT_2 * x) + 0.2 * sin(SQRT_5 * x);
}
float getRainBrightness(float simTime, vec2 glyphPos) {
float columnTimeOffset = randomFloat(vec2(glyphPos.x, 0.)) * 1000.;
float columnSpeedOffset = randomFloat(vec2(glyphPos.x + 0.1, 0.)) * 0.5 + 0.5;
float columnTime = columnTimeOffset + simTime * fallSpeed * columnSpeedOffset;
float rainTime = (glyphPos.y * 0.01 + columnTime) / raindropLength;
rainTime = wobble(rainTime);
return 1.0 - fract(rainTime);
}
vec2 computeRaindrop(float simTime, vec2 glyphPos) {
float brightness = getRainBrightness(simTime, glyphPos);
float brightnessBelow = getRainBrightness(simTime, glyphPos + vec2(0., -1.));
bool cursor = brightness > brightnessBelow;
return vec2(brightness, cursor);
}
vec2 computeSymbol(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous) {
float previousSymbol = previous.r;
float previousAge = previous.g;
bool resetGlyph = isFirstFrame;
if (resetGlyph) {
previousAge = randomFloat(screenPos + 0.5);
previousSymbol = floor(glyphSequenceLength * randomFloat(screenPos));
}
float age = previousAge;
float symbol = previousSymbol;
if (mod(tick, 1.0) == 0.) {
age += cycleSpeed;
if (age >= 1.) {
symbol = floor(glyphSequenceLength * randomFloat(screenPos + simTime));
age = fract(age);
}
}
return vec2(symbol, age);
}
void main() {
vec2 glyphPos = gl_FragCoord.xy;
vec2 screenPos = glyphPos / vec2(numColumns, numRows);
vec2 raindrop = computeRaindrop(time, glyphPos);
bool isFirstFrame = tick <= 1.;
vec4 previous = texture2D( previousComputeState, screenPos );
vec4 previousSymbol = vec4(previous.ba, 0.0, 0.0);
vec2 symbol = computeSymbol(time, isFirstFrame, glyphPos, screenPos, previousSymbol);
gl_FragColor = vec4(raindrop, symbol);
}
`,
uniforms: { uniforms: {
...computeUniforms, ...commonUniforms,
cycleSpeed: 0.03, // The speed glyphs change
fallSpeed: 0.3, // The speed the raindrops progress downwards
raindropLength: 0.75, // Adjusts the frequency of raindrops (and their length) in a column
previousComputeState: computeDoubleBuffer.back, previousComputeState: computeDoubleBuffer.back,
}, },
@@ -59,27 +124,8 @@ export default ({ regl, config }) => {
); );
// We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen // We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen
const glyphMSDF = loadImage(regl, config.glyphMSDFURL); const glyphMSDF = loadImage(regl, "assets/matrixcode_msdf.png");
const rainPassVert = loadText("shaders/glsl/rainPass.vert.glsl"); const output = makePassFBO(regl);
const rainPassFrag = loadText("shaders/glsl/rainPass.frag.glsl");
const output = makePassFBO(regl, config.useHalfFloat);
const renderUniforms = {
...commonUniforms,
...extractEntries(config, [
// vertex
"forwardSpeed",
"glyphVerticalSpacing",
// fragment
"baseBrightness",
"baseContrast",
"glintBrightness",
"glintContrast",
"brightnessThreshold",
"brightnessOverride",
"isolateCursor",
"glyphEdgeCrop",
]),
};
const render = regl({ const render = regl({
blend: { blend: {
enable: true, enable: true,
@@ -88,18 +134,100 @@ export default ({ regl, config }) => {
dst: "one", dst: "one",
}, },
}, },
vert: regl.prop("vert"), vert: `
frag: regl.prop("frag"), precision lowp float;
attribute vec2 aPosition, aCorner;
uniform vec2 screenSize;
varying vec2 vUV;
void main() {
vUV = aPosition + aCorner;
gl_Position = vec4((aPosition + aCorner - 0.5) * 2.0 * screenSize, 0.0, 1.0);
}
`,
frag: `
#define PI 3.14159265359
#ifdef GL_OES_standard_derivatives
#extension GL_OES_standard_derivatives: enable
#endif
precision lowp float;
uniform sampler2D computeState;
uniform float numColumns, numRows;
uniform sampler2D glyphMSDF;
uniform float msdfPxRange;
uniform vec2 glyphMSDFSize;
uniform float glyphSequenceLength;
uniform vec2 glyphTextureGridSize;
varying vec2 vUV;
float median3(vec3 i) {
return max(min(i.r, i.g), min(max(i.r, i.g), i.b));
}
float modI(float a, float b) {
float m = a - floor((a + 0.5) / b) * b;
return floor(m + 0.5);
}
vec3 getBrightness(vec2 raindrop, vec2 uv) {
float base = raindrop.r;
bool isCursor = bool(raindrop.g);
float glint = base;
base = base * 1.1 - 0.5;
glint = glint * 2.5 - 1.5;
return vec3(
(isCursor ? vec2(0.0, 1.0) : vec2(1.0, 0.0)) * base,
glint
);
}
vec2 getSymbolUV(float index) {
float symbolX = modI(index, glyphTextureGridSize.x);
float symbolY = (index - symbolX) / glyphTextureGridSize.x;
symbolY = glyphTextureGridSize.y - symbolY - 1.;
return vec2(symbolX, symbolY);
}
vec2 getSymbol(vec2 uv, float index) {
// resolve UV to cropped position of glyph in MSDF texture
uv = fract(uv * vec2(numColumns, numRows));
uv = (uv + getSymbolUV(index)) / glyphTextureGridSize;
// MSDF: calculate brightness of fragment based on distance to shape
vec2 symbol;
{
vec2 unitRange = vec2(msdfPxRange) / glyphMSDFSize;
vec2 screenTexSize = vec2(1.0) / fwidth(uv);
float screenPxRange = max(0.5 * dot(unitRange, screenTexSize), 1.0);
float signedDistance = median3(texture2D(glyphMSDF, uv).rgb);
float screenPxDistance = screenPxRange * (signedDistance - 0.5);
symbol.r = clamp(screenPxDistance + 0.5, 0.0, 1.0);
}
return symbol;
}
void main() {
vec4 data = texture2D(computeState, vUV);
vec3 brightness = getBrightness(data.rg, vUV);
vec2 symbol = getSymbol(vUV, data.b);
gl_FragColor = vec4(brightness.rg * symbol.r, brightness.b * symbol.g, 0.);
}
`,
uniforms: { uniforms: {
...renderUniforms, ...commonUniforms,
computeState: computeDoubleBuffer.front, computeState: computeDoubleBuffer.front,
glyphMSDF: glyphMSDF.texture, glyphMSDF: glyphMSDF.texture,
msdfPxRange: 4.0, msdfPxRange: 4.0,
glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()], glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()],
screenSize: regl.prop("screenSize"), screenSize: regl.prop("screenSize"),
}, },
@@ -118,29 +246,20 @@ export default ({ regl, config }) => {
{ {
primary: output, primary: output,
}, },
Promise.all([ Promise.all([glyphMSDF.loaded]),
glyphMSDF.loaded,
rainPassCompute.loaded,
rainPassVert.loaded,
rainPassFrag.loaded,
]),
(w, h) => { (w, h) => {
output.resize(w, h); output.resize(w, h);
const aspectRatio = w / h; const aspectRatio = w / h;
[screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; [screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
}, },
(shouldRender) => { () => {
compute({ frag: rainPassCompute.text() }); compute();
regl.clear({
if (shouldRender) { depth: 1,
regl.clear({ color: [0, 0, 0, 1],
depth: 1, framebuffer: output,
color: [0, 0, 0, 1], });
framebuffer: output, render({ screenSize });
});
render({ screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() });
}
} }
); );
}; };

View File

@@ -27,28 +27,13 @@ const makeDoubleBuffer = (regl, props) => {
const isPowerOfTwo = (x) => Math.log2(x) % 1 == 0; const isPowerOfTwo = (x) => Math.log2(x) % 1 == 0;
const loadImage = (regl, url, mipmap) => { const loadImage = (regl, url) => {
let texture = regl.texture([[0]]); let texture = regl.texture([[0]]);
let loaded = false; let loaded = false;
return { return {
texture: () => { texture: () => texture,
if (!loaded && url != null) { width: () => (loaded ? texture.width : 1),
console.warn(`texture still loading: ${url}`); height: () => (loaded ? texture.height : 1),
}
return texture;
},
width: () => {
if (!loaded && url != null) {
console.warn(`texture still loading: ${url}`);
}
return loaded ? texture.width : 1;
},
height: () => {
if (!loaded && url != null) {
console.warn(`texture still loading: ${url}`);
}
return loaded ? texture.height : 1;
},
loaded: (async () => { loaded: (async () => {
if (url != null) { if (url != null) {
const data = new Image(); const data = new Image();
@@ -56,16 +41,10 @@ const loadImage = (regl, url, mipmap) => {
data.src = url; data.src = url;
await data.decode(); await data.decode();
loaded = true; loaded = true;
if (mipmap) {
if (!isPowerOfTwo(data.width) || !isPowerOfTwo(data.height)) {
console.warn(`Can't mipmap a non-power-of-two image: ${url}`);
}
mipmap = false;
}
texture = regl.texture({ texture = regl.texture({
data, data,
mag: "linear", mag: "linear",
min: mipmap ? "mipmap" : "linear", min: "linear",
flipY: true, flipY: true,
}); });
} }
@@ -77,12 +56,7 @@ const loadText = (url) => {
let text = ""; let text = "";
let loaded = false; let loaded = false;
return { return {
text: () => { text: () => text,
if (!loaded) {
console.warn(`text still loading: ${url}`);
}
return text;
},
loaded: (async () => { loaded: (async () => {
if (url != null) { if (url != null) {
text = await (await fetch(url)).text(); text = await (await fetch(url)).text();

View File

@@ -1,17 +0,0 @@
precision mediump float;
uniform float width, height;
uniform sampler2D tex;
uniform vec2 direction;
varying vec2 vUV;
void main() {
vec2 size = width > height ? vec2(width / height, 1.) : vec2(1., height / width);
gl_FragColor =
texture2D(tex, vUV) * 0.442 +
(
texture2D(tex, vUV + direction / max(width, height) * size) +
texture2D(tex, vUV - direction / max(width, height) * size)
) * 0.279;
}

View File

@@ -1,20 +0,0 @@
precision mediump float;
uniform sampler2D pyr_0;
uniform sampler2D pyr_1;
uniform sampler2D pyr_2;
uniform sampler2D pyr_3;
uniform sampler2D pyr_4;
uniform float bloomStrength;
varying vec2 vUV;
void main() {
vec4 total = vec4(0.);
total += texture2D(pyr_0, vUV) * 0.96549;
total += texture2D(pyr_1, vUV) * 0.92832;
total += texture2D(pyr_2, vUV) * 0.88790;
total += texture2D(pyr_3, vUV) * 0.84343;
total += texture2D(pyr_4, vUV) * 0.79370;
gl_FragColor = total * bloomStrength;
}

View File

@@ -1,14 +0,0 @@
precision mediump float;
uniform sampler2D tex;
uniform float highPassThreshold;
varying vec2 vUV;
void main() {
vec4 color = texture2D(tex, vUV);
if (color.r < highPassThreshold) color.r = 0.0;
if (color.g < highPassThreshold) color.g = 0.0;
if (color.b < highPassThreshold) color.b = 0.0;
gl_FragColor = color;
}

View File

@@ -1,39 +0,0 @@
precision mediump float;
#define PI 3.14159265359
uniform sampler2D tex;
uniform sampler2D bloomTex;
uniform sampler2D paletteTex;
uniform float ditherMagnitude;
uniform float time;
uniform vec3 backgroundColor, cursorColor, glintColor;
uniform float cursorIntensity, glintIntensity;
varying vec2 vUV;
highp float rand( const in vec2 uv, const in float t ) {
const highp float a = 12.9898, b = 78.233, c = 43758.5453;
highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
return fract(sin(sn) * c + t);
}
vec4 getBrightness(vec2 uv) {
vec4 primary = texture2D(tex, uv);
vec4 bloom = texture2D(bloomTex, uv);
return primary + bloom;
}
void main() {
vec4 brightness = getBrightness(vUV);
// Dither: subtract a random value from the brightness
brightness -= rand( gl_FragCoord.xy, time ) * ditherMagnitude / 3.0;
// Map the brightness to a position in the palette texture
gl_FragColor = vec4(
texture2D( paletteTex, vec2(brightness.r, 0.0)).rgb
+ min(cursorColor * cursorIntensity * brightness.g, vec3(1.0))
+ min(glintColor * glintIntensity * brightness.b, vec3(1.0))
+ backgroundColor,
1.0
);
}

View File

@@ -1,78 +0,0 @@
precision highp float;
#define PI 3.14159265359
#define SQRT_2 1.4142135623730951
#define SQRT_5 2.23606797749979
uniform sampler2D previousComputeState;
uniform float numColumns, numRows;
uniform float time, tick, cycleFrameSkip;
uniform float animationSpeed, fallSpeed, cycleSpeed;
uniform float glyphSequenceLength;
uniform float raindropLength;
// Helper functions for generating randomness, borrowed from elsewhere
highp float randomFloat( const in vec2 uv ) {
const highp float a = 12.9898, b = 78.233, c = 43758.5453;
highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
return fract(sin(sn) * c);
}
float wobble(float x) {
return x + 0.3 * sin(SQRT_2 * x) + 0.2 * sin(SQRT_5 * x);
}
float getRainBrightness(float simTime, vec2 glyphPos) {
float columnTimeOffset = randomFloat(vec2(glyphPos.x, 0.)) * 1000.;
float columnSpeedOffset = randomFloat(vec2(glyphPos.x + 0.1, 0.)) * 0.5 + 0.5;
float columnTime = columnTimeOffset + simTime * fallSpeed * columnSpeedOffset;
float rainTime = (glyphPos.y * 0.01 + columnTime) / raindropLength;
rainTime = wobble(rainTime);
return 1.0 - fract(rainTime);
}
vec2 computeRaindrop(float simTime, vec2 glyphPos) {
float brightness = getRainBrightness(simTime, glyphPos);
float brightnessBelow = getRainBrightness(simTime, glyphPos + vec2(0., -1.));
bool cursor = brightness > brightnessBelow;
return vec2(brightness, cursor);
}
vec2 computeSymbol(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous) {
float previousSymbol = previous.r;
float previousAge = previous.g;
bool resetGlyph = isFirstFrame;
if (resetGlyph) {
previousAge = randomFloat(screenPos + 0.5);
previousSymbol = floor(glyphSequenceLength * randomFloat(screenPos));
}
float cycleSpeed = animationSpeed * cycleSpeed;
float age = previousAge;
float symbol = previousSymbol;
if (mod(tick, cycleFrameSkip) == 0.) {
age += cycleSpeed * cycleFrameSkip;
if (age >= 1.) {
symbol = floor(glyphSequenceLength * randomFloat(screenPos + simTime));
age = fract(age);
}
}
return vec2(symbol, age);
}
void main() {
float simTime = time * animationSpeed;
vec2 glyphPos = gl_FragCoord.xy;
vec2 screenPos = glyphPos / vec2(numColumns, numRows);
vec2 raindrop = computeRaindrop(simTime, glyphPos);
bool isFirstFrame = tick <= 1.;
vec4 previous = texture2D( previousComputeState, screenPos );
vec4 previousSymbol = vec4(previous.ba, 0.0, 0.0);
vec2 symbol = computeSymbol(simTime, isFirstFrame, glyphPos, screenPos, previousSymbol);
gl_FragColor = vec4(raindrop, symbol);
}

View File

@@ -1,96 +0,0 @@
#define PI 3.14159265359
#ifdef GL_OES_standard_derivatives
#extension GL_OES_standard_derivatives: enable
#endif
precision lowp float;
uniform sampler2D computeState;
uniform float numColumns, numRows;
uniform sampler2D glyphMSDF;
uniform float msdfPxRange;
uniform vec2 glyphMSDFSize;
uniform float glyphHeightToWidth, glyphSequenceLength, glyphEdgeCrop;
uniform float baseContrast, baseBrightness, glintContrast, glintBrightness;
uniform float brightnessOverride, brightnessThreshold;
uniform vec2 glyphTextureGridSize;
uniform bool isolateCursor;
varying vec2 vUV;
float median3(vec3 i) {
return max(min(i.r, i.g), min(max(i.r, i.g), i.b));
}
float modI(float a, float b) {
float m = a - floor((a + 0.5) / b) * b;
return floor(m + 0.5);
}
vec2 getUV(vec2 uv) {
uv.y /= glyphHeightToWidth;
return uv;
}
vec3 getBrightness(vec2 raindrop, vec2 uv) {
float base = raindrop.r;
bool isCursor = bool(raindrop.g) && isolateCursor;
float glint = base;
vec2 textureUV = fract(uv * vec2(numColumns, numRows));
base = base * baseContrast + baseBrightness;
glint = glint * glintContrast + glintBrightness;
// Modes that don't fade glyphs set their actual brightness here
if (brightnessOverride > 0. && base > brightnessThreshold && !isCursor) {
base = brightnessOverride;
}
return vec3(
(isCursor ? vec2(0.0, 1.0) : vec2(1.0, 0.0)) * base,
glint
);
}
vec2 getSymbolUV(float index) {
float symbolX = modI(index, glyphTextureGridSize.x);
float symbolY = (index - symbolX) / glyphTextureGridSize.x;
symbolY = glyphTextureGridSize.y - symbolY - 1.;
return vec2(symbolX, symbolY);
}
vec2 getSymbol(vec2 uv, float index) {
// resolve UV to cropped position of glyph in MSDF texture
uv = fract(uv * vec2(numColumns, numRows));
uv -= 0.5;
uv *= clamp(1. - glyphEdgeCrop, 0., 1.);
uv += 0.5;
uv = (uv + getSymbolUV(index)) / glyphTextureGridSize;
// MSDF: calculate brightness of fragment based on distance to shape
vec2 symbol;
{
vec2 unitRange = vec2(msdfPxRange) / glyphMSDFSize;
vec2 screenTexSize = vec2(1.0) / fwidth(uv);
float screenPxRange = max(0.5 * dot(unitRange, screenTexSize), 1.0);
float signedDistance = median3(texture2D(glyphMSDF, uv).rgb);
float screenPxDistance = screenPxRange * (signedDistance - 0.5);
symbol.r = clamp(screenPxDistance + 0.5, 0.0, 1.0);
}
return symbol;
}
void main() {
vec2 uv = getUV(vUV);
// Unpack the values from the data textures
vec4 data = texture2D(computeState, uv);
vec3 brightness = getBrightness(data.rg, uv);
vec2 symbol = getSymbol(uv, data.b);
gl_FragColor = vec4(brightness.rg * symbol.r, brightness.b * symbol.g, 0.);
}

View File

@@ -1,15 +0,0 @@
#define PI 3.14159265359
precision lowp float;
attribute vec2 aPosition, aCorner;
uniform float glyphVerticalSpacing;
uniform vec2 screenSize;
uniform float time, animationSpeed;
varying vec2 vUV;
void main() {
vUV = aPosition + aCorner;
vec2 position = (aPosition * vec2(1., glyphVerticalSpacing) + aCorner);
vec4 pos = vec4((position - 0.5) * 2.0, 0.0, 1.0);
pos.xy *= screenSize;
gl_Position = pos;
}