Matrix React component 1.0.0

This commit is contained in:
nohren
2023-08-07 02:03:46 -07:00
parent 5ba9049045
commit 3bc0d5d346
24 changed files with 9039 additions and 1236 deletions

View File

@@ -1,4 +1,9 @@
import { loadText, makePassFBO, makePass } from "./utils.js";
import { makePassFBO, makePass } from "./utils";
import highPassFrag from '../../shaders/glsl/bloomPass.highPass.frag.glsl';
import blurFrag from '../../shaders/glsl/bloomPass.blur.frag.glsl';
import combineFrag from '../../shaders/glsl/bloomPass.combine.frag.glsl';
import fsVert from '../../shaders/glsl/bloomPass.vert.glsl';
// The bloom pass is basically an added high-pass blur.
// The blur approximation is the sum of a pyramid of downscaled, blurred textures.
@@ -8,98 +13,136 @@ const pyramidHeight = 5;
// A pyramid is just an array of FBOs, where each FBO is half the width
// and half the height of the FBO below it.
const makePyramid = (regl, height, halfFloat) =>
Array(height)
.fill()
.map((_) => makePassFBO(regl, halfFloat));
Array(height)
.fill()
.map((_) => makePassFBO(regl, halfFloat));
const resizePyramid = (pyramid, vw, vh, scale) =>
pyramid.forEach((fbo, index) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index)));
pyramid.forEach((fbo, index) =>
fbo.resize(
Math.floor((vw * scale) / 2 ** index),
Math.floor((vh * scale) / 2 ** index)
)
);
export default ({ regl, config }, inputs) => {
const { bloomStrength, bloomSize, highPassThreshold } = config;
const enabled = bloomSize > 0 && bloomStrength > 0;
const { bloomStrength, bloomSize, highPassThreshold } = config;
const enabled = bloomSize > 0 && bloomStrength > 0;
// If there's no bloom to apply, return a no-op pass with an empty bloom texture
if (!enabled) {
return makePass({
primary: inputs.primary,
bloom: makePassFBO(regl),
});
}
// If there's no bloom to apply, return a no-op pass with an empty bloom texture
if (!enabled) {
return makePass({
primary: inputs.primary,
bloom: makePassFBO(regl),
});
}
// Build three pyramids of FBOs, one for each step in the process
const highPassPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
const hBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
const vBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
const output = makePassFBO(regl, config.useHalfFloat);
// Build three pyramids of FBOs, one for each step in the process
const highPassPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
const hBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
const vBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
const output = makePassFBO(regl, config.useHalfFloat);
// The high pass restricts the blur to bright things in our input texture.
const highPassFrag = loadText("shaders/glsl/bloomPass.highPass.frag.glsl");
const highPass = regl({
frag: regl.prop("frag"),
uniforms: {
highPassThreshold,
tex: regl.prop("tex"),
},
framebuffer: regl.prop("fbo"),
});
// one big triangle that covers the whole screen
const fullScreenTriangle = regl.buffer([
-1, -1,
3, -1,
-1, 3,
]);
const commonDrawProps = {
attributes: { aPosition: fullScreenTriangle },
count: 3,
};
// The high pass restricts the blur to bright things in our input texture.
const highPass = regl({
...commonDrawProps,
vert: fsVert,
frag: regl.prop("frag"),
uniforms: {
highPassThreshold,
tex: regl.prop("tex"),
},
framebuffer: regl.prop("fbo"),
});
// A 2D gaussian blur is just a 1D blur done horizontally, then done vertically.
// The FBO pyramid's levels represent separate levels of detail;
// by blurring them all, this basic blur approximates a more complex gaussian:
// https://web.archive.org/web/20191124072602/https://software.intel.com/en-us/articles/compute-shader-hdr-and-bloom
const blurFrag = loadText("shaders/glsl/bloomPass.blur.frag.glsl");
const blur = regl({
frag: regl.prop("frag"),
uniforms: {
tex: regl.prop("tex"),
direction: regl.prop("direction"),
height: regl.context("viewportWidth"),
width: regl.context("viewportHeight"),
},
framebuffer: regl.prop("fbo"),
});
// The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture
const combineFrag = loadText("shaders/glsl/bloomPass.combine.frag.glsl");
const combine = regl({
frag: regl.prop("frag"),
uniforms: {
bloomStrength,
...Object.fromEntries(vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])),
},
framebuffer: output,
});
return makePass(
{
primary: inputs.primary,
bloom: output,
},
Promise.all([highPassFrag.loaded, blurFrag.loaded]),
(w, h) => {
// The blur pyramids can be lower resolution than the screen.
resizePyramid(highPassPyramid, w, h, bloomSize);
resizePyramid(hBlurPyramid, w, h, bloomSize);
resizePyramid(vBlurPyramid, w, h, bloomSize);
output.resize(w, h);
},
(shouldRender) => {
if (!shouldRender) {
return;
}
// A 2D gaussian blur is just a 1D blur done horizontally, then done vertically.
// The FBO pyramid's levels represent separate levels of detail;
// by blurring them all, this basic blur approximates a more complex gaussian:
// https://web.archive.org/web/20191124072602/https://software.intel.com/en-us/articles/compute-shader-hdr-and-bloom
for (let i = 0; i < pyramidHeight; i++) {
const highPassFBO = highPassPyramid[i];
const hBlurFBO = hBlurPyramid[i];
const vBlurFBO = vBlurPyramid[i];
highPass({ fbo: highPassFBO, frag: highPassFrag.text(), tex: i === 0 ? inputs.primary : highPassPyramid[i - 1] });
blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] });
blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] });
}
const blur = regl({
...commonDrawProps,
vert: fsVert,
frag: regl.prop("frag"),
uniforms: {
tex: regl.prop("tex"),
direction: regl.prop("direction"),
height: regl.context("viewportWidth"),
width: regl.context("viewportHeight"),
},
framebuffer: regl.prop("fbo"),
});
combine({ frag: combineFrag.text() });
}
);
// The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture
const combine = regl({
...commonDrawProps,
vert: fsVert,
frag: regl.prop("frag"),
uniforms: {
bloomStrength,
...Object.fromEntries(
vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])
),
},
framebuffer: output,
});
return makePass(
{
primary: inputs.primary,
bloom: output,
},
// Promise.all([highPassFrag.loaded, blurFrag.loaded]),
(w, h) => {
// The blur pyramids can be lower resolution than the screen.
resizePyramid(highPassPyramid, w, h, bloomSize);
resizePyramid(hBlurPyramid, w, h, bloomSize);
resizePyramid(vBlurPyramid, w, h, bloomSize);
output.resize(w, h);
},
(shouldRender) => {
if (!shouldRender) {
return;
}
for (let i = 0; i < pyramidHeight; i++) {
const highPassFBO = highPassPyramid[i];
const hBlurFBO = hBlurPyramid[i];
const vBlurFBO = vBlurPyramid[i];
highPass({
fbo: highPassFBO,
frag: highPassFrag,
tex: i === 0 ? inputs.primary : highPassPyramid[i - 1],
});
blur({
fbo: hBlurFBO,
frag: blurFrag,
tex: highPassFBO,
direction: [1, 0],
});
blur({
fbo: vBlurFBO,
frag: blurFrag,
tex: hBlurFBO,
direction: [0, 1],
});
}
combine({ frag: combineFrag });
}
);
};

