Combining the rain pass's compute shaders

This commit is contained in:
Rezmason
2023-08-22 11:46:29 -07:00
parent 7a4f8b0e0b
commit 2d97f764f5
11 changed files with 122 additions and 8490 deletions

View File

@@ -1,10 +1,9 @@
TODO:
Simplify!
Pare down config
Get rid of everything inessential
Remove WebGPU
Remove config options
Remove features
Remove subsystems
Get as much into one file as you possibly can
Remove dependencies
Remove regl

View File

@@ -4,18 +4,12 @@ const config = {
glyphMSDFURL: "assets/matrixcode_msdf.png",
glyphSequenceLength: 57,
glyphTextureGridSize: [8, 8],
effect: "palette", // The name of the effect to apply at the end of the process— mainly handles coloration
baseTexture: null, // The name of the texture to apply to the base layer of the glyphs
glintTexture: null, // The name of the texture to apply to the glint layer of the glyphs
useCamera: false,
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
isolateGlint: false, // Whether the "glint"— highlights on certain symbols in the font— should appear
glintColor: hsl(0, 0, 1), // The color of the glint
glintIntensity: 1, // The intensity of the glint
volumetric: false, // A mode where the raindrops appear in perspective
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
@@ -30,20 +24,12 @@ const config = {
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
brightnessDecay: 1.0, // The rate at which glyphs light up and dim
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
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
rippleThickness: 0.2, // The thickness of the ripple effect
rippleScale: 30, // The size of the ripple effect
rippleSpeed: 0.2, // The rate at which the ripple effect progresses
numColumns: 80, // The maximum dimension of the glyph grid
density: 1, // In volumetric mode, the number of actual columns compared to the grid
palette: [
// The color palette that glyph brightness is color mapped to
{ color: hsl(0.3, 0.9, 0.0), at: 0.0 },
@@ -52,16 +38,8 @@ const config = {
{ 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
slant: 0, // The angle at which rain falls; the orientation of the glyph grid
resolution: 0.75, // An overall scale multiplier
useHalfFloat: false,
renderer: "regl", // The preferred web graphics API
suppressWarnings: false, // Whether to show warnings to visitors on load
isometric: false,
useHoloplay: false,
loops: false,
skipIntro: true,
testFix: null,
};
const canvas = document.createElement("canvas");
@@ -88,7 +66,7 @@ const loadJS = (src) =>
});
const init = async () => {
await Promise.all([loadJS("lib/regl.js"), loadJS("lib/gl-matrix.js")]);
await loadJS("lib/regl.js");
const resize = () => {
const devicePixelRatio = window.devicePixelRatio ?? 1;
@@ -111,24 +89,10 @@ const init = async () => {
}
resize();
if (config.useCamera) {
await setupCamera();
}
const extensions = ["OES_texture_half_float", "OES_texture_half_float_linear"];
// These extensions are also needed, but Safari misreports that they are missing
const optionalExtensions = ["EXT_color_buffer_half_float", "WEBGL_color_buffer_float", "OES_standard_derivatives"];
switch (config.testFix) {
case "fwidth_10_1_2022_A":
extensions.push("OES_standard_derivatives");
break;
case "fwidth_10_1_2022_B":
optionalExtensions.forEach((ext) => extensions.push(ext));
extensions.length = 0;
break;
}
const regl = createREGL({ canvas, pixelRatio: 1, extensions, optionalExtensions });
// All this takes place in a full screen quad.
@@ -142,7 +106,7 @@ const init = async () => {
const targetFrameTimeMilliseconds = 1000 / config.fps;
let last = NaN;
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
const render = ({ viewportWidth, viewportHeight }) => {
if (config.once) {
tick.cancel();
}
@@ -177,7 +141,11 @@ const init = async () => {
}
drawToScreen();
});
});
};
render({viewportWidth: 1, viewportHeight: 1});
const tick = regl.frame(render);
};
document.body.onload = () => {

View File

@@ -2,11 +2,6 @@ import { loadImage, loadText, makePassFBO, makeDoubleBuffer, makePass } from "./
const extractEntries = (src, keys) => Object.fromEntries(Array.from(Object.entries(src)).filter(([key]) => keys.includes(key)));
const rippleTypes = {
box: 0,
circle: 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,
@@ -30,112 +25,41 @@ const brVert = [1, 1];
const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert];
export default ({ regl, config }) => {
// The volumetric mode multiplies the number of columns
// to reach the desired density, and then overlaps them
const volumetric = config.volumetric;
const density = volumetric && config.effect !== "none" ? config.density : 1;
const [numRows, numColumns] = [config.numColumns, Math.floor(config.numColumns * density)];
// The volumetric mode requires us to create a grid of quads,
// rather than a single quad for our geometry
const [numQuadRows, numQuadColumns] = volumetric ? [numRows, numColumns] : [1, 1];
const numQuads = numQuadRows * numQuadColumns;
const quadSize = [1 / numQuadColumns, 1 / numQuadRows];
// Various effect-related values
const rippleType = config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1;
const slantVec = [Math.cos(config.slant), Math.sin(config.slant)];
const slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1);
const showDebugView = config.effect === "none";
const [numRows, numColumns] = [config.numColumns, config.numColumns];
const commonUniforms = {
...extractEntries(config, ["animationSpeed", "glyphHeightToWidth", "glyphSequenceLength", "glyphTextureGridSize"]),
numColumns,
numRows,
showDebugView,
};
const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns);
const rainPassIntro = loadText("shaders/glsl/rainPass.intro.frag.glsl");
const introUniforms = {
const computeDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
const rainPassCompute = loadText("shaders/glsl/rainPass.compute.frag.glsl");
const computeUniforms = {
...commonUniforms,
...extractEntries(config, ["fallSpeed", "skipIntro"]),
...extractEntries(config, ["fallSpeed", "raindropLength"]),
...extractEntries(config, ["cycleSpeed", "cycleFrameSkip"]),
};
const intro = regl({
const compute = regl({
frag: regl.prop("frag"),
uniforms: {
...introUniforms,
previousIntroState: introDoubleBuffer.back,
...computeUniforms,
previousComputeState: computeDoubleBuffer.back,
},
framebuffer: introDoubleBuffer.front,
framebuffer: computeDoubleBuffer.front,
});
const raindropDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
const rainPassRaindrop = loadText("shaders/glsl/rainPass.raindrop.frag.glsl");
const raindropUniforms = {
...commonUniforms,
...extractEntries(config, ["brightnessDecay", "fallSpeed", "raindropLength", "loops", "skipIntro"]),
};
const raindrop = regl({
frag: regl.prop("frag"),
uniforms: {
...raindropUniforms,
introState: introDoubleBuffer.front,
previousRaindropState: raindropDoubleBuffer.back,
},
framebuffer: raindropDoubleBuffer.front,
});
const symbolDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
const rainPassSymbol = loadText("shaders/glsl/rainPass.symbol.frag.glsl");
const symbolUniforms = {
...commonUniforms,
...extractEntries(config, ["cycleSpeed", "cycleFrameSkip", "loops"]),
};
const symbol = regl({
frag: regl.prop("frag"),
uniforms: {
...symbolUniforms,
raindropState: raindropDoubleBuffer.front,
previousSymbolState: symbolDoubleBuffer.back,
},
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,
raindropState: raindropDoubleBuffer.front,
previousEffectState: effectDoubleBuffer.back,
},
framebuffer: effectDoubleBuffer.front,
});
const quadPositions = Array(numQuadRows)
const quadPositions = Array(1)
.fill()
.map((_, y) =>
Array(numQuadColumns)
Array(1)
.fill()
.map((_, x) => Array(numVerticesPerQuad).fill([x, y]))
);
// We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen
const glyphMSDF = loadImage(regl, config.glyphMSDFURL);
const glintMSDF = loadImage(regl, config.glintMSDFURL);
const baseTexture = loadImage(regl, config.baseTextureURL, true);
const glintTexture = loadImage(regl, config.glintTextureURL, true);
const rainPassVert = loadText("shaders/glsl/rainPass.vert.glsl");
const rainPassFrag = loadText("shaders/glsl/rainPass.frag.glsl");
const output = makePassFBO(regl, config.useHalfFloat);
@@ -153,17 +77,8 @@ export default ({ regl, config }) => {
"brightnessThreshold",
"brightnessOverride",
"isolateCursor",
"isolateGlint",
"glyphEdgeCrop",
"isPolar",
]),
density,
numQuadColumns,
numQuadRows,
quadSize,
slantScale,
slantVec,
volumetric,
};
const render = regl({
blend: {
@@ -179,45 +94,25 @@ export default ({ regl, config }) => {
uniforms: {
...renderUniforms,
raindropState: raindropDoubleBuffer.front,
symbolState: symbolDoubleBuffer.front,
effectState: effectDoubleBuffer.front,
computeState: computeDoubleBuffer.front,
glyphMSDF: glyphMSDF.texture,
glintMSDF: glintMSDF.texture,
baseTexture: baseTexture.texture,
glintTexture: glintTexture.texture,
msdfPxRange: 4.0,
glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()],
glintMSDFSize: () => [glintMSDF.width(), glintMSDF.height()],
camera: regl.prop("camera"),
transform: regl.prop("transform"),
screenSize: regl.prop("screenSize"),
},
attributes: {
aPosition: quadPositions,
aCorner: Array(numQuads).fill(quadVertices),
aCorner: quadVertices,
},
count: numQuads * numVerticesPerQuad,
count: numVerticesPerQuad,
framebuffer: output,
});
// Camera and transform math for the volumetric mode
const screenSize = [1, 1];
const { mat4, vec3 } = glMatrix;
const transform = mat4.create();
if (volumetric && config.isometric) {
mat4.rotateX(transform, transform, (Math.PI * 1) / 8);
mat4.rotateY(transform, transform, (Math.PI * 1) / 4);
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
mat4.scale(transform, transform, vec3.fromValues(1, 1, 2));
} else {
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
}
const camera = mat4.create();
return makePass(
{
@@ -225,36 +120,17 @@ export default ({ regl, config }) => {
},
Promise.all([
glyphMSDF.loaded,
glintMSDF.loaded,
baseTexture.loaded,
glintTexture.loaded,
rainPassIntro.loaded,
rainPassRaindrop.loaded,
rainPassSymbol.loaded,
rainPassCompute.loaded,
rainPassVert.loaded,
rainPassFrag.loaded,
]),
(w, h) => {
output.resize(w, h);
const aspectRatio = w / h;
if (volumetric && config.isometric) {
if (aspectRatio > 1) {
mat4.ortho(camera, -1.5 * aspectRatio, 1.5 * aspectRatio, -1.5, 1.5, -1000, 1000);
} else {
mat4.ortho(camera, -1.5, 1.5, -1.5 / aspectRatio, 1.5 / aspectRatio, -1000, 1000);
}
} else {
mat4.perspective(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000);
}
[screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
},
(shouldRender) => {
intro({ frag: rainPassIntro.text() });
raindrop({ frag: rainPassRaindrop.text() });
symbol({ frag: rainPassSymbol.text() });
effect({ frag: rainPassEffect.text() });
compute({ frag: rainPassCompute.text() });
if (shouldRender) {
regl.clear({
@@ -263,7 +139,7 @@ export default ({ regl, config }) => {
framebuffer: output,
});
render({ camera, transform, screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() });
render({ screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() });
}
}
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
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);
}

View File

@@ -1,99 +0,0 @@
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.;
}
float thunderTime = simTime * 0.5;
float thunder = 1. - fract(wobble(thunderTime));
if (loops) {
thunder = 1. - fract(thunderTime + 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. + 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);
}

View File

@@ -4,25 +4,18 @@
#endif
precision lowp float;
uniform sampler2D raindropState, symbolState, effectState;
uniform sampler2D computeState;
uniform float numColumns, numRows;
uniform sampler2D glyphMSDF, glintMSDF, baseTexture, glintTexture;
uniform sampler2D glyphMSDF;
uniform float msdfPxRange;
uniform vec2 glyphMSDFSize, glintMSDFSize;
uniform vec2 glyphMSDFSize;
uniform float glyphHeightToWidth, glyphSequenceLength, glyphEdgeCrop;
uniform float baseContrast, baseBrightness, glintContrast, glintBrightness;
uniform float brightnessOverride, brightnessThreshold;
uniform vec2 glyphTextureGridSize;
uniform vec2 slantVec;
uniform float slantScale;
uniform bool isPolar;
uniform bool showDebugView;
uniform bool volumetric;
uniform bool isolateCursor, isolateGlint;
uniform bool isolateCursor;
varying vec2 vUV;
varying vec4 vRaindrop, vSymbol, vEffect;
varying float vDepth;
float median3(vec3 i) {
return max(min(i.r, i.g), min(max(i.r, i.g), i.b));
@@ -34,39 +27,15 @@ float modI(float a, float b) {
}
vec2 getUV(vec2 uv) {
if (volumetric) {
return uv;
}
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;
return uv;
}
vec3 getBrightness(vec4 raindrop, vec4 effect, float quadDepth, vec2 uv) {
vec3 getBrightness(vec2 raindrop, vec2 uv) {
float base = raindrop.r + max(0., 1.0 - raindrop.a * 5.0);
float base = raindrop.r;
bool isCursor = bool(raindrop.g) && isolateCursor;
float glint = base;
float multipliedEffects = effect.r;
float addedEffects = effect.g;
vec2 textureUV = fract(uv * vec2(numColumns, numRows));
base = base * baseContrast + baseBrightness;
@@ -77,19 +46,10 @@ vec3 getBrightness(vec4 raindrop, vec4 effect, float quadDepth, vec2 uv) {
base = brightnessOverride;
}
base = base * multipliedEffects + addedEffects;
glint = glint * multipliedEffects + addedEffects;
// In volumetric mode, distant glyphs are dimmer
if (volumetric && !showDebugView) {
base = base * min(1.0, quadDepth);
glint = glint * min(1.0, quadDepth);
}
return vec3(
(isCursor ? vec2(0.0, 1.0) : vec2(1.0, 0.0)) * base,
glint
) * raindrop.b;
);
}
vec2 getSymbolUV(float index) {
@@ -119,16 +79,6 @@ vec2 getSymbol(vec2 uv, float index) {
symbol.r = clamp(screenPxDistance + 0.5, 0.0, 1.0);
}
if (isolateGlint) {
vec2 unitRange = vec2(msdfPxRange) / glintMSDFSize;
vec2 screenTexSize = vec2(1.0) / fwidth(uv);
float screenPxRange = max(0.5 * dot(unitRange, screenTexSize), 1.0);
float signedDistance = median3(texture2D(glintMSDF, uv).rgb);
float screenPxDistance = screenPxRange * (signedDistance - 0.5);
symbol.g = clamp(screenPxDistance + 0.5, 0.0, 1.0);
}
return symbol;
}
@@ -137,30 +87,10 @@ void main() {
vec2 uv = getUV(vUV);
// Unpack the values from the data textures
vec4 raindropData = volumetric ? vRaindrop : texture2D(raindropState, uv);
vec4 symbolData = volumetric ? vSymbol : texture2D( symbolState, uv);
vec4 effectData = volumetric ? vEffect : texture2D( effectState, uv);
vec4 data = texture2D(computeState, uv);
vec3 brightness = getBrightness(
raindropData,
effectData,
vDepth,
uv
);
vec2 symbol = getSymbol(uv, symbolData.r);
vec3 brightness = getBrightness(data.rg, uv);
vec2 symbol = getSymbol(uv, data.b);
if (showDebugView) {
gl_FragColor = vec4(
vec3(
raindropData.g,
vec2(
1. - ((1.0 - raindropData.r) * 3.),
1. - ((1.0 - raindropData.r) * 8.)
) * (1. - raindropData.g)
) * symbol.r,
1.
);
} else {
gl_FragColor = vec4(brightness.rg * symbol.r, brightness.b * symbol.g, 0.);
}
gl_FragColor = vec4(brightness.rg * symbol.r, brightness.b * symbol.g, 0.);
}

View File

@@ -1,67 +0,0 @@
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(2., 0., 0., 0.);
}
float columnTimeOffset;
int column = int(glyphPos.x);
if (column == int(numColumns / 2.)) {
columnTimeOffset = -1.;
} else if (column == int(numColumns * 0.75)) {
columnTimeOffset = -2.;
} else {
columnTimeOffset = randomFloat(vec2(glyphPos.x, 0.)) * -4.;
columnTimeOffset += (sin(glyphPos.x / numColumns * PI) - 1.) * 2. - 2.5;
}
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);
}

