From b86b97fde9ad083cb6cf7aa900747732f7ca6525 Mon Sep 17 00:00:00 2001 From: Rezmason Date: Mon, 3 Oct 2022 23:45:56 -0700 Subject: [PATCH] Adding an FPS argument. The renderers now determine whether the current frame should be rendered, and passes use that to determine whether to render or not. The rain pass, however, will still update the simulation at full speed. --- README.md | 2 +- js/config.js | 3 +++ js/regl/bloomPass.js | 6 +++++- js/regl/imagePass.js | 6 +++++- js/regl/main.js | 21 ++++++++++++++++++++- js/regl/mirrorPass.js | 6 +++++- js/regl/palettePass.js | 6 +++++- js/regl/quiltPass.js | 6 +++++- js/regl/rainPass.js | 19 +++++++++++-------- js/regl/stripePass.js | 6 +++++- js/webgpu/bloomPass.js | 6 +++++- js/webgpu/endPass.js | 6 +++++- js/webgpu/imagePass.js | 6 +++++- js/webgpu/main.js | 16 +++++++++++++++- js/webgpu/mirrorPass.js | 6 +++++- js/webgpu/palettePass.js | 6 +++++- js/webgpu/rainPass.js | 18 ++++++++++-------- js/webgpu/stripePass.js | 6 +++++- js/webgpu/utils.js | 6 +++--- 19 files changed, 123 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index ebb5162..efedc55 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Now you know link fu. Here's a list of customization options: - `paletteHSL`, `stripeHSL`, `backgroundHSL`, `cursorHSL`, and `glintHSL` — the same as the above, except they use *H,S,L* (hue, saturation, lightness) instead of *R,G,B*. - `url` - if you set the effect to "image", this is how you specify which image to load. It doesn't work with any URL; I suggest grabbing them from Wikipedia: [https://rezmason.github.io/matrix/?effect=image&url=https://upload.wikimedia.org/wikipedia/commons/f/f5/EagleRock.jpg](https://rezmason.github.io/matrix/?effect=image&url=https://upload.wikimedia.org/wikipedia/commons/f/f5/EagleRock.jpg) - `loops` - (WIP) if set to "true", this causes the effect to loop, so that it can be converted into a looping video. - +- `fps` — the framerate of the effect. Can be any number between 0 and 60. Default is 60. ## Troubleshooting diff --git a/js/config.js b/js/config.js index 9bcbff7..b5973b8 100644 --- a/js/config.js +++ b/js/config.js @@ -75,6 +75,7 @@ const defaults = { glintColor: { space: "rgb", values: [1, 1, 1] }, // The color of the glint volumetric: false, // A mode where the raindrops appear in perspective animationSpeed: 1, // The global rate that all animations progress + fps: 60, // The target frame rate (frames per second) of the effect forwardSpeed: 0.25, // The speed volumetric rain approaches the eye bloomStrength: 0.7, // The intensity of the bloom bloomSize: 0.4, // The amount the bloom calculation is scaled @@ -117,6 +118,7 @@ const defaults = { useHoloplay: false, loops: false, skipIntro: true, + testFix: null, }; const versions = { @@ -457,6 +459,7 @@ const paramMapping = { volumetric: { key: "volumetric", parser: (s) => s.toLowerCase().includes("true") }, loops: { key: "loops", parser: (s) => s.toLowerCase().includes("true") }, + fps: { key: "fps", parser: (s) => nullNaN(range(parseFloat(s), 0, 60)) }, skipIntro: { key: "skipIntro", parser: (s) => s.toLowerCase().includes("true") }, renderer: { key: "renderer", parser: (s) => s }, once: { key: "once", parser: (s) => s.toLowerCase().includes("true") }, diff --git a/js/regl/bloomPass.js b/js/regl/bloomPass.js index 4bd6d83..085368a 100644 --- a/js/regl/bloomPass.js +++ b/js/regl/bloomPass.js @@ -98,7 +98,11 @@ export default ({ regl, config }, inputs) => { 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]; diff --git a/js/regl/imagePass.js b/js/regl/imagePass.js index 783101d..fb3765a 100644 --- a/js/regl/imagePass.js +++ b/js/regl/imagePass.js @@ -26,6 +26,10 @@ export default ({ regl, config }, inputs) => { }, Promise.all([background.loaded, imagePassFrag.loaded]), (w, h) => output.resize(w, h), - () => render({ frag: imagePassFrag.text() }) + (shouldRender) => { + if (shouldRender) { + render({ frag: imagePassFrag.text() }); + } + } ); }; diff --git a/js/regl/main.js b/js/regl/main.js index 18f3248..df5b876 100644 --- a/js/regl/main.js +++ b/js/regl/main.js @@ -88,10 +88,29 @@ export default async (canvas, config) => { const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary }; const drawToScreen = regl({ uniforms: screenUniforms }); await Promise.all(pipeline.map((step) => step.ready)); + + const targetFrameTimeMilliseconds = 1000 / config.fps; + let last = NaN; + const tick = regl.frame(({ viewportWidth, viewportHeight }) => { if (config.once) { tick.cancel(); } + + const now = regl.now() * 1000; + + if (isNaN(last)) { + last = now; + } + + const shouldRender = config.fps >= 60 || now - last >= targetFrameTimeMilliseconds || config.once == true; + + if (shouldRender) { + while (now - targetFrameTimeMilliseconds > last) { + last += targetFrameTimeMilliseconds; + } + } + if (config.useCamera) { cameraTex(cameraCanvas); } @@ -104,7 +123,7 @@ export default async (canvas, config) => { } fullScreenQuad(() => { for (const step of pipeline) { - step.execute(); + step.execute(shouldRender); } drawToScreen(); }); diff --git a/js/regl/mirrorPass.js b/js/regl/mirrorPass.js index d6c54d9..f38f890 100644 --- a/js/regl/mirrorPass.js +++ b/js/regl/mirrorPass.js @@ -41,6 +41,10 @@ export default ({ regl, config, cameraTex, cameraAspectRatio }, inputs) => { output.resize(w, h); aspectRatio = w / h; }, - () => render({ frag: mirrorPassFrag.text() }) + (shouldRender) => { + if (shouldRender) { + render({ frag: mirrorPassFrag.text() }); + } + } ); }; diff --git a/js/regl/palettePass.js b/js/regl/palettePass.js index ab967e1..2a24cc0 100644 --- a/js/regl/palettePass.js +++ b/js/regl/palettePass.js @@ -83,6 +83,10 @@ export default ({ regl, config }, inputs) => { }, palettePassFrag.loaded, (w, h) => output.resize(w, h), - () => render({ frag: palettePassFrag.text() }) + (shouldRender) => { + if (shouldRender) { + render({ frag: palettePassFrag.text() }); + } + } ); }; diff --git a/js/regl/quiltPass.js b/js/regl/quiltPass.js index c9ecf46..e8fd05a 100644 --- a/js/regl/quiltPass.js +++ b/js/regl/quiltPass.js @@ -25,6 +25,10 @@ export default ({ regl, config, lkg }, inputs) => { }, Promise.all([quiltPassFrag.loaded]), (w, h) => output.resize(w, h), - () => render({ frag: quiltPassFrag.text() }) + (shouldRender) => { + if (shouldRender) { + render({ frag: quiltPassFrag.text() }); + } + } ); }; diff --git a/js/regl/rainPass.js b/js/regl/rainPass.js index 6267b1f..8ceb6aa 100644 --- a/js/regl/rainPass.js +++ b/js/regl/rainPass.js @@ -292,19 +292,22 @@ export default ({ regl, config, lkg }) => { } [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() }); - regl.clear({ - depth: 1, - color: [0, 0, 0, 1], - framebuffer: output, - }); - 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.text(), frag: rainPassFrag.text() }); + } } } ); diff --git a/js/regl/stripePass.js b/js/regl/stripePass.js index 0463a71..cdd7084 100644 --- a/js/regl/stripePass.js +++ b/js/regl/stripePass.js @@ -63,6 +63,10 @@ export default ({ regl, config }, inputs) => { }, stripePassFrag.loaded, (w, h) => output.resize(w, h), - () => render({ frag: stripePassFrag.text() }) + (shouldRender) => { + if (shouldRender) { + render({ frag: stripePassFrag.text() }); + } + } ); }; diff --git a/js/webgpu/bloomPass.js b/js/webgpu/bloomPass.js index 9d653b9..06d1b29 100644 --- a/js/webgpu/bloomPass.js +++ b/js/webgpu/bloomPass.js @@ -135,7 +135,11 @@ export default ({ config, device }) => { }; }; - const run = (encoder) => { + const run = (encoder, shouldRender) => { + if (!shouldRender) { + return; + } + const computePass = encoder.beginComputePass(); computePass.setPipeline(blurPipeline); diff --git a/js/webgpu/endPass.js b/js/webgpu/endPass.js index 5df3fb7..5030aad 100644 --- a/js/webgpu/endPass.js +++ b/js/webgpu/endPass.js @@ -49,7 +49,11 @@ export default ({ device, canvasFormat, canvasContext }) => { return null; }; - const run = (encoder) => { + const run = (encoder, shouldRender) => { + if (!shouldRender) { + return; + } + renderPassConfig.colorAttachments[0].view = canvasContext.getCurrentTexture().createView(); const renderPass = encoder.beginRenderPass(renderPassConfig); renderPass.setPipeline(renderPipeline); diff --git a/js/webgpu/imagePass.js b/js/webgpu/imagePass.js index 89c8321..6626ad2 100644 --- a/js/webgpu/imagePass.js +++ b/js/webgpu/imagePass.js @@ -53,7 +53,11 @@ export default ({ config, device }) => { return { primary: output }; }; - const run = (encoder) => { + const run = (encoder, shouldRender) => { + if (!shouldRender) { + return; + } + const computePass = encoder.beginComputePass(); computePass.setPipeline(computePipeline); computePass.setBindGroup(0, computeBindGroup); diff --git a/js/webgpu/main.js b/js/webgpu/main.js index 3415d0e..205d8ed 100644 --- a/js/webgpu/main.js +++ b/js/webgpu/main.js @@ -92,8 +92,10 @@ export default async (canvas, config) => { const effectName = config.effect in effects ? config.effect : "palette"; const pipeline = await makePipeline(context, [makeRain, makeBloomPass, effects[effectName], makeEndPass]); + const targetFrameTimeMilliseconds = 1000 / config.fps; let frames = 0; let start = NaN; + let last = NaN; let outputs; const renderLoop = (now) => { @@ -101,6 +103,17 @@ export default async (canvas, config) => { start = now; } + if (isNaN(last)) { + last = start; + } + + const shouldRender = config.fps >= 60 || now - last >= targetFrameTimeMilliseconds || config.once; + if (shouldRender) { + while (now - targetFrameTimeMilliseconds > last) { + last += targetFrameTimeMilliseconds; + } + } + const devicePixelRatio = window.devicePixelRatio ?? 1; const canvasWidth = canvas.clientWidth * devicePixelRatio; const canvasHeight = canvas.clientHeight * devicePixelRatio; @@ -119,10 +132,11 @@ export default async (canvas, config) => { frames++; const encoder = device.createCommandEncoder(); - pipeline.run(encoder); + pipeline.run(encoder, shouldRender); // Eventually, when WebGPU allows it, we'll remove the endPass and just copy from our pipeline's output to the canvas texture. // encoder.copyTextureToTexture({ texture: outputs?.primary }, { texture: canvasContext.getCurrentTexture() }, canvasSize); device.queue.submit([encoder.finish()]); + if (!config.once) { requestAnimationFrame(renderLoop); } diff --git a/js/webgpu/mirrorPass.js b/js/webgpu/mirrorPass.js index 371b75b..7b8d4b9 100644 --- a/js/webgpu/mirrorPass.js +++ b/js/webgpu/mirrorPass.js @@ -82,7 +82,11 @@ export default ({ config, device, cameraTex, cameraAspectRatio, timeBuffer }) => return { primary: output }; }; - const run = (encoder) => { + const run = (encoder, shouldRender) => { + if (!shouldRender) { + return; + } + if (touchesChanged) { touchesChanged = false; device.queue.writeBuffer(touchBuffer, 0, touchUniforms.toBuffer({ touches })); diff --git a/js/webgpu/palettePass.js b/js/webgpu/palettePass.js index 6861e87..544e210 100644 --- a/js/webgpu/palettePass.js +++ b/js/webgpu/palettePass.js @@ -123,7 +123,11 @@ export default ({ config, device, timeBuffer }) => { return { primary: output }; }; - const run = (encoder) => { + const run = (encoder, shouldRender) => { + if (!shouldRender) { + return; + } + const computePass = encoder.beginComputePass(); computePass.setPipeline(computePipeline); computePass.setBindGroup(0, computeBindGroup); diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index c6d94a1..bfeb473 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -207,7 +207,7 @@ export default ({ config, device, timeBuffer }) => { }; }; - const run = (encoder) => { + const run = (encoder, shouldRender) => { // We render the code into an Target using MSDFs: https://github.com/Chlumsky/msdfgen const introPass = encoder.beginComputePass(); @@ -222,13 +222,15 @@ export default ({ config, device, timeBuffer }) => { computePass.dispatchWorkgroups(Math.ceil(gridSize[0] / 32), gridSize[1], 1); computePass.end(); - renderPassConfig.colorAttachments[0].view = output.createView(); - renderPassConfig.colorAttachments[1].view = highPassOutput.createView(); - const renderPass = encoder.beginRenderPass(renderPassConfig); - renderPass.setPipeline(renderPipeline); - renderPass.setBindGroup(0, renderBindGroup); - renderPass.draw(numVerticesPerQuad * numQuads, 1, 0, 0); - renderPass.end(); + if (shouldRender) { + renderPassConfig.colorAttachments[0].view = output.createView(); + renderPassConfig.colorAttachments[1].view = highPassOutput.createView(); + const renderPass = encoder.beginRenderPass(renderPassConfig); + renderPass.setPipeline(renderPipeline); + renderPass.setBindGroup(0, renderBindGroup); + renderPass.draw(numVerticesPerQuad * numQuads, 1, 0, 0); + renderPass.end(); + } }; return makePass("Rain", loaded, build, run); diff --git a/js/webgpu/stripePass.js b/js/webgpu/stripePass.js index c0f3aaf..e59092b 100644 --- a/js/webgpu/stripePass.js +++ b/js/webgpu/stripePass.js @@ -92,7 +92,11 @@ export default ({ config, device, timeBuffer }) => { }; }; - const run = (encoder) => { + const run = (encoder, shouldRender) => { + if (!shouldRender) { + return; + } + const computePass = encoder.beginComputePass(); computePass.setPipeline(computePipeline); const computeBindGroup = makeBindGroup(device, computePipeline, 0, [ diff --git a/js/webgpu/utils.js b/js/webgpu/utils.js index 608fc24..f10fe8a 100644 --- a/js/webgpu/utils.js +++ b/js/webgpu/utils.js @@ -118,9 +118,9 @@ const makeBindGroup = (device, pipeline, index, entries) => const makePass = (name, loaded, build, run) => ({ loaded: loaded ?? Promise.resolve(), build: build ?? ((size, inputs) => inputs), - run: (encoder) => { + run: (encoder, shouldRender) => { encoder.pushDebugGroup(`Pass "${name}"`); - run?.(encoder); + run?.(encoder, shouldRender); encoder.popDebugGroup(); }, }); @@ -131,7 +131,7 @@ const makePipeline = async (context, steps) => { return { steps, build: (canvasSize) => steps.reduce((outputs, step) => step.build(canvasSize, outputs), null), - run: (encoder) => steps.forEach((step) => step.run(encoder)), + run: (encoder, shouldRender) => steps.forEach((step) => step.run(encoder, shouldRender)), }; };