Removed sun shower. Thunder and ripples are now handled by a third compute shader.

This commit is contained in:
Rezmason
2022-09-14 22:57:39 -07:00
parent 0d1d661401
commit 4c6e6fd662
9 changed files with 163 additions and 121 deletions

View File

@@ -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)

View File

@@ -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 },

View File

@@ -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);

View File

@@ -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],

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;