Files
matrix/js/regl/rainPass.js
2025-04-24 08:54:03 -07:00

397 lines
11 KiB
JavaScript

import {
loadImage,
makePassFBO,
makeDoubleBuffer,
makePass,
} from "./utils.js";
import { mat4, vec3 } from "gl-matrix";
import rainPassIntro from "../../shaders/glsl/rainPass.intro.frag.glsl";
import rainPassRaindrop from "../../shaders/glsl/rainPass.raindrop.frag.glsl";
import rainPassSymbol from "../../shaders/glsl/rainPass.symbol.frag.glsl";
import rainPassEffect from "../../shaders/glsl/rainPass.effect.frag.glsl";
import rainPassVert from "../../shaders/glsl/rainPass.vert.glsl";
import rainPassFrag from "../../shaders/glsl/rainPass.frag.glsl";
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,
// 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];
const blVert = [1, 0];
const brVert = [1, 1];
const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert];
export default ({ regl, config, lkg }) => {
// 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 commonUniforms = {
...extractEntries(config, [
"animationSpeed",
"glyphHeightToWidth",
"glyphSequenceLength",
"glyphTextureGridSize",
]),
numColumns,
numRows,
showDebugView,
};
const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns);
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 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 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 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)
.fill()
.map((_, y) =>
Array(numQuadColumns)
.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 output = makePassFBO(regl, config.useHalfFloat);
const renderUniforms = {
...commonUniforms,
...extractEntries(config, [
// vertex
"forwardSpeed",
"glyphVerticalSpacing",
// fragment
"baseBrightness",
"baseContrast",
"glintBrightness",
"glintContrast",
"hasBaseTexture",
"hasGlintTexture",
"brightnessThreshold",
"brightnessOverride",
"isolateCursor",
"isolateGlint",
"glyphEdgeCrop",
"isPolar",
]),
density,
numQuadColumns,
numQuadRows,
quadSize,
slantScale,
slantVec,
volumetric,
};
const render = regl({
blend: {
enable: true,
func: {
src: "one",
dst: "one",
},
},
vert: regl.prop("vert"),
frag: regl.prop("frag"),
uniforms: {
...renderUniforms,
raindropState: raindropDoubleBuffer.front,
symbolState: symbolDoubleBuffer.front,
effectState: effectDoubleBuffer.front,
glyphMSDF: glyphMSDF.texture,
glintMSDF: glintMSDF.texture,
baseTexture: baseTexture.texture,
glintTexture: glintTexture.texture,
glyphTransform: regl.prop('glyphTransform'),
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"),
},
viewport: regl.prop("viewport"),
attributes: {
aPosition: quadPositions,
aCorner: Array(numQuads).fill(quadVertices),
},
count: numQuads * 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 if (lkg.enabled) {
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1.1));
mat4.scale(transform, transform, vec3.fromValues(1, 1, 1));
mat4.scale(transform, transform, vec3.fromValues(0.15, 0.15, 0.15));
} else {
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
}
const camera = mat4.create();
const vantagePoints = [];
return makePass(
{
primary: output,
},
Promise.all([
glyphMSDF.loaded,
glintMSDF.loaded,
baseTexture.loaded,
glintTexture.loaded,
]),
(w, h) => {
output.resize(w, h);
const aspectRatio = w / h;
const [numTileColumns, numTileRows] = [lkg.tileX, lkg.tileY];
const numVantagePoints = numTileRows * numTileColumns;
const tileWidth = Math.floor(w / numTileColumns);
const tileHeight = Math.floor(h / numTileRows);
vantagePoints.length = 0;
for (let row = 0; row < numTileRows; row++) {
for (let column = 0; column < numTileColumns; column++) {
const index = column + row * numTileColumns;
const camera = mat4.create();
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 if (lkg.enabled) {
mat4.perspective(
camera,
(Math.PI / 180) * lkg.fov,
lkg.quiltAspect,
0.0001,
1000
);
const distanceToTarget = -1; // TODO: Get from somewhere else
let vantagePointAngle =
(Math.PI / 180) *
lkg.viewCone *
(index / (numVantagePoints - 1) - 0.5);
if (isNaN(vantagePointAngle)) {
vantagePointAngle = 0;
}
const xOffset = distanceToTarget * Math.tan(vantagePointAngle);
mat4.translate(camera, camera, vec3.fromValues(xOffset, 0, 0));
camera[8] =
-xOffset /
(distanceToTarget *
Math.tan((Math.PI / 180) * 0.5 * lkg.fov) *
lkg.quiltAspect); // Is this right??
} else {
mat4.perspective(
camera,
(Math.PI / 180) * 90,
aspectRatio,
0.0001,
1000
);
}
const viewport = {
x: column * tileWidth,
y: row * tileHeight,
width: tileWidth,
height: tileHeight,
};
vantagePoints.push({ camera, viewport });
}
}
[screenSize[0], screenSize[1]] =
aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
},
(shouldRender) => {
intro({ frag: rainPassIntro });
raindrop({ frag: rainPassRaindrop });
symbol({ frag: rainPassSymbol });
effect({ frag: rainPassEffect });
if (shouldRender) {
regl.clear({
depth: 1,
color: [0, 0, 0, 1],
framebuffer: output,
});
for (const vantagePoint of vantagePoints) {
render({
...vantagePoint,
transform,
screenSize,
vert: rainPassVert,
frag: rainPassFrag,
glyphTransform: [1, 0, 0, 1]
});
}
}
}
);
};