View File

@@ -1,4 +1,5 @@
import { loadImage, loadText, makePassFBO, makePass } from "./utils.js";
import imagePassFrag from "../../shaders/glsl/imagePass.frag.glsl";
// Multiplies the rendered rain and bloom by a loaded in image
@@ -8,7 +9,6 @@ export default ({ regl, config }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat);
const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL;
const background = loadImage(regl, bgURL);
const imagePassFrag = loadText("shaders/glsl/imagePass.frag.glsl");
const render = regl({
frag: regl.prop("frag"),
uniforms: {
@@ -22,11 +22,11 @@ export default ({ regl, config }, inputs) => {
{
primary: output,
},
Promise.all([background.loaded, imagePassFrag.loaded]),
Promise.all([background.loaded]),
(w, h) => output.resize(w, h),
(shouldRender) => {
if (shouldRender) {
render({ frag: imagePassFrag.text() });
render({ frag: imagePassFrag });
}
}
);

View File

@@ -1,89 +1,101 @@
// import HoloPlayCore from "holoplay-core";
const HoloPlayCore = require("holoplay-core");
const recordedDevice = {
buttons: [0, 0, 0, 0],
calibration: {
DPI: { value: 324 },
center: { value: 0.15018756687641144 },
configVersion: "3.0",
flipImageX: { value: 0 },
flipImageY: { value: 0 },
flipSubp: { value: 0 },
fringe: { value: 0 },
invView: { value: 1 },
pitch: { value: 52.58013153076172 },
screenH: { value: 2048 },
screenW: { value: 1536 },
slope: { value: -7.145165920257568 },
verticalAngle: { value: 0 },
viewCone: { value: 40 },
},
defaultQuilt: {
quiltAspect: 0.75,
quiltX: 3840,
quiltY: 3840,
tileX: 8,
tileY: 6,
},
hardwareVersion: "portrait",
hwid: "LKG-P11063",
index: 0,
joystickIndex: -1,
state: "ok",
unityIndex: 1,
windowCoords: [1440, 900],
buttons: [0, 0, 0, 0],
calibration: {
DPI: { value: 324 },
center: { value: 0.15018756687641144 },
configVersion: "3.0",
flipImageX: { value: 0 },
flipImageY: { value: 0 },
flipSubp: { value: 0 },
fringe: { value: 0 },
invView: { value: 1 },
pitch: { value: 52.58013153076172 },
screenH: { value: 2048 },
screenW: { value: 1536 },
slope: { value: -7.145165920257568 },
verticalAngle: { value: 0 },
viewCone: { value: 40 },
},
defaultQuilt: {
quiltAspect: 0.75,
quiltX: 3840,
quiltY: 3840,
tileX: 8,
tileY: 6,
},
hardwareVersion: "portrait",
hwid: "LKG-P11063",
index: 0,
joystickIndex: -1,
state: "ok",
unityIndex: 1,
windowCoords: [1440, 900],
};
const interpretDevice = (device) => {
if (device == null) {
return { enabled: false, tileX: 1, tileY: 1 };
}
if (device == null) {
return { enabled: false, tileX: 1, tileY: 1 };
}
const fov = 15;
const fov = 15;
const calibration = Object.fromEntries(
Object.entries(device.calibration)
.map(([key, value]) => [key, value.value])
.filter(([key, value]) => value != null)
);
const calibration = Object.fromEntries(
Object.entries(device.calibration)
.map(([key, value]) => [key, value.value])
.filter(([key, value]) => value != null)
);
const screenInches = calibration.screenW / calibration.DPI;
const pitch = calibration.pitch * screenInches * Math.cos(Math.atan(1.0 / calibration.slope));
const tilt = (calibration.screenH / (calibration.screenW * calibration.slope)) * -(calibration.flipImageX * 2 - 1);
const subp = 1 / (calibration.screenW * 3);
const screenInches = calibration.screenW / calibration.DPI;
const pitch =
calibration.pitch *
screenInches *
Math.cos(Math.atan(1.0 / calibration.slope));
const tilt =
(calibration.screenH / (calibration.screenW * calibration.slope)) *
-(calibration.flipImageX * 2 - 1);
const subp = 1 / (calibration.screenW * 3);
const defaultQuilt = device.defaultQuilt;
const defaultQuilt = device.defaultQuilt;
const quiltViewPortion = [
(Math.floor(defaultQuilt.quiltX / defaultQuilt.tileX) * defaultQuilt.tileX) / defaultQuilt.quiltX,
(Math.floor(defaultQuilt.quiltY / defaultQuilt.tileY) * defaultQuilt.tileY) / defaultQuilt.quiltY,
];
const quiltViewPortion = [
(Math.floor(defaultQuilt.quiltX / defaultQuilt.tileX) *
defaultQuilt.tileX) /
defaultQuilt.quiltX,
(Math.floor(defaultQuilt.quiltY / defaultQuilt.tileY) *
defaultQuilt.tileY) /
defaultQuilt.quiltY,
];
return {
...defaultQuilt,
...calibration,
pitch,
tilt,
subp,
return {
...defaultQuilt,
...calibration,
pitch,
tilt,
subp,
quiltViewPortion,
fov,
enabled: true,
};
quiltViewPortion,
fov,
enabled: true,
};
};
export default async (useHoloplay = false, useRecordedDevice = false) => {
if (!useHoloplay) {
return interpretDevice(null);
}
const HoloPlayCore = await import("../../lib/holoplaycore.module.js");
const device = await new Promise(
(resolve, reject) =>
new HoloPlayCore.Client(
(data) => resolve(data.devices?.[0]),
(error) => resolve(null)
)
);
if (device == null && useRecordedDevice) {
return interpretDevice(recordedDevice);
}
return interpretDevice(device);
if (!useHoloplay) {
return interpretDevice(null);
}
// const HoloPlayCore = await import("../../lib/holoplaycore.module.js");
const device = await new Promise(
(resolve, reject) =>
new HoloPlayCore.Client(
(data) => resolve(data.devices?.[0]),
(error) => resolve(null)
)
);
if (device == null && useRecordedDevice) {
return interpretDevice(recordedDevice);
}
return interpretDevice(device);
};

View File

@@ -1,5 +1,5 @@
import { makeFullScreenQuad, makePipeline } from "./utils.js";
import createREGL from "regl";
import makeRain from "./rainPass.js";
import makeBloomPass from "./bloomPass.js";
import makePalettePass from "./palettePass.js";
@@ -7,126 +7,166 @@ import makeStripePass from "./stripePass.js";
import makeImagePass from "./imagePass.js";
import makeQuiltPass from "./quiltPass.js";
import makeMirrorPass from "./mirrorPass.js";
import { setupCamera, cameraCanvas, cameraAspectRatio } from "../camera.js";
import {
setupCamera,
cameraCanvas,
cameraAspectRatio,
} from "../utils/camera.js";
import getLKG from "./lkgHelper.js";
const effects = {
none: null,
plain: makePalettePass,
palette: makePalettePass,
customStripes: makeStripePass,
stripes: makeStripePass,
pride: makeStripePass,
transPride: makeStripePass,
trans: makeStripePass,
image: makeImagePass,
mirror: makeMirrorPass,
none: null,
plain: makePalettePass,
palette: makePalettePass,
customStripes: makeStripePass,
stripes: makeStripePass,
pride: makeStripePass,
transPride: makeStripePass,
trans: makeStripePass,
image: makeImagePass,
mirror: makeMirrorPass,
};
const dimensions = { width: 1, height: 1 };
const loadJS = (src) =>
new Promise((resolve, reject) => {
const tag = document.createElement("script");
tag.onload = resolve;
tag.onerror = reject;
tag.src = src;
document.body.appendChild(tag);
});
// const loadJS = (src) =>
// new Promise((resolve, reject) => {
// const tag = document.createElement("script");
// tag.onload = resolve;
// tag.onerror = reject;
// tag.src = src;
// document.body.appendChild(tag);
// });
export default async (canvas, config) => {
await Promise.all([loadJS("lib/regl.min.js"), loadJS("lib/gl-matrix.js")]);
// Promise.all([loadJS("lib/regl.min.js"), loadJS("lib/gl-matrix.js")]);
const resize = () => {
const devicePixelRatio = window.devicePixelRatio ?? 1;
canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * config.resolution);
canvas.height = Math.ceil(canvas.clientHeight * devicePixelRatio * config.resolution);
};
window.onresize = resize;
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
window.ondblclick = () => {
if (document.fullscreenElement == null) {
if (canvas.webkitRequestFullscreen != null) {
canvas.webkitRequestFullscreen();
} else {
canvas.requestFullscreen();
}
} else {
document.exitFullscreen();
}
};
}
resize();
export const createRain = async (canvas, config, gl) => {
const resize = () => {
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.ceil(window.innerWidth * dpr * config.resolution);
canvas.height = Math.ceil(window.innerHeight * dpr * config.resolution);
};
if (config.useCamera) {
await setupCamera();
}
window.onresize = resize;
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
window.ondblclick = () => {
if (document.fullscreenElement == null) {
if (canvas.webkitRequestFullscreen != null) {
canvas.webkitRequestFullscreen();
} else {
canvas.requestFullscreen();
}
} else {
document.exitFullscreen();
}
};
}
resize();
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"];
if (config.useCamera) {
await setupCamera();
}
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 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",
];
const regl = createREGL({ canvas, pixelRatio: 1, extensions, optionalExtensions });
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 cameraTex = regl.texture(cameraCanvas);
const lkg = await getLKG(config.useHoloplay, true);
const regl = createREGL({
gl,
pixelRatio: 1,
extensions,
optionalExtensions,
});
// All this takes place in a full screen quad.
const fullScreenQuad = makeFullScreenQuad(regl);
const effectName = config.effect in effects ? config.effect : "palette";
const context = { regl, config, lkg, cameraTex, cameraAspectRatio };
const pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName], makeQuiltPass]);
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
const drawToScreen = regl({ uniforms: screenUniforms });
await Promise.all(pipeline.map((step) => step.ready));
const cameraTex = regl.texture(cameraCanvas);
const lkg = await getLKG(config.useHoloplay, true);
const targetFrameTimeMilliseconds = 1000 / config.fps;
let last = NaN;
// All this takes place in a full screen quad.
const fullScreenQuad = makeFullScreenQuad(regl);
const effectName = config.effect in effects ? config.effect : "palette";
const context = { regl, config, lkg, cameraTex, cameraAspectRatio };
const pipeline = makePipeline(context, [
makeRain,
makeBloomPass,
effects[effectName],
makeQuiltPass,
]);
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
if (config.once) {
tick.cancel();
}
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
const drawToScreen = regl({ uniforms: screenUniforms });
await Promise.all(pipeline.map((step) => step.ready));
pipeline.forEach((step) => step.setSize(canvas.width, canvas.height));
dimensions.width = canvas.width;
dimensions.height = canvas.height;
const now = regl.now() * 1000;
const targetFrameTimeMilliseconds = 1000 / config.fps;
let last = NaN;
if (isNaN(last)) {
last = now;
}
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
if (config.once) {
tick.cancel();
}
const shouldRender = config.fps >= 60 || now - last >= targetFrameTimeMilliseconds || config.once == true;
const now = regl.now() * 1000;
if (shouldRender) {
while (now - targetFrameTimeMilliseconds > last) {
last += targetFrameTimeMilliseconds;
}
}
if (isNaN(last)) {
last = now;
}
if (config.useCamera) {
cameraTex(cameraCanvas);
}
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {
dimensions.width = viewportWidth;
dimensions.height = viewportHeight;
for (const step of pipeline) {
step.setSize(viewportWidth, viewportHeight);
}
}
fullScreenQuad(() => {
for (const step of pipeline) {
step.execute(shouldRender);
}
drawToScreen();
});
});
const shouldRender =
config.fps >= 60 ||
now - last >= targetFrameTimeMilliseconds ||
config.once == true;
if (shouldRender) {
while (now - targetFrameTimeMilliseconds > last) {
last += targetFrameTimeMilliseconds;
}
}
if (config.useCamera) {
cameraTex(cameraCanvas);
}
if (
dimensions.width !== viewportWidth ||
dimensions.height !== viewportHeight
) {
dimensions.width = viewportWidth;
dimensions.height = viewportHeight;
for (const step of pipeline) {
step.setSize(viewportWidth, viewportHeight);
}
}
fullScreenQuad(() => {
for (const step of pipeline) {
step.execute(shouldRender);
}
drawToScreen();
});
});
return { regl, tick, canvas };
};
export const destroyRain = ({ regl, tick, canvas }) => {
tick.cancel(); // stop RAF
regl.destroy(); // release all GPU resources & event listeners
//canvas.remove(); // drop from the DOM
};

