diff --git a/js/rainPass.js b/js/rainPass.js index 69e6c21..9204c30 100644 --- a/js/rainPass.js +++ b/js/rainPass.js @@ -84,7 +84,7 @@ export default (regl, config) => { frag: regl.prop("frag"), uniforms: { ...computeUniforms, - lastState: doubleBuffer.back, + previousState: doubleBuffer.back, }, framebuffer: doubleBuffer.front, diff --git a/shaders/rainPass.compute b/shaders/rainPass.compute index 04a227c..63680c0 100644 --- a/shaders/rainPass.compute +++ b/shaders/rainPass.compute @@ -1,88 +1,91 @@ precision highp float; -// This shader is the star of the show. -// In normal operation, each pixel represents a glyph's: -// R: brightness -// G: progress through the glyph sequence -// B: current glyph index -// A: additional brightness, for effects +// This shader is the star of the show. For each glyph, it determines its: +// R: brightness +// G: progress through the glyph sequence +// B: depth, aka distance from the screen +// A: additional brightness for effects\ + +// Listen. +// I understand if this shader looks confusing. Please don't be discouraged! +// It's just a handful of sine and fract functions. Try commenting parts out to learn +// how the different steps combine to produce the result. And feel free to reach out. -RM #define PI 3.14159265359 -#define RADS_TO_HZ 0.15915494309 #define SQRT_2 1.4142135623730951 #define SQRT_5 2.23606797749979 -uniform float time; -uniform float tick; +uniform sampler2D previousState; uniform float numColumns, numRows; -uniform sampler2D lastState; -uniform bool hasSun; -uniform bool hasThunder; +uniform float time, tick, cycleFrameSkip; +uniform float animationSpeed, fallSpeed, cycleSpeed; + +uniform bool hasSun, hasThunder; uniform bool showComputationTexture; uniform float brightnessOverride, brightnessThreshold, brightnessDecay; -uniform float animationSpeed, fallSpeed, cycleSpeed; -uniform float raindropLength; -uniform float glyphHeightToWidth; -uniform int cycleStyle; -uniform float cycleFrameSkip; +uniform float raindropLength, glyphHeightToWidth; +uniform int cycleStyle, rippleType; uniform float rippleScale, rippleSpeed, rippleThickness; -uniform int rippleType; uniform float cursorEffectThreshold; -float max2(vec2 v) { - return max(v.x, v.y); -} +// Helper functions for generating randomness, borrowed from elsewhere -highp float rand( const in vec2 uv ) { +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); } -vec2 rand2(vec2 p) { - return fract(vec2(sin(p.x * 591.32 + p.y * 154.077), cos(p.x * 391.32 + p.y * 49.077))); +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))); } +// Core functions + +// Rain time is the shader'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) { - float columnTimeOffset = rand(vec2(glyphPos.x, 0.0)); - float columnSpeedOffset = rand(vec2(glyphPos.x + 0.1, 0.0)); - // columnSpeedOffset = 0.0; // loop - float columnTime = (columnTimeOffset * 1000.0 + simTime * 0.5 * fallSpeed) * (0.5 + columnSpeedOffset * 0.5) + (sin(RADS_TO_HZ * simTime * fallSpeed * columnSpeedOffset) * 0.2); + float columnTimeOffset = randomFloat(vec2(glyphPos.x, 0.)); + float columnSpeedOffset = randomFloat(vec2(glyphPos.x + 0.1, 0.)); + // columnSpeedOffset = 0.; // TODO: loop + float columnTime = (columnTimeOffset * 1000. + simTime * 0.5 * fallSpeed) * (0.5 + columnSpeedOffset * 0.5) + (sin(simTime * fallSpeed * columnSpeedOffset) * 0.2); return (glyphPos.y * 0.01 + columnTime) / raindropLength; } -float getRainBrightness(float rainTime) { - float value = 1.0 - fract((rainTime + 0.3 * sin(RADS_TO_HZ * SQRT_2 * rainTime) + 0.2 * sin(RADS_TO_HZ * SQRT_5 * rainTime))); - // value = 1.0 - fract(rainTime); // loop - return log(value * 1.25) * 3.0; +float getBrightness(float rainTime) { + float value = 1. - fract((rainTime + 0.3 * sin(SQRT_2 * rainTime) + 0.2 * sin(SQRT_5 * rainTime))); + // value = 1. - fract(rainTime); // TODO: loop + return log(value * 1.25) * 3.; } -float getGlyphCycleSpeed(float rainTime, float brightness) { - float glyphCycleSpeed = 0.0; - if (cycleStyle == 0 && brightness > 0.0) { - glyphCycleSpeed = pow(1.0 - brightness, 4.0); +float getCycleSpeed(float rainTime, float brightness) { + float localCycleSpeed = 0.; + if (cycleStyle == 0 && brightness > 0.) { + localCycleSpeed = pow(1. - brightness, 4.); } else if (cycleStyle == 1) { - glyphCycleSpeed = fract(rainTime); + localCycleSpeed = fract(rainTime); } - return glyphCycleSpeed; + return animationSpeed * cycleSpeed * localCycleSpeed; } -float applySunShower(float rainBrightness, vec2 screenPos) { - if (rainBrightness < -4.) { - return rainBrightness; +// Additional effects + +float applySunShowerBrightness(float brightness, vec2 screenPos) { + if (brightness >= -4.) { + brightness = pow(fract(brightness * 0.5), 3.) * screenPos.y * 1.5; } - float value = pow(fract(rainBrightness * 0.5), 3.0) * screenPos.y * 1.5; - return value; + return brightness; } -float applyThunder(float rainBrightness, float simTime, vec2 screenPos) { +float applyThunderBrightness(float brightness, float simTime, vec2 screenPos) { simTime *= 0.5; - float thunder = 1.0 - fract((simTime + 0.3 * sin(RADS_TO_HZ * SQRT_2 * simTime) + 0.2 * sin(RADS_TO_HZ * SQRT_5 * simTime))); - // thunder = 1.0 - fract(simTime + 0.3); // loop - thunder = log(thunder * 1.5) * 4.0; + float thunder = 1. - fract((simTime + 0.3 * sin(SQRT_2 * simTime) + 0.2 * sin(SQRT_5 * simTime))); + // thunder = 1. - fract(simTime + 0.3); // TODO: loop + + thunder = log(thunder * 1.5) * 4.; thunder = clamp(thunder, 0., 1.); thunder = thunder * pow(screenPos.y, 2.) * 3.; - return rainBrightness + thunder; + return brightness + thunder; } float applyRippleEffect(float effect, float simTime, vec2 screenPos) { @@ -90,15 +93,16 @@ float applyRippleEffect(float effect, float simTime, vec2 screenPos) { return effect; } - float rippleTime = (simTime * 0.5 + 0.2 * sin(RADS_TO_HZ * simTime)) * rippleSpeed + 1.; - // rippleTime = (simTime * 0.5) * rippleSpeed + 1.; // loop + float rippleTime = (simTime * 0.5 + 0.2 * sin(simTime)) * rippleSpeed + 1.; + // rippleTime = (simTime * 0.5) * rippleSpeed + 1.; // TODO: loop - vec2 offset = rand2(vec2(floor(rippleTime), 0.)) - 0.5; - // offset = vec2(0.); // loop - vec2 ripplePos = screenPos * 2.0 - 1.0 + offset; + vec2 offset = randomVec2(vec2(floor(rippleTime), 0.)) - 0.5; + // offset = vec2(0.); // TODO: loop + vec2 ripplePos = screenPos * 2. - 1. + offset; float rippleDistance; if (rippleType == 0) { - rippleDistance = max2(abs(ripplePos) * vec2(1.0, glyphHeightToWidth)); + vec2 boxDistance = abs(ripplePos) * vec2(1., glyphHeightToWidth); + rippleDistance = max(boxDistance.x, boxDistance.y); } else if (rippleType == 1) { rippleDistance = length(ripplePos); } @@ -106,77 +110,83 @@ float applyRippleEffect(float effect, float simTime, vec2 screenPos) { float rippleValue = fract(rippleTime) * rippleScale - rippleDistance; if (rippleValue > 0. && rippleValue < rippleThickness) { - return effect + 0.75; - } else { - return effect; + effect += 0.75; } + + return effect; } float applyCursorEffect(float effect, float brightness) { if (brightness >= cursorEffectThreshold) { - effect = 1.0; + effect = 1.; } return effect; } -void main() { +// Main function +vec4 computeResult(bool isFirstFrame, vec4 previousResult, vec2 glyphPos, vec2 screenPos) { + + // Determine the glyph's local time. + float simTime = time * animationSpeed; + float rainTime = getRainTime(simTime, glyphPos); + + // Rain time is the backbone of this effect. + + // Determine the glyph's brightness. + float previousBrightness = previousResult.r; + float brightness = getBrightness(rainTime); + if (hasSun) { + brightness = applySunShowerBrightness(brightness, screenPos); + } + if (hasThunder) { + brightness = applyThunderBrightness(brightness, simTime, screenPos); + } + + // Determine the glyph's cycle— the percent this glyph has progressed through the glyph sequence + float previousCycle = previousResult.g; + bool resetGlyph = isFirstFrame; // || previousBrightness <= 0.; // TODO: loop + if (resetGlyph) { + previousCycle = showComputationTexture ? 0. : randomFloat(screenPos); + } + float localCycleSpeed = getCycleSpeed(rainTime, brightness); + float cycle = previousCycle; + if (mod(tick, cycleFrameSkip) == 0.) { + cycle = fract(previousCycle + 0.005 * localCycleSpeed * cycleFrameSkip); + } + + // 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 + effect = applyCursorEffect(effect, brightness); // The bright glyphs at the "bottom" of raindrops + + // Modes that don't fade glyphs set their actual brightness here + if (brightnessOverride > 0. && brightness > brightnessThreshold) { + brightness = brightnessOverride; + } + + // Blend the glyph's brightness with its previous brightness, so it winks on and off organically + if (!isFirstFrame) { + brightness = mix(previousBrightness, brightness, brightnessDecay); + } + + // Determine the glyph depth. This is a static value for each column. + float depth = randomFloat(vec2(screenPos.x, 0.)); + + vec4 result = vec4(brightness, cycle, depth, effect); + + // Better use of the blue channel, for demonstrating how the glyph cycle works + if (showComputationTexture) { + result.b = min(1., localCycleSpeed); + } + + return result; +} + +void main() { + bool isFirstFrame = tick <= 1.; vec2 glyphPos = gl_FragCoord.xy; vec2 screenPos = glyphPos / vec2(numColumns, numRows); - float simTime = time * animationSpeed; - - // Read the current values of the glyph - vec4 data = texture2D( lastState, screenPos ); - bool isInitializing = length(data) == 0.; - float oldRainBrightness = data.r; - float oldGlyphCycle = data.g; - if (isInitializing) { - oldGlyphCycle = showComputationTexture ? 0.5 : rand(screenPos); - } - - if (oldRainBrightness <= 0.0) { - // oldGlyphCycle = showComputationTexture ? 0.5 : rand(screenPos); // loop - } - - float rainTime = getRainTime(simTime, glyphPos); - float rainBrightness = getRainBrightness(rainTime); - - if (hasSun) rainBrightness = applySunShower(rainBrightness, screenPos); - if (hasThunder) rainBrightness = applyThunder(rainBrightness, simTime, screenPos); - - float glyphCycleSpeed = getGlyphCycleSpeed(rainTime, rainBrightness); - float glyphCycle = oldGlyphCycle; - if (mod(tick, cycleFrameSkip) == 0.0) { - glyphCycle = fract(oldGlyphCycle + 0.005 * animationSpeed * cycleSpeed * glyphCycleSpeed * cycleFrameSkip); - } - - float effect = 0.; - effect = applyRippleEffect(effect, simTime, screenPos); - effect = applyCursorEffect(effect, rainBrightness); - - float glyphDepth = rand(vec2(glyphPos.x, 0.0)); - - if (brightnessOverride > 0. && rainBrightness > brightnessThreshold) { - rainBrightness = brightnessOverride; - } - - if (!isInitializing) { - rainBrightness = mix(oldRainBrightness, rainBrightness, brightnessDecay); - } - - if (showComputationTexture) { - gl_FragColor = vec4( - rainBrightness, - glyphCycle, - min(1.0, glyphCycleSpeed), // Better use of the blue channel, for show and tell - 1.0 - ); - } else { - gl_FragColor = vec4( - rainBrightness, - glyphCycle, - glyphDepth, - effect - ); - } + vec4 previousResult = texture2D( previousState, screenPos ); + gl_FragColor = computeResult(isFirstFrame, previousResult, glyphPos, screenPos); }