From a48b8dffbe64cab07a3a6d678c77e2e96c26a112 Mon Sep 17 00:00:00 2001 From: Rezmason Date: Sun, 26 Jan 2020 13:53:19 -0800 Subject: [PATCH] Cleaned up config by moving its responsibilities into the passes --- TODO.txt | 33 ++------ js/bloomPass.js | 32 +++++--- js/config.js | 196 +++++++++++++--------------------------------- js/imagePass.js | 6 +- js/main.js | 13 +-- js/palettePass.js | 40 +++++++++- js/renderer.js | 105 +++++++++++++++++-------- js/stripePass.js | 47 +++++++++-- js/utils.js | 6 ++ 9 files changed, 249 insertions(+), 229 deletions(-) diff --git a/TODO.txt b/TODO.txt index 22ecf3f..468d2f1 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,34 +1,11 @@ TODO: -Clean up config.js - Too many responsibilities - Pass-specific properties should be made into uniforms in the passes +Deja vu effect: flashing rows + Make them flash all the time + Then use a thunder-like pattern to show and hide the flash -Reach out to someone about producing sounds +Sounds Raindrop sound - https://youtu.be/bPhu01wpf0k?t=34 - https://youtu.be/Yt2-h13XK7Y?t=33 Deja vu sound - https://youtu.be/XfEuxRDYiyc?t=17 - Square sound - https://youtu.be/ngnlBZNuVb8?t=204 - https://youtu.be/-al0i3zSyj0?t=68 - https://youtu.be/d8My5jIaXoY?t=191 - Neo flying, apparently - https://youtu.be/gZzTVJ-NLYQ?t=230 + ripple sound And some kind of ambient sound that they play over - -Much later: - Deluxe compute variables - Flashing row effect - Neo flying - Staticky julia set looking silhouette - More patterns? - Symbol duplication is common - - - - -Also interesting: - The Matrix code for the Zion Control construct is sparser, slower, bluer, and annotated - https://www.youtube.com/watch?v=Jt5z3OEjDzU diff --git a/js/bloomPass.js b/js/bloomPass.js index 9de7cc3..c7372e4 100644 --- a/js/bloomPass.js +++ b/js/bloomPass.js @@ -1,4 +1,10 @@ -import { makePassFBO, makePyramid, resizePyramid, makePass } from "./utils.js"; +import { + extractEntries, + makePassFBO, + makePyramid, + resizePyramid, + makePass +} from "./utils.js"; // The bloom pass is basically an added high-pass blur. @@ -10,7 +16,14 @@ const levelStrengths = Array(pyramidHeight) ) .reverse(); -export default (regl, { bloomSize }, input) => { +export default (regl, config, input) => { + const uniforms = extractEntries(config, [ + "bloomRadius", + "bloomSize", + "bloomStrength", + "highPassThreshold" + ]); + const highPassPyramid = makePyramid(regl, pyramidHeight); const hBlurPyramid = makePyramid(regl, pyramidHeight); const vBlurPyramid = makePyramid(regl, pyramidHeight); @@ -34,6 +47,7 @@ export default (regl, { bloomSize }, input) => { } `, uniforms: { + ...uniforms, tex: regl.prop("tex") }, framebuffer: regl.prop("fbo") @@ -45,10 +59,10 @@ export default (regl, { bloomSize }, input) => { const blur = regl({ frag: ` precision mediump float; - varying vec2 vUV; + uniform float width, height; uniform sampler2D tex; uniform vec2 direction; - uniform float width, height; + varying vec2 vUV; void main() { vec2 size = width > height ? vec2(width / height, 1.) : vec2(1., height / width); gl_FragColor = @@ -60,6 +74,7 @@ export default (regl, { bloomSize }, input) => { } `, uniforms: { + ...uniforms, tex: regl.prop("tex"), direction: regl.prop("direction"), height: regl.context("viewportWidth"), @@ -90,6 +105,7 @@ export default (regl, { bloomSize }, input) => { } `, uniforms: { + ...uniforms, tex: input, ...Object.fromEntries( vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo]) @@ -98,8 +114,6 @@ export default (regl, { bloomSize }, input) => { framebuffer: output }); - const scale = bloomSize; - return makePass( output, () => { @@ -114,9 +128,9 @@ export default (regl, { bloomSize }, input) => { }, (w, h) => { // The blur pyramids can be lower resolution than the screen. - resizePyramid(highPassPyramid, w, h, scale); - resizePyramid(hBlurPyramid, w, h, scale); - resizePyramid(vBlurPyramid, w, h, scale); + resizePyramid(highPassPyramid, w, h, config.bloomSize); + resizePyramid(hBlurPyramid, w, h, config.bloomSize); + resizePyramid(vBlurPyramid, w, h, config.bloomSize); output.resize(w, h); } ); diff --git a/js/config.js b/js/config.js index 4b13c3a..f97c7cb 100644 --- a/js/config.js +++ b/js/config.js @@ -17,8 +17,10 @@ const fonts = { }; const defaults = { + animationSpeed: 1, bloomRadius: 0.5, bloomStrength: 1, + bloomSize: 0.5, highPassThreshold: 0.3, cycleSpeed: 1, cycleStyleName: "cycleFasterWhenDimmed", @@ -107,7 +109,7 @@ const versions = { { rgb: [1.0, 1.0, 0.9], at: 1.0 } ], raindropLength: 0.6, - slant: 360 / 16 + slant: (22.5 * Math.PI) / 180 }, paradise: { ...defaults, @@ -136,146 +138,56 @@ const versions = { versions.throwback = versions.operator; versions["1999"] = versions.classic; -export default (searchString, make1DTexture) => { - const urlParams = new URLSearchParams(searchString); - const getParam = (keyOrKeys, defaultValue) => { - if (Array.isArray(keyOrKeys)) { - const keys = keyOrKeys; - const key = keys.find(key => urlParams.has(key)); - return key != null ? urlParams.get(key) : defaultValue; - } else { - const key = keyOrKeys; - return urlParams.has(key) ? urlParams.get(key) : defaultValue; - } - }; +const range = (f, min = -Infinity, max = Infinity) => + Math.max(min, Math.min(max, f)); +const nullNaN = f => (isNaN(f) ? null : f); - const versionName = getParam("version", "classic"); - const version = - versions[versionName] == null ? versions.classic : versions[versionName]; - - const config = { ...version }; - - config.animationSpeed = parseFloat(getParam("animationSpeed", 1)); - config.fallSpeed *= parseFloat(getParam("fallSpeed", 1)); - config.cycleSpeed *= parseFloat(getParam("cycleSpeed", 1)); - config.numColumns = parseInt(getParam("width", config.numColumns)); - config.raindropLength = parseFloat( - getParam(["raindropLength", "dropLength"], config.raindropLength) - ); - config.glyphSequenceLength = config.glyphSequenceLength; - config.slant = - (parseFloat(getParam(["slant", "angle"], config.slant)) * Math.PI) / 180; - config.slantVec = [Math.cos(config.slant), Math.sin(config.slant)]; - config.slantScale = - 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1); - config.glyphEdgeCrop = parseFloat(getParam("encroach", config.glyphEdgeCrop)); - config.glyphHeightToWidth = parseFloat( - getParam("stretch", config.glyphHeightToWidth) - ); - config.cursorEffectThreshold = getParam( - "cursorEffectThreshold", - config.cursorEffectThreshold - ); - config.bloomSize = Math.max( - 0.01, - Math.min(1, parseFloat(getParam("bloomSize", 0.5))) - ); - config.effect = getParam("effect", "plain"); - config.bgURL = getParam( - "url", - "https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg" - ); - config.customStripes = getParam( - "colors", - "0.4,0.15,0.1,0.4,0.15,0.1,0.8,0.8,0.6,0.8,0.8,0.6,1.0,0.7,0.8,1.0,0.7,0.8," - ) - .split(",") - .map(parseFloat); - config.showComputationTexture = config.effect === "none"; - - switch (config.cycleStyleName) { - case "cycleFasterWhenDimmed": - config.cycleStyle = 0; - break; - case "cycleRandomly": - default: - config.cycleStyle = 1; - break; - } - - switch (config.rippleTypeName) { - case "box": - config.rippleType = 0; - break; - case "circle": - config.rippleType = 1; - break; - default: - config.rippleType = -1; - } - - const PALETTE_SIZE = 2048; - const paletteColors = Array(PALETTE_SIZE); - const sortedEntries = version.paletteEntries - .slice() - .sort((e1, e2) => e1.at - e2.at) - .map(entry => ({ - rgb: entry.rgb, - 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 - }); - 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 - ]; - } - } - }); - - config.palette = make1DTexture(paletteColors.flat().map(i => i * 0xff)); - - let stripeColors = [0, 0, 0]; - - if (config.effect === "pride") { - config.effect = "stripes"; - config.stripeColors = [ - [1, 0, 0], - [1, 0.5, 0], - [1, 1, 0], - [0, 1, 0], - [0, 0, 1], - [0.8, 0, 1] - ].flat(); - } - - if (config.effect === "customStripes" || config.effect === "stripes") { - config.effect = "stripes"; - const numStripeColors = Math.floor(config.stripeColors.length / 3); - stripeColors = config.stripeColors.slice(0, numStripeColors * 3); - } - - config.stripes = make1DTexture(stripeColors.map(f => Math.floor(f * 0xff))); - - const uniforms = Object.fromEntries( - Object.entries(config).filter(([key, value]) => { - const type = typeof (Array.isArray(value) ? value[0] : value); - return type !== "string" && type !== "object"; - }) - ); - - return [config, uniforms]; +const paramMapping = { + version: { key: "version", parser: s => s }, + effect: { key: "effect", parser: s => s }, + width: { key: "numColumns", parser: s => nullNaN(parseInt(s)) }, + animationSpeed: { + key: "animationSpeed", + parser: s => nullNaN(parseFloat(s)) + }, + cycleSpeed: { key: "cycleSpeed", parser: s => nullNaN(parseFloat(s)) }, + fallSpeed: { key: "fallSpeed", parser: s => nullNaN(parseFloat(s)) }, + raindropLength: { + key: "raindropLength", + parser: s => nullNaN(parseFloat(s)) + }, + slant: { + key: "slant", + parser: s => nullNaN((parseFloat(s) * Math.PI) / 180) + }, + bloomSize: { + key: "bloomSize", + parser: s => nullNaN(range(parseFloat(s), 0.01, 1)) + }, + url: { key: "bgURL", parser: s => s }, + colors: { key: "stripeColors", parser: s => s } +}; +paramMapping.dropLength = paramMapping.raindropLength; +paramMapping.angle = paramMapping.slant; + +export default (searchString, make1DTexture) => { + const urlParams = Object.fromEntries( + Array.from(new URLSearchParams(searchString).entries()) + .filter(([key]) => key in paramMapping) + .map(([key, value]) => [ + paramMapping[key].key, + paramMapping[key].parser(value) + ]) + .filter(([_, value]) => value != null) + ); + + const version = + urlParams.version in versions + ? versions[urlParams.version] + : versions.classic; + + return { + ...version, + ...urlParams + }; }; diff --git a/js/imagePass.js b/js/imagePass.js index f0b43b8..4375e8a 100644 --- a/js/imagePass.js +++ b/js/imagePass.js @@ -1,7 +1,11 @@ import { loadImage, makePassFBO, makePass } from "./utils.js"; -export default (regl, { bgURL }, input) => { +const defaultBGURL = + "https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg"; + +export default (regl, config, input) => { const output = makePassFBO(regl); + const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL; const bgLoader = loadImage(regl, bgURL); return makePass( output, diff --git a/js/main.js b/js/main.js index 4d52f15..03c030f 100644 --- a/js/main.js +++ b/js/main.js @@ -1,4 +1,4 @@ -import { makeFullScreenQuad, make1DTexture, makePipeline } from "./utils.js"; +import { makeFullScreenQuad, makePipeline } from "./utils.js"; import makeConfig from "./config.js"; import makeMatrixRenderer from "./renderer.js"; import makeBloomPass from "./bloomPass.js"; @@ -26,13 +26,13 @@ const regl = createREGL({ const effects = { none: null, plain: makePalettePass, + customStripes: makeStripePass, stripes: makeStripePass, + pride: makeStripePass, image: makeImagePass }; -const [config, uniforms] = makeConfig(window.location.search, data => - make1DTexture(regl, data) -); +const config = makeConfig(window.location.search); const effect = config.effect in effects ? config.effect : "plain"; const resize = () => { @@ -44,7 +44,7 @@ resize(); document.body.onload = async () => { // All this takes place in a full screen quad. - const fullScreenQuad = makeFullScreenQuad(regl, uniforms); + const fullScreenQuad = makeFullScreenQuad(regl); const pipeline = makePipeline( [ makeMatrixRenderer, @@ -61,7 +61,8 @@ document.body.onload = async () => { } }); await Promise.all(pipeline.map(({ ready }) => ready)); - regl.frame(({ viewportWidth, viewportHeight }) => { + const tick = regl.frame(({ viewportWidth, viewportHeight }) => { + // tick.cancel(); pipeline.forEach(({ resize }) => resize(viewportWidth, viewportHeight)); fullScreenQuad(() => { pipeline.forEach(({ render }) => render()); diff --git a/js/palettePass.js b/js/palettePass.js index 2ad6d41..2c7fa63 100644 --- a/js/palettePass.js +++ b/js/palettePass.js @@ -1,4 +1,4 @@ -import { makePassFBO, makePass } from "./utils.js"; +import { make1DTexture, makePassFBO, makePass } from "./utils.js"; // The rendered texture's values are mapped to colors in a palette texture. // A little noise is introduced, to hide the banding that appears @@ -6,8 +6,43 @@ import { makePassFBO, makePass } from "./utils.js"; // won't persist across subsequent frames. This is a safe trick // in screen space. -export default (regl, {}, input) => { +export default (regl, config, input) => { const output = makePassFBO(regl); + + const PALETTE_SIZE = 2048; + const paletteColors = Array(PALETTE_SIZE); + const sortedEntries = config.paletteEntries + .slice() + .sort((e1, e2) => e1.at - e2.at) + .map(entry => ({ + rgb: entry.rgb, + 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 + }); + 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 + ]; + } + } + }); + + const palette = make1DTexture(regl, paletteColors.flat().map(i => i * 0xff)); + return makePass( output, regl({ @@ -35,6 +70,7 @@ export default (regl, {}, input) => { uniforms: { tex: input, + palette, ditherMagnitude: 0.05 }, framebuffer: output diff --git a/js/renderer.js b/js/renderer.js index 453a779..e358c1f 100644 --- a/js/renderer.js +++ b/js/renderer.js @@ -1,4 +1,20 @@ -import { loadImage, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js"; +import { + extractEntries, + loadImage, + makePassFBO, + makeDoubleBuffer, + makePass +} from "./utils.js"; + +const rippleTypes = { + box: 0, + circle: 1 +}; + +const cycleStyles = { + cycleFasterWhenDimmed: 0, + cycleRandomly: 1 +}; export default (regl, config) => { // These two framebuffers are used to compute the raining code. @@ -16,6 +32,47 @@ export default (regl, config) => { const output = makePassFBO(regl); + const uniforms = extractEntries(config, [ + // rain general + "glyphHeightToWidth", + "glyphTextureColumns", + "numColumns", + // rain update + "animationSpeed", + "brightnessMinimum", + "brightnessMix", + "brightnessMultiplier", + "brightnessOffset", + "cursorEffectThreshold", + "cycleSpeed", + "fallSpeed", + "glyphSequenceLength", + "hasSun", + "hasThunder", + "raindropLength", + "rippleScale", + "rippleSpeed", + "rippleThickness", + // rain render + "glyphEdgeCrop", + "isPolar" + ]); + + uniforms.rippleType = + config.rippleTypeName in rippleTypes + ? rippleTypes[config.rippleTypeName] + : -1; + uniforms.cycleStyle = + config.cycleStyleName in cycleStyles + ? cycleStyles[config.cycleStyleName] + : 0; + uniforms.slantVec = [Math.cos(config.slant), Math.sin(config.slant)]; + uniforms.slantScale = + 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1); + uniforms.showComputationTexture = config.effect === "none"; + + const msdfLoader = loadImage(regl, config.glyphTexURL); + // This shader is the star of the show. // In normal operation, each pixel represents a glyph's: // R: brightness @@ -30,34 +87,19 @@ export default (regl, config) => { #define SQRT_2 1.4142135623730951 #define SQRT_5 2.23606797749979 + uniform float time; uniform float numColumns; uniform sampler2D lastState; - uniform bool hasSun; uniform bool hasThunder; uniform bool showComputationTexture; - - uniform float brightnessMinimum; - uniform float brightnessMultiplier; - uniform float brightnessOffset; - uniform float brightnessMix; - - uniform float time; - uniform float animationSpeed; - uniform float cycleSpeed; - uniform float fallSpeed; + uniform float brightnessMinimum, brightnessMultiplier, brightnessOffset, brightnessMix; + uniform float animationSpeed, fallSpeed, cycleSpeed; uniform float raindropLength; - - uniform float glyphHeightToWidth; - uniform float glyphSequenceLength; - uniform float glyphTextureColumns; + uniform float glyphHeightToWidth, glyphSequenceLength, glyphTextureColumns; uniform int cycleStyle; - - uniform float rippleScale; - uniform float rippleSpeed; - uniform float rippleThickness; + uniform float rippleScale, rippleSpeed, rippleThickness; uniform int rippleType; - uniform float cursorEffectThreshold; float max2(vec2 v) { @@ -88,10 +130,10 @@ export default (regl, config) => { float getGlyphCycleSpeed(float rainTime, float brightness) { float glyphCycleSpeed = 0.0; - if (cycleStyle == 1) { - glyphCycleSpeed = fract((rainTime + 0.7 * sin(SQRT_2 * rainTime) + 1.1 * sin(SQRT_5 * rainTime))) * 0.75; - } else if (cycleStyle == 0 && brightness > 0.0) { + if (cycleStyle == 0 && brightness > 0.0) { glyphCycleSpeed = pow(1.0 - brightness, 4.0); + } else if (cycleStyle == 1) { + glyphCycleSpeed = fract((rainTime + 0.7 * sin(SQRT_2 * rainTime) + 1.1 * sin(SQRT_5 * rainTime))) * 0.75; } return glyphCycleSpeed; } @@ -207,20 +249,18 @@ export default (regl, config) => { `, uniforms: { + ...uniforms, lastState: doubleBuffer.back }, framebuffer: doubleBuffer.front }); - const msdfLoader = loadImage(regl, config.glyphTexURL); - // We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen const render = regl({ vert: ` attribute vec2 aPosition; - uniform float width; - uniform float height; + uniform float width, height; varying vec2 vUV; void main() { vUV = aPosition / 2.0 + 0.5; @@ -237,15 +277,11 @@ export default (regl, config) => { #endif precision lowp float; - uniform sampler2D glyphTex; - uniform sampler2D lastState; uniform float numColumns; - uniform float glyphTextureColumns; + uniform sampler2D glyphTex, lastState; + uniform float glyphHeightToWidth, glyphTextureColumns, glyphEdgeCrop; uniform vec2 slantVec; uniform float slantScale; - uniform float glyphHeightToWidth; - uniform float glyphEdgeCrop; - uniform bool isPolar; uniform bool showComputationTexture; @@ -309,6 +345,7 @@ export default (regl, config) => { `, uniforms: { + ...uniforms, glyphTex: msdfLoader.texture, height: regl.context("viewportWidth"), width: regl.context("viewportHeight"), diff --git a/js/stripePass.js b/js/stripePass.js index 32ee70b..e232615 100644 --- a/js/stripePass.js +++ b/js/stripePass.js @@ -1,7 +1,38 @@ -import { makePassFBO, makePass } from "./utils.js"; +import { make1DTexture, makePassFBO, makePass } from "./utils.js"; -export default (regl, {}, input) => { +const neapolitanStripeColors = [ + [0.4, 0.15, 0.1], + [0.4, 0.15, 0.1], + [0.8, 0.8, 0.6], + [0.8, 0.8, 0.6], + [1.0, 0.7, 0.8], + [1.0, 0.7, 0.8] +].flat(); + +const prideStripeColors = [ + [1, 0, 0], + [1, 0.5, 0], + [1, 1, 0], + [0, 1, 0], + [0, 0, 1], + [0.8, 0, 1] +].flat(); + +export default (regl, config, input) => { const output = makePassFBO(regl); + + const stripeColors = + "stripeColors" in config + ? config.stripeColors.split(",").map(parseFloat) + : config.effect === "pride" + ? prideStripeColors + : neapolitanStripeColors; + const numStripeColors = Math.floor(stripeColors.length / 3); + const stripes = make1DTexture( + regl, + stripeColors.slice(0, numStripeColors * 3).map(f => Math.floor(f * 0xff)) + ); + return makePass( output, regl({ @@ -12,24 +43,26 @@ export default (regl, {}, input) => { uniform sampler2D tex; uniform sampler2D stripes; uniform float ditherMagnitude; + uniform float time; varying vec2 vUV; - highp float rand( const in vec2 uv ) { + highp float rand( const in vec2 uv, const in float t ) { 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); + return fract(sin(sn) * c + t); } void main() { - vec3 color = texture2D(stripes, vUV).rgb - rand( gl_FragCoord.xy ) * ditherMagnitude; - float brightness = texture2D(tex, vUV).r; + vec3 color = texture2D(stripes, vUV).rgb; + float brightness = texture2D(tex, vUV).r - rand( gl_FragCoord.xy, time ) * ditherMagnitude; gl_FragColor = vec4(color * brightness, 1.0); } `, uniforms: { tex: input, - ditherMagnitude: 0.1 + stripes, + ditherMagnitude: 0.05 }, framebuffer: output }) diff --git a/js/utils.js b/js/utils.js index 8084bd1..53f811c 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,3 +1,8 @@ +const extractEntries = (src, keys) => + Object.fromEntries( + Array.from(Object.entries(src)).filter(([key]) => keys.includes(key)) + ); + const makePassTexture = regl => regl.texture({ width: 1, @@ -167,6 +172,7 @@ const makePipeline = (steps, getInput, ...params) => ); export { + extractEntries, makePassTexture, makePassFBO, makeDoubleBuffer,