mirror of
https://github.com/Rezmason/matrix.git
synced 2026-04-18 22:29:28 -07:00
Crushed down the config, removed a ton more inessential stuff
This commit is contained in:
5
TODO.txt
5
TODO.txt
@@ -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
|
||||||
|
|||||||
78
index.html
78
index.html
@@ -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>
|
||||||
|
|||||||
109
js/bloomPass.js
109
js/bloomPass.js
@@ -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();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)];
|
|
||||||
};
|
|
||||||
79
js/main.js
79
js/main.js
@@ -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();
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
273
js/rainPass.js
273
js/rainPass.js
@@ -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() });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
38
js/utils.js
38
js/utils.js
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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.);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user