From 4c6e6fd6621df3b4c38cd9684a9722cc4e21ce5a Mon Sep 17 00:00:00 2001 From: Rezmason Date: Wed, 14 Sep 2022 22:57:39 -0700 Subject: [PATCH] Removed sun shower. Thunder and ripples are now handled by a third compute shader. --- TODO.txt | 13 ++-- js/config.js | 6 +- js/regl/main.js | 2 +- js/regl/rainPass.js | 71 +++++++++--------- shaders/glsl/rainPass.effect.frag.glsl | 99 ++++++++++++++++++++++++++ shaders/glsl/rainPass.frag.glsl | 9 ++- shaders/glsl/rainPass.shine.frag.glsl | 77 ++------------------ shaders/glsl/rainPass.symbol.frag.glsl | 2 +- shaders/glsl/rainPass.vert.glsl | 5 +- 9 files changed, 163 insertions(+), 121 deletions(-) create mode 100644 shaders/glsl/rainPass.effect.frag.glsl diff --git a/TODO.txt b/TODO.txt index ce37fb3..5eb0f63 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,15 +1,20 @@ TODO: +Test all the versions! + Reformulate the basis https://buf.com/films/the-matrix-resurrections Rain pass frag's output should match its debug view output + Move brightness and contrast logic to the later passes The channels get individually blurred Then the other passes use the red, green and blue channels for separate things + red: cursor + green: long tail + blue: short tail Tune the colors - Maybe glow can be an SDF-derived effect instead, look into it - - Migrate the rest of the project over - Migrate WebGPU + New config properties + Find a way to support the old stuff? + Migrate to WebGPU Audio system Toggle (or number representing frequency) diff --git a/js/config.js b/js/config.js index 18c5b60..d7bbacf 100644 --- a/js/config.js +++ b/js/config.js @@ -75,7 +75,6 @@ const defaults = { 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 - hasSun: false, // Makes the glyphs more radiant. Admittedly not very technical. hasThunder: false, // An effect that adds dramatic lightning flashes isPolar: false, // Whether the glyphs arc across the screen or sit in a standard grid rippleTypeName: null, // The variety of the ripple effect @@ -154,10 +153,10 @@ const versions = { bloomStrength: 1, highPassThreshold: 0, cycleSpeed: 0.05, - baseBrightness: -0.1, + baseBrightness: -1.3, + baseContrast: 2, brightnessDecay: 0.05, fallSpeed: 0.04, - hasSun: true, isPolar: true, rippleTypeName: "circle", rippleSpeed: 0.1, @@ -211,7 +210,6 @@ const versions = { fallSpeed: 0.1, cycleStyleName: "cycleRandomly", highPassThreshold: 0.0, - hasSun: true, paletteEntries: [ { hsl: [0.6, 1.0, 0.05], at: 0.0 }, { hsl: [0.6, 0.8, 0.1], at: 0.1 }, diff --git a/js/regl/main.js b/js/regl/main.js index 635fe6c..1e8da12 100644 --- a/js/regl/main.js +++ b/js/regl/main.js @@ -34,7 +34,7 @@ const loadJS = (src) => }); export default async (canvas, config) => { - await Promise.all([loadJS("lib/regl.min.js"), loadJS("lib/gl-matrix.js")]); + await Promise.all([loadJS("lib/regl.js"), loadJS("lib/gl-matrix.js")]); const resize = () => { canvas.width = Math.ceil(canvas.clientWidth * config.resolution); diff --git a/js/regl/rainPass.js b/js/regl/rainPass.js index 64b3870..ba6c9b4 100644 --- a/js/regl/rainPass.js +++ b/js/regl/rainPass.js @@ -12,6 +12,21 @@ const cycleStyles = { cycleRandomly: 1, }; +// 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 tlVert = [0, 0]; const trVert = [0, 1]; @@ -46,36 +61,11 @@ export default ({ regl, config, lkg }) => { showDebugView, }; - // These two framebuffers are used to compute the raining code. - // they take turns being the source and destination of the "compute" shader. - // The half float data type is crucial! It lets us store almost any real number, - // whereas the default type limits us to integers between 0 and 255. - - // This double buffer is smaller than the screen, because its pixels correspond - // with glyphs in the final image, and the glyphs are much larger than a pixel. - const shineDoubleBuffer = makeDoubleBuffer(regl, { - width: numColumns, - height: numRows, - wrapT: "clamp", - type: "half float", - }); + const shineDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); const rainPassShine = loadText("shaders/glsl/rainPass.shine.frag.glsl"); const shineUniforms = { ...commonUniforms, - ...extractEntries(config, [ - "baseBrightness", - "baseContrast", - "brightnessDecay", - "fallSpeed", - "hasSun", - "hasThunder", - "raindropLength", - "rippleScale", - "rippleSpeed", - "rippleThickness", - "loops", - ]), - rippleType, + ...extractEntries(config, ["baseBrightness", "baseContrast", "brightnessDecay", "fallSpeed", "raindropLength", "loops"]), }; const shine = regl({ frag: regl.prop("frag"), @@ -87,12 +77,7 @@ export default ({ regl, config, lkg }) => { framebuffer: shineDoubleBuffer.front, }); - const symbolDoubleBuffer = makeDoubleBuffer(regl, { - width: numColumns, - height: numRows, - wrapT: "clamp", - type: "half float", - }); + const symbolDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); const rainPassSymbol = loadText("shaders/glsl/rainPass.symbol.frag.glsl"); const symbolUniforms = { ...commonUniforms, @@ -110,6 +95,24 @@ export default ({ regl, config, lkg }) => { framebuffer: symbolDoubleBuffer.front, }); + const effectDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); + const rainPassEffect = loadText("shaders/glsl/rainPass.effect.frag.glsl"); + const effectUniforms = { + ...commonUniforms, + ...extractEntries(config, ["hasThunder", "rippleScale", "rippleSpeed", "rippleThickness", "loops"]), + rippleType, + }; + const effect = regl({ + frag: regl.prop("frag"), + uniforms: { + ...effectUniforms, + shineState: shineDoubleBuffer.front, + previousEffectState: effectDoubleBuffer.back, + }, + + framebuffer: effectDoubleBuffer.front, + }); + const quadPositions = Array(numQuadRows) .fill() .map((_, y) => @@ -160,6 +163,7 @@ export default ({ regl, config, lkg }) => { shineState: shineDoubleBuffer.front, symbolState: symbolDoubleBuffer.front, + effectState: effectDoubleBuffer.front, glyphTex: msdf.texture, camera: regl.prop("camera"), @@ -254,6 +258,7 @@ export default ({ regl, config, lkg }) => { () => { shine({ frag: rainPassShine.text() }); symbol({ frag: rainPassSymbol.text() }); + effect({ frag: rainPassEffect.text() }); regl.clear({ depth: 1, color: [0, 0, 0, 1], diff --git a/shaders/glsl/rainPass.effect.frag.glsl b/shaders/glsl/rainPass.effect.frag.glsl new file mode 100644 index 0000000..32004d6 --- /dev/null +++ b/shaders/glsl/rainPass.effect.frag.glsl @@ -0,0 +1,99 @@ +precision highp float; + +// These effects are used to spice up the non-canon versions of the code rain. +// The shader writes them to the channels of a data texture: +// R: multiplied effects— magnify the cell's brightness +// G: added effects— offset the cell's brightness +// B: unused +// A: unused + +#define SQRT_2 1.4142135623730951 +#define SQRT_5 2.23606797749979 + +uniform sampler2D previousEffectState; +uniform float numColumns, numRows; +uniform float time, tick; +uniform float animationSpeed; + +uniform bool hasThunder, loops; +uniform float glyphHeightToWidth; +uniform int rippleType; +uniform float rippleScale, rippleSpeed, rippleThickness; + +// Helper functions for generating randomness, borrowed from elsewhere + +vec2 randomVec2( const in vec2 uv ) { + return fract(vec2(sin(uv.x * 591.32 + uv.y * 154.077), cos(uv.x * 391.32 + uv.y * 49.077))); +} + +float wobble(float x) { + return x + 0.3 * sin(SQRT_2 * x) + 0.2 * sin(SQRT_5 * x); +} + +float getThunder(float simTime, vec2 screenPos) { + if (!hasThunder) { + return 0.; + } + + simTime *= 0.5; + float thunder = 1. - fract(wobble(simTime)); + if (loops) { + thunder = 1. - fract(simTime + 0.3); + } + + thunder = log(thunder * 1.5) * 4.; + thunder = clamp(thunder, 0., 1.) * 10. * pow(screenPos.y, 2.); + return thunder; +} + +float getRipple(float simTime, vec2 screenPos) { + if (rippleType == -1) { + return 0.; + } + + float rippleTime = (simTime * 0.5 + sin(simTime) * 0.2) * rippleSpeed + 1.; // TODO: clarify + if (loops) { + rippleTime = (simTime * 0.5) * rippleSpeed + 1.; + } + + vec2 offset = randomVec2(vec2(floor(rippleTime), 0.)) - 0.5; + if (loops) { + offset = vec2(0.); + } + vec2 ripplePos = screenPos * 2. - 1. + offset; + float rippleDistance; + if (rippleType == 0) { + vec2 boxDistance = abs(ripplePos) * vec2(1., glyphHeightToWidth); + rippleDistance = max(boxDistance.x, boxDistance.y); + } else if (rippleType == 1) { + rippleDistance = length(ripplePos); + } + + float rippleValue = fract(rippleTime) * rippleScale - rippleDistance; + + if (rippleValue > 0. && rippleValue < rippleThickness) { + return 0.75; + } + + return 0.; +} + +// Main function + +vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous) { + + float multipliedEffects = 1.0 + getThunder(simTime, screenPos); + float addedEffects = getRipple(simTime, screenPos); // Round or square ripples across the grid + + vec4 result = vec4(multipliedEffects, addedEffects, 0., 0.); + return result; +} + +void main() { + float simTime = time * animationSpeed; + bool isFirstFrame = tick <= 1.; + vec2 glyphPos = gl_FragCoord.xy; + vec2 screenPos = glyphPos / vec2(numColumns, numRows); + vec4 previous = texture2D( previousEffectState, screenPos ); + gl_FragColor = computeResult(simTime, isFirstFrame, glyphPos, screenPos, previous); +} diff --git a/shaders/glsl/rainPass.frag.glsl b/shaders/glsl/rainPass.frag.glsl index aa1bfc9..f5ce0ed 100644 --- a/shaders/glsl/rainPass.frag.glsl +++ b/shaders/glsl/rainPass.frag.glsl @@ -4,7 +4,7 @@ #endif precision lowp float; -uniform sampler2D shineState, symbolState; +uniform sampler2D shineState, symbolState, effectState; uniform float numColumns, numRows; uniform sampler2D glyphTex; uniform float glyphHeightToWidth, glyphSequenceLength, glyphEdgeCrop; @@ -17,7 +17,7 @@ uniform bool showDebugView; uniform bool volumetric; varying vec2 vUV; -varying vec4 vShine, vSymbol; +varying vec4 vShine, vSymbol, vEffect; varying float vDepth; float median3(vec3 i) { @@ -63,6 +63,7 @@ void main() { // Unpack the values from the data textures vec4 shine = volumetric ? vShine : texture2D(shineState, uv); vec4 symbol = volumetric ? vSymbol : texture2D(symbolState, uv); + vec4 effect = volumetric ? vEffect : texture2D(effectState, uv); vec2 symbolUV = getSymbolUV(symbol.r); float brightness = shine.r; @@ -72,8 +73,10 @@ void main() { brightness = brightnessOverride; } + brightness *= effect.r; // multiplied effects + brightness += effect.g; // added effects brightness = max(shine.b * cursorBrightness, brightness); - brightness = max(shine.a, brightness); + // In volumetric mode, distant glyphs are dimmer if (volumetric && !showDebugView) { brightness = brightness * min(1., vDepth); diff --git a/shaders/glsl/rainPass.shine.frag.glsl b/shaders/glsl/rainPass.shine.frag.glsl index 55e9d66..00a3204 100644 --- a/shaders/glsl/rainPass.shine.frag.glsl +++ b/shaders/glsl/rainPass.shine.frag.glsl @@ -1,11 +1,11 @@ precision highp float; // This shader is the star of the show. -// It writes falling rain to four channels of a data texture: +// It writes falling rain to the channels of a data texture: // R: brightness // G: unused // B: whether the cell is a "cursor" -// A: some other effect, such as a ripple +// A: unused // Listen. // I understand if this shader looks confusing. Please don't be discouraged! @@ -21,12 +21,10 @@ uniform float numColumns, numRows; uniform float time, tick; uniform float animationSpeed, fallSpeed; -uniform bool hasSun, hasThunder, loops; +uniform bool loops; uniform float brightnessDecay; uniform float baseContrast, baseBrightness; uniform float raindropLength, glyphHeightToWidth; -uniform int rippleType; -uniform float rippleScale, rippleSpeed, rippleThickness; // Helper functions for generating randomness, borrowed from elsewhere @@ -66,88 +64,21 @@ float getBrightness(float rainTime) { return (1. - fract(rainTime)) * baseContrast + baseBrightness; } -// Additional effects - -float applySunShowerBrightness(float brightness, vec2 screenPos) { - if (brightness >= -4.) { - brightness = pow(fract(brightness * 0.5), 3.) * screenPos.y * 1.5; - } - return brightness; -} - -float applyThunderBrightness(float brightness, float simTime, vec2 screenPos) { - simTime *= 0.5; - float thunder = 1. - fract(wobble(simTime)); - if (loops) { - thunder = 1. - fract(simTime + 0.3); - } - - thunder = log(thunder * 1.5) * 4.; - thunder = clamp(thunder, 0., 1.); - thunder = thunder * pow(screenPos.y, 2.) * 3.; - return brightness + thunder; -} - -float applyRippleEffect(float effect, float simTime, vec2 screenPos) { - if (rippleType == -1) { - return effect; - } - - float rippleTime = (simTime * 0.5 + sin(simTime) * 0.2) * rippleSpeed + 1.; // TODO: clarify - if (loops) { - rippleTime = (simTime * 0.5) * rippleSpeed + 1.; - } - - vec2 offset = randomVec2(vec2(floor(rippleTime), 0.)) - 0.5; - if (loops) { - offset = vec2(0.); - } - vec2 ripplePos = screenPos * 2. - 1. + offset; - float rippleDistance; - if (rippleType == 0) { - vec2 boxDistance = abs(ripplePos) * vec2(1., glyphHeightToWidth); - rippleDistance = max(boxDistance.x, boxDistance.y); - } else if (rippleType == 1) { - rippleDistance = length(ripplePos); - } - - float rippleValue = fract(rippleTime) * rippleScale - rippleDistance; - - if (rippleValue > 0. && rippleValue < rippleThickness) { - effect += 0.75; - } - - return effect; -} - // Main function vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous) { - - // Determine the glyph's local time. float rainTime = getRainTime(simTime, glyphPos); float rainTimeBelow = getRainTime(simTime, glyphPos + vec2(0., -1.)); float cursor = fract(rainTime) < fract(rainTimeBelow) ? 1.0 : 0.0; - - // Rain time is the backbone of this effect. - - // Determine the glyph's brightness. float brightness = getBrightness(rainTime); - if (hasSun) brightness = applySunShowerBrightness(brightness, screenPos); - if (hasThunder) brightness = applyThunderBrightness(brightness, simTime, screenPos); - - // Determine the glyph's effect— the amount the glyph lights up for other reasons - float effect = 0.; - effect = applyRippleEffect(effect, simTime, screenPos); // Round or square ripples across the grid - // Blend the glyph's brightness with its previous brightness, so it winks on and off organically if (!isFirstFrame) { float previousBrightness = previous.r; brightness = mix(previousBrightness, brightness, brightnessDecay); } - vec4 result = vec4(brightness, fract(rainTime), cursor, effect); + vec4 result = vec4(brightness, fract(rainTime), cursor, 0.0); return result; } diff --git a/shaders/glsl/rainPass.symbol.frag.glsl b/shaders/glsl/rainPass.symbol.frag.glsl index 3cecf59..120b51c 100644 --- a/shaders/glsl/rainPass.symbol.frag.glsl +++ b/shaders/glsl/rainPass.symbol.frag.glsl @@ -1,7 +1,7 @@ precision highp float; // This shader governs the glyphs appearing in the rain. -// It writes each glyph's state to four channels of a data texture: +// It writes each glyph's state to the channels of a data texture: // R: symbol // G: age // B: unused diff --git a/shaders/glsl/rainPass.vert.glsl b/shaders/glsl/rainPass.vert.glsl index 7968b40..07537c9 100644 --- a/shaders/glsl/rainPass.vert.glsl +++ b/shaders/glsl/rainPass.vert.glsl @@ -1,7 +1,7 @@ #define PI 3.14159265359 precision lowp float; attribute vec2 aPosition, aCorner; -uniform sampler2D shineState, symbolState; +uniform sampler2D shineState, symbolState, effectState; uniform float density; uniform vec2 quadSize; uniform float glyphHeightToWidth, glyphVerticalSpacing; @@ -10,7 +10,7 @@ uniform vec2 screenSize; uniform float time, animationSpeed, forwardSpeed; uniform bool volumetric; varying vec2 vUV; -varying vec4 vShine, vSymbol; +varying vec4 vShine, vSymbol, vEffect; varying float vDepth; highp float rand( const in vec2 uv ) { @@ -24,6 +24,7 @@ void main() { vUV = (aPosition + aCorner) * quadSize; vShine = texture2D(shineState, aPosition * quadSize); vSymbol = texture2D(symbolState, aPosition * quadSize); + vEffect = texture2D(effectState, aPosition * quadSize); // Calculate the world space position float quadDepth = 0.0;