From d1f00e7e42d3ea5d4479dc6bd1188b477b2a14bb Mon Sep 17 00:00:00 2001 From: Rezmason Date: Wed, 23 Aug 2023 11:28:29 -0700 Subject: [PATCH] Crushed down the config, removed a ton more inessential stuff --- TODO.txt | 5 +- index.html | 78 +------ js/bloomPass.js | 109 +++++---- js/colorToRGB.js | 12 - js/main.js | 79 +------ js/palettePass.js | 123 ++++------ js/rainPass.js | 273 ++++++++++++++++------ js/utils.js | 38 +-- shaders/glsl/bloomPass.blur.frag.glsl | 17 -- shaders/glsl/bloomPass.combine.frag.glsl | 20 -- shaders/glsl/bloomPass.highPass.frag.glsl | 14 -- shaders/glsl/palettePass.frag.glsl | 39 ---- shaders/glsl/rainPass.compute.frag.glsl | 78 ------- shaders/glsl/rainPass.frag.glsl | 96 -------- shaders/glsl/rainPass.vert.glsl | 15 -- 15 files changed, 323 insertions(+), 673 deletions(-) delete mode 100644 js/colorToRGB.js delete mode 100644 shaders/glsl/bloomPass.blur.frag.glsl delete mode 100644 shaders/glsl/bloomPass.combine.frag.glsl delete mode 100644 shaders/glsl/bloomPass.highPass.frag.glsl delete mode 100644 shaders/glsl/palettePass.frag.glsl delete mode 100644 shaders/glsl/rainPass.compute.frag.glsl delete mode 100644 shaders/glsl/rainPass.frag.glsl delete mode 100644 shaders/glsl/rainPass.vert.glsl diff --git a/TODO.txt b/TODO.txt index ddd98e4..11cc3a8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,9 +1,6 @@ TODO: Simplify! - Pare down config - Get rid of everything inessential - Remove features - Remove subsystems Get as much into one file as you possibly can Remove regl + Record WebGL debug calls diff --git a/index.html b/index.html index 90150eb..2f221c2 100644 --- a/index.html +++ b/index.html @@ -27,83 +27,7 @@ width: 100vw; 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; - } - - - - + diff --git a/js/bloomPass.js b/js/bloomPass.js index 11e2710..427a04b 100644 --- a/js/bloomPass.js +++ b/js/bloomPass.js @@ -1,12 +1,7 @@ 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; -// 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) => Array(height) .fill() @@ -15,28 +10,33 @@ const makePyramid = (regl, height, halfFloat) => const resizePyramid = (pyramid, vw, vh, scale) => pyramid.forEach((fbo, index) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index))); -export default ({ regl, config }, inputs) => { - const { bloomStrength, bloomSize, highPassThreshold } = config; - const enabled = bloomSize > 0 && bloomStrength > 0; +export default ({ regl }, inputs) => { + const bloomStrength = 0.7; // The intensity of the bloom + 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 - if (!enabled) { - return makePass({ - primary: inputs.primary, - bloom: makePassFBO(regl), - }); - } + const highPassPyramid = makePyramid(regl, pyramidHeight); + const hBlurPyramid = makePyramid(regl, pyramidHeight); + const vBlurPyramid = makePyramid(regl, pyramidHeight); + const output = 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({ - 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: { highPassThreshold, tex: regl.prop("tex"), @@ -44,14 +44,26 @@ export default ({ regl, config }, inputs) => { 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({ - 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: { tex: regl.prop("tex"), 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 - const combineFrag = loadText("shaders/glsl/bloomPass.combine.frag.glsl"); 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: { bloomStrength, ...Object.fromEntries(vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])), @@ -77,7 +104,7 @@ export default ({ regl, config }, inputs) => { primary: inputs.primary, bloom: output, }, - Promise.all([highPassFrag.loaded, blurFrag.loaded]), + null, (w, h) => { // The blur pyramids can be lower resolution than the screen. resizePyramid(highPassPyramid, w, h, bloomSize); @@ -85,21 +112,17 @@ export default ({ regl, config }, inputs) => { resizePyramid(vBlurPyramid, w, h, bloomSize); output.resize(w, h); }, - (shouldRender) => { - if (!shouldRender) { - return; - } - + () => { for (let i = 0; i < pyramidHeight; i++) { const highPassFBO = highPassPyramid[i]; const hBlurFBO = hBlurPyramid[i]; const vBlurFBO = vBlurPyramid[i]; - highPass({ fbo: highPassFBO, frag: highPassFrag.text(), tex: i === 0 ? inputs.primary : highPassPyramid[i - 1] }); - blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] }); - blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] }); + highPass({ fbo: highPassFBO, tex: i === 0 ? inputs.primary : highPassPyramid[i - 1] }); + blur({ fbo: hBlurFBO, tex: highPassFBO, direction: [1, 0] }); + blur({ fbo: vBlurFBO, tex: hBlurFBO, direction: [0, 1] }); } - combine({ frag: combineFrag.text() }); + combine(); } ); }; diff --git a/js/colorToRGB.js b/js/colorToRGB.js deleted file mode 100644 index 54f3f16..0000000 --- a/js/colorToRGB.js +++ /dev/null @@ -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)]; -}; diff --git a/js/main.js b/js/main.js index e9ae70f..ad82f7c 100644 --- a/js/main.js +++ b/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"); document.body.appendChild(canvas); document.addEventListener("touchmove", (e) => e.preventDefault(), { @@ -70,8 +26,8 @@ const init = async () => { const resize = () => { const devicePixelRatio = window.devicePixelRatio ?? 1; - canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * config.resolution); - canvas.height = Math.ceil(canvas.clientHeight * devicePixelRatio * config.resolution); + canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * 0.75); + canvas.height = Math.ceil(canvas.clientHeight * devicePixelRatio * 0.75); }; window.onresize = resize; if (document.fullscreenEnabled || document.webkitFullscreenEnabled) { @@ -97,37 +53,14 @@ const init = async () => { // All this takes place in a full screen quad. const fullScreenQuad = makeFullScreenQuad(regl); - const context = { regl, config }; - const pipeline = makePipeline(context, [makeRain, makeBloomPass, makePalettePass]); + const pipeline = makePipeline({ regl }, [makeRain, makeBloomPass, makePalettePass]); const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary }; const drawToScreen = regl({ uniforms: screenUniforms }); await Promise.all(pipeline.map((step) => step.ready)); - const targetFrameTimeMilliseconds = 1000 / config.fps; - let last = NaN; - const render = ({ viewportWidth, viewportHeight }) => { - if (config.once) { - tick.cancel(); - } - 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) { dimensions.width = viewportWidth; dimensions.height = viewportHeight; @@ -137,17 +70,17 @@ const init = async () => { } fullScreenQuad(() => { for (const step of pipeline) { - step.execute(shouldRender); + step.execute(); } drawToScreen(); }); }; - render({viewportWidth: 1, viewportHeight: 1}); + render({ viewportWidth: 1, viewportHeight: 1 }); const tick = regl.frame(render); }; document.body.onload = () => { init(); -} +}; diff --git a/js/palettePass.js b/js/palettePass.js index fa8d128..24aa5f1 100644 --- a/js/palettePass.js +++ b/js/palettePass.js @@ -1,79 +1,54 @@ -import colorToRGB from "./colorToRGB.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"); +import { make1DTexture, makePassFBO, makePass } from "./utils.js"; +export default ({ regl }, inputs) => { + const output = makePassFBO(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: { - backgroundColor: colorToRGB(backgroundColor), - cursorColor: colorToRGB(cursorColor), - glintColor: colorToRGB(glintColor), - cursorIntensity, - glintIntensity, - ditherMagnitude, tex: inputs.primary, 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, }); @@ -82,12 +57,8 @@ export default ({ regl, config }, inputs) => { { primary: output, }, - palettePassFrag.loaded, + null, (w, h) => output.resize(w, h), - (shouldRender) => { - if (shouldRender) { - render({ frag: palettePassFrag.text() }); - } - } + () => render() ); }; diff --git a/js/rainPass.js b/js/rainPass.js index 40e9c7c..f679129 100644 --- a/js/rainPass.js +++ b/js/rainPass.js @@ -1,21 +1,4 @@ -import { loadImage, loadText, 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", - }); +import { loadImage, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js"; const numVerticesPerQuad = 2 * 3; const tlVert = [0, 0]; @@ -24,26 +7,108 @@ const blVert = [1, 0]; const brVert = [1, 1]; const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert]; -export default ({ regl, config }) => { - const [numRows, numColumns] = [config.numColumns, config.numColumns]; +export default ({ regl }) => { + const size = 80; // The maximum dimension of the glyph grid const commonUniforms = { - ...extractEntries(config, ["animationSpeed", "glyphHeightToWidth", "glyphSequenceLength", "glyphTextureGridSize"]), - numColumns, - numRows, + glyphSequenceLength: 57, + glyphTextureGridSize: [8, 8], + numColumns: size, + numRows: size, }; - const computeDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); - const rainPassCompute = loadText("shaders/glsl/rainPass.compute.frag.glsl"); - const computeUniforms = { - ...commonUniforms, - ...extractEntries(config, ["fallSpeed", "raindropLength"]), - ...extractEntries(config, ["cycleSpeed", "cycleFrameSkip"]), - }; + const computeDoubleBuffer = makeDoubleBuffer(regl, { + width: size, + height: size, + wrapT: "clamp", + type: "half float", + }); + 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: { - ...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, }, @@ -59,27 +124,8 @@ export default ({ regl, config }) => { ); // We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen - const glyphMSDF = loadImage(regl, config.glyphMSDFURL); - const rainPassVert = loadText("shaders/glsl/rainPass.vert.glsl"); - 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 glyphMSDF = loadImage(regl, "assets/matrixcode_msdf.png"); + const output = makePassFBO(regl); const render = regl({ blend: { enable: true, @@ -88,18 +134,100 @@ export default ({ regl, config }) => { dst: "one", }, }, - vert: regl.prop("vert"), - frag: regl.prop("frag"), + vert: ` + 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: { - ...renderUniforms, - + ...commonUniforms, computeState: computeDoubleBuffer.front, glyphMSDF: glyphMSDF.texture, - msdfPxRange: 4.0, glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()], - screenSize: regl.prop("screenSize"), }, @@ -118,29 +246,20 @@ export default ({ regl, config }) => { { primary: output, }, - Promise.all([ - glyphMSDF.loaded, - rainPassCompute.loaded, - rainPassVert.loaded, - rainPassFrag.loaded, - ]), + Promise.all([glyphMSDF.loaded]), (w, h) => { output.resize(w, h); const aspectRatio = w / h; [screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; }, - (shouldRender) => { - compute({ frag: rainPassCompute.text() }); - - if (shouldRender) { - regl.clear({ - depth: 1, - color: [0, 0, 0, 1], - framebuffer: output, - }); - - render({ screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() }); - } + () => { + compute(); + regl.clear({ + depth: 1, + color: [0, 0, 0, 1], + framebuffer: output, + }); + render({ screenSize }); } ); }; diff --git a/js/utils.js b/js/utils.js index efbb284..7cf2387 100644 --- a/js/utils.js +++ b/js/utils.js @@ -27,28 +27,13 @@ const makeDoubleBuffer = (regl, props) => { const isPowerOfTwo = (x) => Math.log2(x) % 1 == 0; -const loadImage = (regl, url, mipmap) => { +const loadImage = (regl, url) => { let texture = regl.texture([[0]]); let loaded = false; return { - texture: () => { - if (!loaded && url != null) { - console.warn(`texture still loading: ${url}`); - } - 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; - }, + texture: () => texture, + width: () => (loaded ? texture.width : 1), + height: () => (loaded ? texture.height : 1), loaded: (async () => { if (url != null) { const data = new Image(); @@ -56,16 +41,10 @@ const loadImage = (regl, url, mipmap) => { data.src = url; await data.decode(); 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({ data, mag: "linear", - min: mipmap ? "mipmap" : "linear", + min: "linear", flipY: true, }); } @@ -77,12 +56,7 @@ const loadText = (url) => { let text = ""; let loaded = false; return { - text: () => { - if (!loaded) { - console.warn(`text still loading: ${url}`); - } - return text; - }, + text: () => text, loaded: (async () => { if (url != null) { text = await (await fetch(url)).text(); diff --git a/shaders/glsl/bloomPass.blur.frag.glsl b/shaders/glsl/bloomPass.blur.frag.glsl deleted file mode 100644 index f094050..0000000 --- a/shaders/glsl/bloomPass.blur.frag.glsl +++ /dev/null @@ -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; -} diff --git a/shaders/glsl/bloomPass.combine.frag.glsl b/shaders/glsl/bloomPass.combine.frag.glsl deleted file mode 100644 index 431c0ce..0000000 --- a/shaders/glsl/bloomPass.combine.frag.glsl +++ /dev/null @@ -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; -} diff --git a/shaders/glsl/bloomPass.highPass.frag.glsl b/shaders/glsl/bloomPass.highPass.frag.glsl deleted file mode 100644 index 9a24637..0000000 --- a/shaders/glsl/bloomPass.highPass.frag.glsl +++ /dev/null @@ -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; -} diff --git a/shaders/glsl/palettePass.frag.glsl b/shaders/glsl/palettePass.frag.glsl deleted file mode 100644 index 6fda862..0000000 --- a/shaders/glsl/palettePass.frag.glsl +++ /dev/null @@ -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 - ); -} diff --git a/shaders/glsl/rainPass.compute.frag.glsl b/shaders/glsl/rainPass.compute.frag.glsl deleted file mode 100644 index 9d0550c..0000000 --- a/shaders/glsl/rainPass.compute.frag.glsl +++ /dev/null @@ -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); -} diff --git a/shaders/glsl/rainPass.frag.glsl b/shaders/glsl/rainPass.frag.glsl deleted file mode 100644 index 1231694..0000000 --- a/shaders/glsl/rainPass.frag.glsl +++ /dev/null @@ -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.); -} diff --git a/shaders/glsl/rainPass.vert.glsl b/shaders/glsl/rainPass.vert.glsl deleted file mode 100644 index e158cf9..0000000 --- a/shaders/glsl/rainPass.vert.glsl +++ /dev/null @@ -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; -}