View File

@@ -1,93 +0,0 @@
precision highp float;
// This shader is the star of the show.
// It writes falling rain to the channels of a data texture:
// R: raindrop brightness
// G: whether the cell is a "cursor"
// B: whether the cell is "activated" — to animate the intro
// A: unused
// 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 SQRT_2 1.4142135623730951
#define SQRT_5 2.23606797749979
uniform sampler2D previousRaindropState, introState;
uniform float numColumns, numRows;
uniform float time, tick;
uniform float animationSpeed, fallSpeed;
uniform bool loops, skipIntro;
uniform float brightnessDecay;
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);
}
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);
}
// 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.
// It's also why those bright areas are truncated into raindrops.
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;
if (loops) {
columnSpeedOffset = 0.5;
}
float columnTime = columnTimeOffset + simTime * fallSpeed * columnSpeedOffset;
float rainTime = (glyphPos.y * 0.01 + columnTime) / raindropLength;
if (!loops) {
rainTime = wobble(rainTime);
}
return 1.0 - fract(rainTime);
}
// Main function
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 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) {
float previousBrightness = previous.r;
brightness = mix(previousBrightness, brightness, brightnessDecay);
}
vec4 result = vec4(brightness, cursor, activated, introProgress);
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( previousRaindropState, screenPos );
vec4 intro = texture2D( introState, vec2(screenPos.x, 0.) );
gl_FragColor = computeResult(simTime, isFirstFrame, glyphPos, previous, intro);
}

