diff --git a/README.md b/README.md index d8fe8a2..4c9a06e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ - [Trans flag colors](https://rezmason.github.io/matrix/?effect=trans) - [Custom stripes (`colors=R,G,B,R,G,B,R,G,B, etc`)](https://rezmason.github.io/matrix/?effect=customStripes&colors=1,0,0,1,1,0,0,1,0) - [Custom image (`url=www.website.com/picture.jpg`)](https://rezmason.github.io/matrix/?effect=image&url=https://upload.wikimedia.org/wikipedia/commons/f/f5/EagleRock.jpg) -- [Raw compute texture (`effect=none`) (_epilepsy warning_: lots of flickering)](https://rezmason.github.io/matrix/?effect=none) +- [Debug view (`effect=none`) (_epilepsy warning_: this once had lots of flickering)](https://rezmason.github.io/matrix/?effect=none) +- [Starting from a blank screen](https://rezmason.github.io/matrix/?skipIntro=false) (which some people really like, but isn't the default mode) - [The free classic font (TrueType).](https://github.com/Rezmason/matrix/raw/master/assets/Matrix-Code.ttf) - [The free *Resurrections* font (TrueType).](https://github.com/Rezmason/matrix/raw/master/assets/Matrix-Resurrected.ttf) @@ -87,6 +88,7 @@ Now you know link fu. Here's a list of customization options: - "paradise" is how the Matrix's idyllic predecessor may have appeared: warm, simplistic, encompassing. - "resurrections" is the updated Matrix code - "palimpsest" is a custom version inspired by the art and sound of [Rob Dougan](https://en.wikipedia.org/wiki/Rob_Dougan)'s [Furious Angels](https://en.wikipedia.org/wiki/Furious_Angels). +- **skipIntro** - whether or not to start from a blank screen. Can be "true" or "false", default is *true*. - **font** - the set of glyphs to draw. Current options are "matrixcode", "resurrections", "gothic", "coptic", "huberfishA", and "huberfishD". - **width** - the number of columns (and rows) to draw. Default is 80. - **volumetric** - when set to "true", this renders the glyphs with depth, slowly approaching the eye. Default is "false". diff --git a/js/config.js b/js/config.js index e410fba..7b9317f 100644 --- a/js/config.js +++ b/js/config.js @@ -112,6 +112,7 @@ const defaults = { isometric: false, useHoloplay: false, loops: false, + skipIntro: true, }; const versions = { @@ -410,6 +411,7 @@ const paramMapping = { glintColor: { key: "glintColor", parser: (s) => s.split(",").map(parseFloat) }, volumetric: { key: "volumetric", parser: (s) => s.toLowerCase().includes("true") }, loops: { key: "loops", parser: (s) => s.toLowerCase().includes("true") }, + skipIntro: { key: "skipIntro", parser: (s) => s.toLowerCase().includes("true") }, renderer: { key: "renderer", parser: (s) => s }, once: { key: "once", parser: (s) => s.toLowerCase().includes("true") }, isometric: { key: "isometric", parser: (s) => s.toLowerCase().includes("true") }, diff --git a/js/regl/rainPass.js b/js/regl/rainPass.js index 838f6fa..f301e90 100644 --- a/js/regl/rainPass.js +++ b/js/regl/rainPass.js @@ -55,17 +55,34 @@ export default ({ regl, config, lkg }) => { showDebugView, }; + const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns); + const rainPassIntro = loadText("shaders/glsl/rainPass.intro.frag.glsl"); + const introUniforms = { + ...commonUniforms, + ...extractEntries(config, ["fallSpeed", "skipIntro"]), + }; + const intro = regl({ + frag: regl.prop("frag"), + uniforms: { + ...introUniforms, + previousIntroState: introDoubleBuffer.back, + }, + + framebuffer: introDoubleBuffer.front, + }); + const raindropDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); - const rainPassShine = loadText("shaders/glsl/rainPass.raindrop.frag.glsl"); + const rainPassRaindrop = loadText("shaders/glsl/rainPass.raindrop.frag.glsl"); const raindropUniforms = { ...commonUniforms, - ...extractEntries(config, ["brightnessDecay", "fallSpeed", "raindropLength", "loops"]), + ...extractEntries(config, ["brightnessDecay", "fallSpeed", "raindropLength", "loops", "skipIntro"]), }; const raindrop = regl({ frag: regl.prop("frag"), uniforms: { ...raindropUniforms, - previousShineState: raindropDoubleBuffer.back, + introState: introDoubleBuffer.front, + previousRaindropState: raindropDoubleBuffer.back, }, framebuffer: raindropDoubleBuffer.front, @@ -217,7 +234,8 @@ export default ({ regl, config, lkg }) => { glintMSDF.loaded, baseTexture.loaded, glintTexture.loaded, - rainPassShine.loaded, + rainPassIntro.loaded, + rainPassRaindrop.loaded, rainPassSymbol.loaded, rainPassVert.loaded, rainPassFrag.loaded, @@ -271,7 +289,8 @@ export default ({ regl, config, lkg }) => { [screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; }, () => { - raindrop({ frag: rainPassShine.text() }); + intro({ frag: rainPassIntro.text() }); + raindrop({ frag: rainPassRaindrop.text() }); symbol({ frag: rainPassSymbol.text() }); effect({ frag: rainPassEffect.text() }); regl.clear({ diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index 5d1ef6e..24bb035 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -96,6 +96,11 @@ export default ({ config, device, timeBuffer }) => { const rainShaderUniforms = structs.from(rainShader.code); configBuffer = makeConfigBuffer(device, rainShaderUniforms.Config, config, density, gridSize); + const introCellsBuffer = device.createBuffer({ + size: gridSize[0] * rainShaderUniforms.IntroCell.minSize, + usage: GPUBufferUsage.STORAGE, + }); + const cellsBuffer = device.createBuffer({ size: numCells * rainShaderUniforms.Cell.minSize, usage: GPUBufferUsage.STORAGE, @@ -148,7 +153,7 @@ export default ({ config, device, timeBuffer }) => { }), ]); - computeBindGroup = makeBindGroup(device, computePipeline, 0, [configBuffer, timeBuffer, cellsBuffer]); + computeBindGroup = makeBindGroup(device, computePipeline, 0, [configBuffer, timeBuffer, cellsBuffer, introCellsBuffer]); renderBindGroup = makeBindGroup(device, renderPipeline, 0, [ configBuffer, timeBuffer, diff --git a/shaders/glsl/rainPass.frag.glsl b/shaders/glsl/rainPass.frag.glsl index bd44da6..bc30372 100644 --- a/shaders/glsl/rainPass.frag.glsl +++ b/shaders/glsl/rainPass.frag.glsl @@ -61,7 +61,7 @@ vec2 getUV(vec2 uv) { vec3 getBrightness(vec4 raindrop, vec4 effect, float quadDepth, vec2 uv) { - float base = raindrop.r; + float base = raindrop.r + max(0., 1.0 - raindrop.a * 5.0); bool isCursor = bool(raindrop.g) && isolateCursor; float glint = base; float multipliedEffects = effect.r; @@ -94,7 +94,7 @@ vec3 getBrightness(vec4 raindrop, vec4 effect, float quadDepth, vec2 uv) { return vec3( (isCursor ? vec2(0.0, 1.0) : vec2(1.0, 0.0)) * base, glint - ); + ) * raindrop.b; } vec2 getSymbolUV(float index) { diff --git a/shaders/glsl/rainPass.intro.frag.glsl b/shaders/glsl/rainPass.intro.frag.glsl new file mode 100644 index 0000000..b09bc9f --- /dev/null +++ b/shaders/glsl/rainPass.intro.frag.glsl @@ -0,0 +1,59 @@ +precision highp float; + +// This shader governs the "intro"— the initial stream of rain from a blank screen. +// It writes falling rain to the channels of a data texture: +// R: raindrop length +// G: unused +// B: unused +// A: unused + +#define PI 3.14159265359 +#define SQRT_2 1.4142135623730951 +#define SQRT_5 2.23606797749979 + +uniform sampler2D previousIntroState; +uniform float numColumns, numRows; +uniform float time, tick; +uniform float animationSpeed, fallSpeed; + +uniform bool skipIntro; + +// 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); +} + +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); +} + +// Main function + +vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous) { + if (skipIntro) { + return vec4(1., 0., 0., 0.); + } + + float columnTimeOffset = randomFloat(vec2(glyphPos.x, 0.)) * -10.; + columnTimeOffset += sin(glyphPos.x / numColumns * PI) - 1.; + float introTime = (simTime + columnTimeOffset) * fallSpeed / numRows * 100.; + + vec4 result = vec4(introTime, 0., 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( previousIntroState, screenPos ); + gl_FragColor = computeResult(simTime, isFirstFrame, glyphPos, screenPos, previous); +} diff --git a/shaders/glsl/rainPass.raindrop.frag.glsl b/shaders/glsl/rainPass.raindrop.frag.glsl index 5407dd3..870f3ae 100644 --- a/shaders/glsl/rainPass.raindrop.frag.glsl +++ b/shaders/glsl/rainPass.raindrop.frag.glsl @@ -4,7 +4,7 @@ precision highp float; // It writes falling rain to the channels of a data texture: // R: raindrop brightness // G: whether the cell is a "cursor" -// B: unused +// B: whether the cell is "activated" — to animate the intro // A: unused // Listen. @@ -16,15 +16,14 @@ precision highp float; #define SQRT_2 1.4142135623730951 #define SQRT_5 2.23606797749979 -uniform sampler2D previousShineState; +uniform sampler2D previousRaindropState, introState; uniform float numColumns, numRows; uniform float time, tick; uniform float animationSpeed, fallSpeed; -uniform bool loops; +uniform bool loops, skipIntro; uniform float brightnessDecay; -uniform float baseContrast, baseBrightness; -uniform float raindropLength, glyphHeightToWidth; +uniform float raindropLength; // Helper functions for generating randomness, borrowed from elsewhere @@ -61,10 +60,17 @@ float getRainBrightness(float simTime, vec2 glyphPos) { // Main function -vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous) { +vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec4 previous, vec4 intro) { float brightness = getRainBrightness(simTime, glyphPos); float brightnessBelow = getRainBrightness(simTime, glyphPos + vec2(0., -1.)); - float cursor = brightness > brightnessBelow ? 1.0 : 0.0; + + float introProgress = intro.r - (1. - glyphPos.y / numRows); + float introProgressBelow = intro.r - (1. - (glyphPos.y - 1.) / numRows); + + bool activated = bool(previous.b) || skipIntro || introProgress > 0.; + bool activatedBelow = skipIntro || introProgressBelow > 0.; + + bool cursor = brightness > brightnessBelow || (activated && !activatedBelow); // Blend the glyph's brightness with its previous brightness, so it winks on and off organically if (!isFirstFrame) { @@ -72,7 +78,7 @@ vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenP brightness = mix(previousBrightness, brightness, brightnessDecay); } - vec4 result = vec4(brightness, cursor, 0.0, 0.0); + vec4 result = vec4(brightness, cursor, activated, introProgress); return result; } @@ -81,6 +87,7 @@ void main() { bool isFirstFrame = tick <= 1.; vec2 glyphPos = gl_FragCoord.xy; vec2 screenPos = glyphPos / vec2(numColumns, numRows); - vec4 previous = texture2D( previousShineState, screenPos ); - gl_FragColor = computeResult(simTime, isFirstFrame, glyphPos, screenPos, previous); + vec4 previous = texture2D( previousRaindropState, screenPos ); + vec4 intro = texture2D( introState, vec2(screenPos.x, 0.) ); + gl_FragColor = computeResult(simTime, isFirstFrame, glyphPos, previous, intro); } diff --git a/shaders/wgsl/rainPass.wgsl b/shaders/wgsl/rainPass.wgsl index ddd585b..eb42d09 100644 --- a/shaders/wgsl/rainPass.wgsl +++ b/shaders/wgsl/rainPass.wgsl @@ -43,6 +43,7 @@ struct Config { isolateCursor : i32, isolateGlint : i32, loops : i32, + skipIntro : i32, highPassThreshold : f32, }; @@ -70,12 +71,22 @@ struct CellData { cells: array, }; +struct IntroCell { + progress : vec4, +}; + +// The array of cells that the compute shader updates, and the fragment shader draws. +struct IntroCellData { + cells: array, +}; + // Shared bindings @group(0) @binding(0) var config : Config; @group(0) @binding(1) var time : Time; // Compute-specific bindings @group(0) @binding(2) var cells_RW : CellData; +@group(0) @binding(3) var introCells_RW : IntroCellData; // Render-specific bindings @group(0) @binding(2) var scene : Scene; @@ -205,11 +216,32 @@ fn getRipple(simTime : f32, screenPos : vec2) -> f32 { // Compute shader main functions -fn computeRaindrop (simTime : f32, isFirstFrame : bool, glyphPos : vec2, screenPos : vec2, previous : vec4) -> vec4 { +fn computeIntro (simTime : f32, isFirstFrame : bool, glyphPos : vec2, screenPos : vec2, previous : vec4) -> vec4 { + if (bool(config.skipIntro)) { + return vec4(1.0, 0.0, 0.0, 0.0); + } + + var columnTimeOffset = randomFloat(glyphPos) * -10.0; + columnTimeOffset += sin(glyphPos.x / config.gridSize.x * PI) - 1.0; + var introTime = (simTime + columnTimeOffset) * config.fallSpeed / config.gridSize.y * 100.0; + + var result = vec4(introTime, 0.0, 0.0, 0.0); + return result; +} + +fn computeRaindrop (simTime : f32, isFirstFrame : bool, glyphPos : vec2, screenPos : vec2, previous : vec4, progress : vec4) -> vec4 { var brightness = getRainBrightness(simTime, glyphPos); - var brightnessBelow = getRainBrightness(simTime, glyphPos + vec2(0., -1.)); - var cursor = select(0.0, 1.0, brightness > brightnessBelow); + var brightnessBelow = getRainBrightness(simTime, glyphPos + vec2(0.0, -1.0)); + + var introProgress = progress.r - (1.0 - glyphPos.y / config.gridSize.y); + var introProgressBelow = progress.r - (1.0 - (glyphPos.y - 1.0) / config.gridSize.y); + + var skipIntro = bool(config.skipIntro); + var activated = bool(previous.b) || skipIntro || introProgress > 0.0; + var activatedBelow = skipIntro || introProgressBelow > 0.0; + + var cursor = brightness > brightnessBelow || (activated && !activatedBelow); // Blend the glyph's brightness with its previous brightness, so it winks on and off organically if (!isFirstFrame) { @@ -217,7 +249,7 @@ fn computeRaindrop (simTime : f32, isFirstFrame : bool, glyphPos : vec2, sc brightness = mix(previousBrightness, brightness, config.brightnessDecay); } - var result = vec4(brightness, cursor, 0.0, 0.0); + var result = vec4(brightness, f32(cursor), f32(activated), introProgress); return result; } @@ -277,8 +309,15 @@ fn computeEffect (simTime : f32, isFirstFrame : bool, glyphPos : vec2, scre var glyphPos = vec2(f32(column), f32(row)); var screenPos = glyphPos / config.gridSize; + var introCell = introCells_RW.cells[column]; + + if (row == i32(config.gridSize.y - 1)) { + introCell.progress = computeIntro(simTime, isFirstFrame, glyphPos, screenPos, introCell.progress); + introCells_RW.cells[column] = introCell; + } + var cell = cells_RW.cells[i]; - cell.raindrop = computeRaindrop(simTime, isFirstFrame, glyphPos, screenPos, cell.raindrop); + cell.raindrop = computeRaindrop(simTime, isFirstFrame, glyphPos, screenPos, cell.raindrop, introCell.progress); cell.symbol = computeSymbol(simTime, isFirstFrame, glyphPos, screenPos, cell.symbol, cell.raindrop); cell.effect = computeEffect(simTime, isFirstFrame, glyphPos, screenPos, cell.effect, cell.raindrop); cells_RW.cells[i] = cell; @@ -379,7 +418,7 @@ fn getUV(inputUV : vec2) -> vec2 { fn getBrightness(raindrop : vec4, effect : vec4, uv : vec2, quadDepth : f32) -> vec3 { - var base = raindrop.r; + var base = raindrop.r + max(0.0, 1.0 - raindrop.a * 5.0); var isCursor = bool(raindrop.g) && bool(config.isolateCursor); var glint = base; var multipliedEffects = effect.r; @@ -412,7 +451,7 @@ fn getBrightness(raindrop : vec4, effect : vec4, uv : vec2, quadD return vec3( select(vec2(1.0, 0.0), vec2(0.0, 1.0), isCursor) * base, glint - ); + ) * raindrop.b; } fn getSymbolUV(symbol : i32) -> vec2 {