View File

@@ -1,4 +1,5 @@
import { loadText, makePassFBO, makePass } from "./utils.js";
import mirrorPassFrag from "../../shaders/glsl/mirrorPass.frag.glsl";
let start;
const numClicks = 5;
@@ -15,7 +16,6 @@ window.onclick = (e) => {
export default ({ regl, config, cameraTex, cameraAspectRatio }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat);
const mirrorPassFrag = loadText("shaders/glsl/mirrorPass.frag.glsl");
const render = regl({
frag: regl.prop("frag"),
uniforms: {
@@ -36,14 +36,14 @@ export default ({ regl, config, cameraTex, cameraAspectRatio }, inputs) => {
{
primary: output,
},
Promise.all([mirrorPassFrag.loaded]),
null, // No async loading, glsl bundled and loaded into memory at document load
(w, h) => {
output.resize(w, h);
aspectRatio = w / h;
},
(shouldRender) => {
if (shouldRender) {
render({ frag: mirrorPassFrag.text() });
render({ frag: mirrorPassFrag });
}
}
);

View File

@@ -1,5 +1,6 @@
import colorToRGB from "../colorToRGB.js";
import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js";
import colorToRGB from "../utils/colorToRGB";
import { make1DTexture, makePassFBO, makePass } from "./utils.js";
import palettePassFrag from "../../shaders/glsl/palettePass.frag.glsl";
// Maps the brightness of the rendered rain and bloom to colors
// in a 1D gradient palette texture generated from the passed-in color sequence
@@ -7,45 +8,47 @@ import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js";
// This shader introduces noise into the renders, to avoid banding
const makePalette = (regl, entries) => {
const PALETTE_SIZE = 2048;
const paletteColors = Array(PALETTE_SIZE);
const PALETTE_SIZE = 2048;
const paletteColors = Array(PALETTE_SIZE);
// Convert HSL gradient into sorted RGB gradient, capping the ends
const sortedEntries = entries
.slice()
.sort((e1, e2) => e1.at - e2.at)
.map((entry) => ({
rgb: colorToRGB(entry.color),
arrayIndex: Math.floor(Math.max(Math.min(1, entry.at), 0) * (PALETTE_SIZE - 1)),
}));
sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 });
sortedEntries.push({
rgb: sortedEntries[sortedEntries.length - 1].rgb,
arrayIndex: PALETTE_SIZE - 1,
});
// Convert HSL gradient into sorted RGB gradient, capping the ends
const sortedEntries = entries
.slice()
.sort((e1, e2) => e1.at - e2.at)
.map((entry) => ({
rgb: colorToRGB(entry.color),
arrayIndex: Math.floor(
Math.max(Math.min(1, entry.at), 0) * (PALETTE_SIZE - 1)
),
}));
sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 });
sortedEntries.push({
rgb: sortedEntries[sortedEntries.length - 1].rgb,
arrayIndex: PALETTE_SIZE - 1,
});
// Interpolate between the sorted RGB entries to generate
// the palette texture data
sortedEntries.forEach((entry, index) => {
paletteColors[entry.arrayIndex] = entry.rgb.slice();
if (index + 1 < sortedEntries.length) {
const nextEntry = sortedEntries[index + 1];
const diff = nextEntry.arrayIndex - entry.arrayIndex;
for (let i = 0; i < diff; i++) {
const ratio = i / diff;
paletteColors[entry.arrayIndex + i] = [
entry.rgb[0] * (1 - ratio) + nextEntry.rgb[0] * ratio,
entry.rgb[1] * (1 - ratio) + nextEntry.rgb[1] * ratio,
entry.rgb[2] * (1 - ratio) + nextEntry.rgb[2] * ratio,
];
}
}
});
// Interpolate between the sorted RGB entries to generate
// the palette texture data
sortedEntries.forEach((entry, index) => {
paletteColors[entry.arrayIndex] = entry.rgb.slice();
if (index + 1 < sortedEntries.length) {
const nextEntry = sortedEntries[index + 1];
const diff = nextEntry.arrayIndex - entry.arrayIndex;
for (let i = 0; i < diff; i++) {
const ratio = i / diff;
paletteColors[entry.arrayIndex + i] = [
entry.rgb[0] * (1 - ratio) + nextEntry.rgb[0] * ratio,
entry.rgb[1] * (1 - ratio) + nextEntry.rgb[1] * ratio,
entry.rgb[2] * (1 - ratio) + nextEntry.rgb[2] * ratio,
];
}
}
});
return make1DTexture(
regl,
paletteColors.map((rgb) => [...rgb, 1])
);
return make1DTexture(
regl,
paletteColors.map((rgb) => [...rgb, 1])
);
};
// The rendered texture's values are mapped to colors in a palette texture.
@@ -55,39 +58,44 @@ const makePalette = (regl, entries) => {
// in screen space.
export default ({ regl, config }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat);
const paletteTex = makePalette(regl, config.palette);
const { backgroundColor, cursorColor, glintColor, cursorIntensity, glintIntensity, ditherMagnitude } = config;
const output = makePassFBO(regl, config.useHalfFloat);
const paletteTex = makePalette(regl, config.palette);
const {
backgroundColor,
cursorColor,
glintColor,
cursorIntensity,
glintIntensity,
ditherMagnitude,
} = config;
const palettePassFrag = loadText("shaders/glsl/palettePass.frag.glsl");
const render = regl({
frag: regl.prop("frag"),
const render = regl({
frag: regl.prop("frag"),
uniforms: {
backgroundColor: colorToRGB(backgroundColor),
cursorColor: colorToRGB(cursorColor),
glintColor: colorToRGB(glintColor),
cursorIntensity,
glintIntensity,
ditherMagnitude,
tex: inputs.primary,
bloomTex: inputs.bloom,
paletteTex,
},
framebuffer: output,
});
uniforms: {
backgroundColor: colorToRGB(backgroundColor),
cursorColor: colorToRGB(cursorColor),
glintColor: colorToRGB(glintColor),
cursorIntensity,
glintIntensity,
ditherMagnitude,
tex: inputs.primary,
bloomTex: inputs.bloom,
paletteTex,
},
framebuffer: output,
});
return makePass(
{
primary: output,
},
palettePassFrag.loaded,
(w, h) => output.resize(w, h),
(shouldRender) => {
if (shouldRender) {
render({ frag: palettePassFrag.text() });
}
}
);
return makePass(
{
primary: output,
},
palettePassFrag.loaded,
(w, h) => output.resize(w, h),
(shouldRender) => {
if (shouldRender) {
render({ frag: palettePassFrag });
}
}
);
};