View File

@@ -1,64 +0,0 @@
precision highp float;
// This shader governs the glyphs appearing in the rain.
// It writes each glyph's state to the channels of a data texture:
// R: symbol
// G: age
// B: unused
// A: unused
#define PI 3.14159265359
uniform sampler2D previousSymbolState, raindropState;
uniform float numColumns, numRows;
uniform float time, tick, cycleFrameSkip;
uniform float animationSpeed, cycleSpeed;
uniform bool loops, showDebugView;
uniform float glyphSequenceLength;
// 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);
}
// Main function
vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous, vec4 raindrop) {
float previousSymbol = previous.r;
float previousAge = previous.g;
bool resetGlyph = isFirstFrame;
if (loops) {
resetGlyph = resetGlyph || raindrop.r <= 0.;
}
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);
}
}
vec4 result = vec4(symbol, age, 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( previousSymbolState, screenPos );
vec4 raindrop = texture2D( raindropState, screenPos );
gl_FragColor = computeResult(simTime, isFirstFrame, glyphPos, screenPos, previous, raindrop);
}

View File

@@ -1,51 +1,15 @@
#define PI 3.14159265359
precision lowp float;
attribute vec2 aPosition, aCorner;
uniform sampler2D raindropState, symbolState, effectState;
uniform float density;
uniform vec2 quadSize;
uniform float glyphHeightToWidth, glyphVerticalSpacing;
uniform mat4 camera, transform;
uniform float glyphVerticalSpacing;
uniform vec2 screenSize;
uniform float time, animationSpeed, forwardSpeed;
uniform bool volumetric;
uniform float time, animationSpeed;
varying vec2 vUV;
varying vec4 vRaindrop, vSymbol, vEffect;
varying float vDepth;
highp float rand( 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);
}
void main() {
vUV = (aPosition + aCorner) * quadSize;
vRaindrop = texture2D(raindropState, aPosition * quadSize);
vSymbol = texture2D( symbolState, aPosition * quadSize);
vEffect = texture2D( effectState, aPosition * quadSize);
// Calculate the world space position
float quadDepth = 0.0;
if (volumetric) {
float startDepth = rand(vec2(aPosition.x, 0.));
quadDepth = fract(startDepth + time * animationSpeed * forwardSpeed);
vDepth = quadDepth;
}
vec2 position = (aPosition * vec2(1., glyphVerticalSpacing) + aCorner * vec2(density, 1.)) * quadSize;
if (volumetric) {
position.y += rand(vec2(aPosition.x, 1.)) * quadSize.y;
}
vec4 pos = vec4((position - 0.5) * 2.0, quadDepth, 1.0);
// Convert the world space position to screen space
if (volumetric) {
pos.x /= glyphHeightToWidth;
pos = camera * transform * pos;
} else {
pos.xy *= screenSize;
}
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;
}