diff --git a/TODO.txt b/TODO.txt index cb6104e..359efc7 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,18 +1,15 @@ 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 - New config properties + The post-bloom passes should apply brightness, contrast, and handle effects + r: cursor, g: long tail, b: short tail, a: maybe effects data goes here? we'll see + But, the texture format beyond the rain pass is 8-bit. Something to watch out for. + Create new, simpler default colorizer + Config properties: Cursor color, long tail color, short tail color + Figure out what to do with the thunder and ripple + Tune ALL the versions! Find a way to support the old stuff? Migrate to WebGPU @@ -27,7 +24,6 @@ Audio system Randomize pitch a little? Playdate version - Separate glyph cycling from brightness Audio system Falling sound Launch sound diff --git a/js/config.js b/js/config.js index 97ccc0b..50fa0e9 100644 --- a/js/config.js +++ b/js/config.js @@ -150,11 +150,11 @@ const versions = { font: "coptic", bloomStrength: 1, highPassThreshold: 0, - cycleSpeed: 0.05, + cycleSpeed: 0.005, baseBrightness: -1.3, baseContrast: 2, brightnessDecay: 0.05, - fallSpeed: 0.04, + fallSpeed: 0.02, isPolar: true, rippleTypeName: "circle", rippleSpeed: 0.1, @@ -176,7 +176,7 @@ const versions = { baseContrast: 1.17, highPassThreshold: 0, numColumns: 70, - cycleSpeed: 0.05, + cycleSpeed: 0.03, bloomStrength: 0.7, fallSpeed: 0.3, paletteEntries: [ @@ -218,7 +218,7 @@ const versions = { font: "resurrections", numColumns: 20, fallSpeed: 0.35, - cycleSpeed: 0.3, + cycleSpeed: 0.04, glyphEdgeCrop: 0.1, ditherMagnitude: 0, paletteEntries: [ @@ -240,7 +240,7 @@ const versions = { ["3d"]: { volumetric: true, fallSpeed: 0.5, - cycleSpeed: 0.35, + cycleSpeed: 0.03, baseBrightness: -0.9, baseContrast: 1.5, raindropLength: 0.3, diff --git a/js/regl/rainPass.js b/js/regl/rainPass.js index 513436e..005a1a8 100644 --- a/js/regl/rainPass.js +++ b/js/regl/rainPass.js @@ -59,7 +59,7 @@ export default ({ regl, config, lkg }) => { const rainPassShine = loadText("shaders/glsl/rainPass.shine.frag.glsl"); const shineUniforms = { ...commonUniforms, - ...extractEntries(config, ["baseBrightness", "baseContrast", "brightnessDecay", "fallSpeed", "raindropLength", "loops"]), + ...extractEntries(config, ["brightnessDecay", "fallSpeed", "raindropLength", "loops"]), }; const shine = regl({ frag: regl.prop("frag"), @@ -126,6 +126,8 @@ export default ({ regl, config, lkg }) => { "forwardSpeed", "glyphVerticalSpacing", // fragment + "baseBrightness", + "baseContrast", "brightnessThreshold", "brightnessOverride", "cursorBrightness", diff --git a/shaders/glsl/rainPass.frag.glsl b/shaders/glsl/rainPass.frag.glsl index f5ce0ed..39efe49 100644 --- a/shaders/glsl/rainPass.frag.glsl +++ b/shaders/glsl/rainPass.frag.glsl @@ -8,6 +8,7 @@ uniform sampler2D shineState, symbolState, effectState; uniform float numColumns, numRows; uniform sampler2D glyphTex; uniform float glyphHeightToWidth, glyphSequenceLength, glyphEdgeCrop; +uniform float baseContrast, baseBrightness; uniform float brightnessOverride, brightnessThreshold, cursorBrightness; uniform vec2 glyphTextureGridSize; uniform vec2 slantVec; @@ -29,84 +30,99 @@ float modI(float a, float b) { return floor(m + 0.5); } -vec2 getSymbolUV(float symbol) { - float symbolX = modI(symbol, glyphTextureGridSize.x); - float symbolY = (symbol - symbolX) / glyphTextureGridSize.x; - symbolY = glyphTextureGridSize.y - symbolY - 1.; - return vec2(symbolX, symbolY); -} +vec2 getUV(vec2 uv) { -void main() { - - vec2 uv = vUV; - - // In normal mode, derives the current glyph and UV from vUV - if (!volumetric) { - if (isPolar) { - // Curved space that makes letters appear to radiate from up above - uv -= 0.5; - uv *= 0.5; - uv.y -= 0.5; - float radius = length(uv); - float angle = atan(uv.y, uv.x) / (2. * PI) + 0.5; - uv = vec2(fract(angle * 4. - 0.5), 1.5 * (1. - sqrt(radius))); - } else { - // Applies the slant and scales space so the viewport is fully covered - uv = vec2( - (uv.x - 0.5) * slantVec.x + (uv.y - 0.5) * slantVec.y, - (uv.y - 0.5) * slantVec.x - (uv.x - 0.5) * slantVec.y - ) * slantScale + 0.5; - } - uv.y /= glyphHeightToWidth; + if (volumetric) { + return uv; } - // 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); + if (isPolar) { + // Curved space that makes letters appear to radiate from up above + uv -= 0.5; + uv *= 0.5; + uv.y -= 0.5; + float radius = length(uv); + float angle = atan(uv.y, uv.x) / (2. * PI) + 0.5; + uv = vec2(fract(angle * 4. - 0.5), 1.5 * (1. - sqrt(radius))); + } else { + // Applies the slant and scales space so the viewport is fully covered + uv = vec2( + (uv.x - 0.5) * slantVec.x + (uv.y - 0.5) * slantVec.y, + (uv.y - 0.5) * slantVec.x - (uv.x - 0.5) * slantVec.y + ) * slantScale + 0.5; + } - float brightness = shine.r; + uv.y /= glyphHeightToWidth; + + return uv; +} + +float getBrightness(float brightness, float cursor, float multipliedEffects, float addedEffects) { + brightness = (1.0 - brightness) * baseContrast + baseBrightness; // Modes that don't fade glyphs set their actual brightness here if (brightnessOverride > 0. && brightness > brightnessThreshold) { brightness = brightnessOverride; } - brightness *= effect.r; // multiplied effects - brightness += effect.g; // added effects - brightness = max(shine.b * cursorBrightness, brightness); + brightness *= multipliedEffects; + brightness += addedEffects; + brightness = max(cursor * cursorBrightness, brightness); // In volumetric mode, distant glyphs are dimmer if (volumetric && !showDebugView) { brightness = brightness * min(1., vDepth); } + return brightness; +} + +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); +} + +float getSymbol(vec2 uv, float index) { // resolve UV to cropped position of glyph in MSDF texture - vec2 glyphUV = fract(uv * vec2(numColumns, numRows)); - glyphUV -= 0.5; - glyphUV *= clamp(1. - glyphEdgeCrop, 0., 1.); - glyphUV += 0.5; - vec2 msdfUV = (glyphUV + symbolUV) / glyphTextureGridSize; + 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 - vec3 dist = texture2D(glyphTex, msdfUV).rgb; + vec3 dist = texture2D(glyphTex, uv).rgb; float sigDist = median3(dist) - 0.5; - float alpha = clamp(sigDist/fwidth(sigDist) + 0.5, 0., 1.); + return clamp(sigDist/fwidth(sigDist) + 0.5, 0., 1.); +} + +void main() { + + vec2 uv = getUV(vUV); + + // Unpack the values from the data textures + vec4 shineData = volumetric ? vShine : texture2D( shineState, uv); + vec4 symbolData = volumetric ? vSymbol : texture2D(symbolState, uv); + vec4 effectData = volumetric ? vEffect : texture2D(effectState, uv); + + float brightness = getBrightness(shineData.r, shineData.g, effectData.r, effectData.g); + float symbol = getSymbol(uv, symbolData.r); if (showDebugView) { gl_FragColor = vec4( vec3( - shine.b, + shineData.g, vec2( - 1.0 - (shine.g * 3.0), - 1.0 - (shine.g * 10.0) - ) * (1.0 - shine.b) - ) * alpha, + 1.0 - (shineData.r * 3.0), + 1.0 - (shineData.r * 8.0) + ) * (1.0 - shineData.g) + ) * symbol, 1. ); } else { - gl_FragColor = vec4(brightness * alpha, 0., 0., 1.); + gl_FragColor = vec4(brightness * symbol, 0., 0., 1.); } } diff --git a/shaders/glsl/rainPass.shine.frag.glsl b/shaders/glsl/rainPass.shine.frag.glsl index 00a3204..e63f670 100644 --- a/shaders/glsl/rainPass.shine.frag.glsl +++ b/shaders/glsl/rainPass.shine.frag.glsl @@ -42,11 +42,10 @@ float wobble(float x) { return x + 0.3 * sin(SQRT_2 * x) + 0.2 * sin(SQRT_5 * x); } -// Core functions - -// Rain time is the shader's key underlying concept. +// This is the code rain's key underlying concept. // It's why glyphs that share a column are lit simultaneously, and are brighter toward the bottom. -float getRainTime(float simTime, vec2 glyphPos) { +// It's also why those bright areas are truncated into raindrops. +float getBrightness(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; if (loops) { @@ -57,20 +56,15 @@ float getRainTime(float simTime, vec2 glyphPos) { if (!loops) { rainTime = wobble(rainTime); } - return rainTime; -} - -float getBrightness(float rainTime) { - return (1. - fract(rainTime)) * baseContrast + baseBrightness; + return fract(rainTime); } // Main function vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous) { - float rainTime = getRainTime(simTime, glyphPos); - float rainTimeBelow = getRainTime(simTime, glyphPos + vec2(0., -1.)); - float cursor = fract(rainTime) < fract(rainTimeBelow) ? 1.0 : 0.0; - float brightness = getBrightness(rainTime); + float brightness = getBrightness(simTime, glyphPos); + float brightnessBelow = getBrightness(simTime, glyphPos + vec2(0., -1.)); + float cursor = brightness < brightnessBelow ? 1.0 : 0.0; // Blend the glyph's brightness with its previous brightness, so it winks on and off organically if (!isFirstFrame) { @@ -78,7 +72,7 @@ vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenP brightness = mix(previousBrightness, brightness, brightnessDecay); } - vec4 result = vec4(brightness, fract(rainTime), cursor, 0.0); + vec4 result = vec4(brightness, cursor, 0.0, 0.0); return result; }