View File

@@ -1,4 +1,5 @@
import { loadText, makePassFBO, makePass } from "./utils.js";
import { makePassFBO, makePass } from "./utils.js";
import quiltPassFrag from "../../shaders/glsl/quiltPass.frag.glsl";
// Multiplies the rendered rain and bloom by a loaded in image
@@ -10,7 +11,7 @@ export default ({ regl, config, lkg }, inputs) => {
}
const output = makePassFBO(regl, config.useHalfFloat);
const quiltPassFrag = loadText("shaders/glsl/quiltPass.frag.glsl");
const render = regl({
frag: regl.prop("frag"),
uniforms: {
@@ -23,11 +24,11 @@ export default ({ regl, config, lkg }, inputs) => {
{
primary: output,
},
Promise.all([quiltPassFrag.loaded]),
null,
(w, h) => output.resize(w, h),
(shouldRender) => {
if (shouldRender) {
render({ frag: quiltPassFrag.text() });
render({ frag: quiltPassFrag });
}
}
);

View File

@@ -1,10 +1,25 @@
import { loadImage, loadText, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js";
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 extractEntries = (src, keys) =>
Object.fromEntries(
Array.from(Object.entries(src)).filter(([key]) => keys.includes(key))
);
const rippleTypes = {
box: 0,
circle: 1,
box: 0,
circle: 1,
};
// These compute buffers are used to compute the properties of cells in the grid.
@@ -15,13 +30,12 @@ const rippleTypes = {
// 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",
data: Array(width * height * 4).fill(0)
});
makeDoubleBuffer(regl, {
width,
height,
wrapT: "clamp",
type: "half float",
});
const numVerticesPerQuad = 2 * 3;
const tlVert = [0, 0];
@@ -31,290 +45,352 @@ const brVert = [1, 1];
const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert];
export default ({ regl, config, lkg }) => {
const { mat2, mat4, vec2, vec3 } = glMatrix;
// 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 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];
// 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";
// 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 glyphTransform = mat2.fromScaling(mat2.create(), vec2.fromValues(config.glyphFlip ? -1 : 1, 1));
mat2.rotate(glyphTransform, glyphTransform, (config.glyphRotation * Math.PI) / 180);
const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns);
const commonUniforms = {
...extractEntries(config, ["animationSpeed", "glyphHeightToWidth", "glyphSequenceLength", "glyphTextureGridSize"]),
numColumns,
numRows,
showDebugView,
};
const introUniforms = {
...commonUniforms,
...extractEntries(config, ["fallSpeed", "skipIntro"]),
};
const intro = regl({
frag: regl.prop("frag"),
uniforms: {
...introUniforms,
previousIntroState: introDoubleBuffer.back,
},
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,
});
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,
},
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,
});
framebuffer: raindropDoubleBuffer.front,
});
const symbolDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
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,
},
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,
});
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,
},
const effectDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
framebuffer: effectDoubleBuffer.front,
});
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,
},
const quadPositions = Array(numQuadRows)
.fill()
.map((_, y) =>
Array(numQuadColumns)
.fill()
.map((_, x) => Array(numVerticesPerQuad).fill([x, y]))
);
framebuffer: effectDoubleBuffer.front,
});
// 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);
const renderUniforms = {
...commonUniforms,
...extractEntries(config, [
// vertex
"forwardSpeed",
"glyphVerticalSpacing",
// fragment
"baseBrightness",
"baseContrast",
"glintBrightness",
"glintContrast",
"hasBaseTexture",
"hasGlintTexture",
"brightnessThreshold",
"brightnessOverride",
"isolateCursor",
"isolateGlint",
"glyphEdgeCrop",
"isPolar",
]),
glyphTransform,
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"),
const quadPositions = Array(numQuadRows)
.fill()
.map((_, y) =>
Array(numQuadColumns)
.fill()
.map((_, x) => Array(numVerticesPerQuad).fill([x, y]))
);
uniforms: {
...renderUniforms,
// 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"),
raindropState: raindropDoubleBuffer.front,
symbolState: symbolDoubleBuffer.front,
effectState: effectDoubleBuffer.front,
glyphMSDF: glyphMSDF.texture,
glintMSDF: glintMSDF.texture,
baseTexture: baseTexture.texture,
glintTexture: glintTexture.texture,
uniforms: {
...renderUniforms,
msdfPxRange: 4.0,
glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()],
glintMSDFSize: () => [glintMSDF.width(), glintMSDF.height()],
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'),
camera: regl.prop("camera"),
transform: regl.prop("transform"),
screenSize: regl.prop("screenSize"),
},
msdfPxRange: 4.0,
glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()],
glintMSDFSize: () => [glintMSDF.width(), glintMSDF.height()],
viewport: regl.prop("viewport"),
camera: regl.prop("camera"),
transform: regl.prop("transform"),
screenSize: regl.prop("screenSize"),
},
attributes: {
aPosition: quadPositions,
aCorner: Array(numQuads).fill(quadVertices),
},
count: numQuads * numVerticesPerQuad,
viewport: regl.prop("viewport"),
framebuffer: output,
});
attributes: {
aPosition: quadPositions,
aCorner: Array(numQuads).fill(quadVertices),
},
count: numQuads * numVerticesPerQuad,
// Camera and transform math for the volumetric mode
const screenSize = [1, 1];
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();
framebuffer: output,
});
const vantagePoints = [];
// 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();
return makePass(
{
primary: output,
},
Promise.all([
glyphMSDF.loaded,
glintMSDF.loaded,
baseTexture.loaded,
glintTexture.loaded,
rainPassIntro.loaded,
rainPassRaindrop.loaded,
rainPassSymbol.loaded,
rainPassVert.loaded,
rainPassFrag.loaded,
]),
(w, h) => {
output.resize(w, h);
const aspectRatio = w / h;
const vantagePoints = [];
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();
return makePass(
{
primary: output,
},
Promise.all([
glyphMSDF.loaded,
glintMSDF.loaded,
baseTexture.loaded,
glintTexture.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 if (lkg.enabled) {
mat4.perspective(camera, (Math.PI / 180) * lkg.fov, lkg.quiltAspect, 0.0001, 1000);
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();
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);
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
);
mat4.translate(camera, camera, vec3.fromValues(xOffset, 0, 0));
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);
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);
}
mat4.translate(camera, camera, vec3.fromValues(xOffset, 0, 0));
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.text() });
raindrop({ frag: rainPassRaindrop.text() });
symbol({ frag: rainPassSymbol.text() });
effect({ frag: rainPassEffect.text() });
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
);
}
if (shouldRender) {
regl.clear({
depth: 1,
color: [0, 0, 0, 1],
framebuffer: output,
});
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 });
for (const vantagePoint of vantagePoints) {
render({ ...vantagePoint, transform, screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() });
}
}
}
);
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]
});
}
}
}
);
};

View File

@@ -1,5 +1,6 @@
import colorToRGB from "../colorToRGB.js";
import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js";
import colorToRGB from "../utils/colorToRGB";
import { make1DTexture, makePassFBO, makePass } from "./utils";
import stripePassFrag from "../../shaders/glsl/stripePass.frag.glsl";
// Multiplies the rendered rain and bloom by a 1D gradient texture
// generated from the passed-in color sequence
@@ -7,67 +8,77 @@ import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js";
// This shader introduces noise into the renders, to avoid banding
const transPrideStripeColors = [
{ space: "rgb", values: [0.36, 0.81, 0.98] },
{ space: "rgb", values: [0.96, 0.66, 0.72] },
{ space: "rgb", values: [1.0, 1.0, 1.0] },
{ space: "rgb", values: [0.96, 0.66, 0.72] },
{ space: "rgb", values: [0.36, 0.81, 0.98] },
{ space: "rgb", values: [0.36, 0.81, 0.98] },
{ space: "rgb", values: [0.96, 0.66, 0.72] },
{ space: "rgb", values: [1.0, 1.0, 1.0] },
{ space: "rgb", values: [0.96, 0.66, 0.72] },
{ space: "rgb", values: [0.36, 0.81, 0.98] },
]
.map((color) => Array(3).fill(color))
.flat();
.map((color) => Array(3).fill(color))
.flat();
const prideStripeColors = [
{ space: "rgb", values: [0.89, 0.01, 0.01] },
{ space: "rgb", values: [1.0, 0.55, 0.0] },
{ space: "rgb", values: [1.0, 0.93, 0.0] },
{ space: "rgb", values: [0.0, 0.5, 0.15] },
{ space: "rgb", values: [0.0, 0.3, 1.0] },
{ space: "rgb", values: [0.46, 0.03, 0.53] },
{ space: "rgb", values: [0.89, 0.01, 0.01] },
{ space: "rgb", values: [1.0, 0.55, 0.0] },
{ space: "rgb", values: [1.0, 0.93, 0.0] },
{ space: "rgb", values: [0.0, 0.5, 0.15] },
{ space: "rgb", values: [0.0, 0.3, 1.0] },
{ space: "rgb", values: [0.46, 0.03, 0.53] },
]
.map((color) => Array(2).fill(color))
.flat();
.map((color) => Array(2).fill(color))
.flat();
export default ({ regl, config }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat);
const output = makePassFBO(regl, config.useHalfFloat);
const { backgroundColor, cursorColor, glintColor, cursorIntensity, glintIntensity, ditherMagnitude } = config;
const {
backgroundColor,
cursorColor,
glintColor,
cursorIntensity,
glintIntensity,
ditherMagnitude,
} = config;
// Expand and convert stripe colors into 1D texture data
const stripeColors = "stripeColors" in config ? config.stripeColors : config.effect === "pride" ? prideStripeColors : transPrideStripeColors;
const stripeTex = make1DTexture(
regl,
stripeColors.map((color) => [...colorToRGB(color), 1])
);
// Expand and convert stripe colors into 1D texture data
const stripeColors =
"stripeColors" in config
? config.stripeColors
: config.effect === "pride"
? prideStripeColors
: transPrideStripeColors;
const stripeTex = make1DTexture(
regl,
stripeColors.map((color) => [...colorToRGB(color), 1])
);
const stripePassFrag = loadText("shaders/glsl/stripePass.frag.glsl");
const render = regl({
frag: regl.prop("frag"),
const render = regl({
frag: regl.prop("frag"),
uniforms: {
backgroundColor: colorToRGB(backgroundColor),
cursorColor: colorToRGB(cursorColor),
glintColor: colorToRGB(glintColor),
cursorIntensity,
glintIntensity,
ditherMagnitude,
tex: inputs.primary,
bloomTex: inputs.bloom,
stripeTex,
},
framebuffer: output,
});
uniforms: {
backgroundColor: colorToRGB(backgroundColor),
cursorColor: colorToRGB(cursorColor),
glintColor: colorToRGB(glintColor),
cursorIntensity,
glintIntensity,
ditherMagnitude,
tex: inputs.primary,
bloomTex: inputs.bloom,
stripeTex,
},
framebuffer: output,
});
return makePass(
{
primary: output,
},
stripePassFrag.loaded,
(w, h) => output.resize(w, h),
(shouldRender) => {
if (shouldRender) {
render({ frag: stripePassFrag.text() });
}
}
);
return makePass(
{
primary: output,
},
null,
(w, h) => output.resize(w, h),
(shouldRender) => {
if (shouldRender) {
render({ frag: stripePassFrag });
}
}
);
};