Fixed some major bugs: the WebGPU cache should store loaded images and text, never GPU resource handles; renamed renderer "formulate" to "configure"; WebGPU renderer's configure function needs early returns after each major await, in case there's a new config; the render loops are now locally stored closures; renderers now have start and stop functions; fixed bugs in the REGL and WebGPU mirror passes; WebGPU bloom pass now enforces texture dimensions are greater than zero; the react component now stores the renderer type in a useRef and returns early from renderer init awaits to prevent multiple renderers from instantiating.

This commit is contained in:
Rezmason
2025-05-25 03:30:26 -07:00
parent 1da1feb356
commit b6570de106
15 changed files with 405 additions and 351 deletions

View File

@@ -1,19 +1,21 @@
import { loadText, makePassFBO, makePass } from "./utils.js";
let start;
const numClicks = 5;
const clicks = Array(numClicks).fill([0, 0, -Infinity]).flat();
let aspectRatio = 1;
export default ({ regl, canvas, cache, config, cameraTex, cameraAspectRatio }, inputs) => {
let index = 0;
window.onclick = (e) => {
clicks[index * 3 + 0] = 0 + e.clientX / e.srcElement.clientWidth;
clicks[index * 3 + 1] = 1 - e.clientY / e.srcElement.clientHeight;
clicks[index * 3 + 2] = (Date.now() - start) / 1000;
index = (index + 1) % numClicks;
};
let start;
const numClicks = 5;
const clicks = Array(numClicks).fill().map(_ => ([0, 0, -Infinity]));
let aspectRatio = 1;
let index = 0;
canvas.onmousedown = (e) => {
const rect = e.srcElement.getBoundingClientRect();
clicks[index][0] = 0 + (e.clientX - rect.x) / rect.width;
clicks[index][1] = 1 - (e.clientY - rect.y) / rect.height;
clicks[index][2] = (performance.now() - start) / 1000;
index = (index + 1) % numClicks;
};
export default ({ regl, cache, config, cameraTex, cameraAspectRatio }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat);
const mirrorPassFrag = loadText(cache, "shaders/glsl/mirrorPass.frag.glsl");
const render = regl({
@@ -23,14 +25,19 @@ export default ({ regl, cache, config, cameraTex, cameraAspectRatio }, inputs) =
tex: inputs.primary,
bloomTex: inputs.bloom,
cameraTex,
clicks: () => clicks,
// REGL bug can misinterpret array uniforms
["clicks[0]"]: () => clicks[0],
["clicks[1]"]: () => clicks[1],
["clicks[2]"]: () => clicks[2],
["clicks[3]"]: () => clicks[3],
["clicks[4]"]: () => clicks[4],
aspectRatio: () => aspectRatio,
cameraAspectRatio,
},
framebuffer: output,
});
start = Date.now();
start = performance.now();
return makePass(
{

View File

@@ -24,7 +24,7 @@ const effects = {
export default class REGLRenderer extends Renderer {
#tick;
#renderFunc;
#regl;
#glMatrix;
@@ -43,26 +43,24 @@ export default class REGLRenderer extends Renderer {
});
}
async formulate(config) {
await super.formulate(config);
const canvas = this.canvas;
const cache = this.cache;
const regl = this.#regl;
const glMatrix = this.#glMatrix;
const dimensions = { width: 1, height: 1 };
async configure(config) {
await super.configure(config);
if (config.useCamera) {
await setupCamera();
}
const canvas = this.canvas;
const cache = this.cache;
const regl = this.#regl;
const glMatrix = this.#glMatrix;
const dimensions = { width: 1, height: 1 };
const cameraTex = regl.texture(cameraCanvas);
// 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, cache, config, cameraTex, cameraAspectRatio, glMatrix };
const context = { regl, canvas, cache, config, cameraTex, cameraAspectRatio, glMatrix };
const pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName]]);
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
@@ -75,17 +73,15 @@ export default class REGLRenderer extends Renderer {
const targetFrameTimeMilliseconds = 1000 / config.fps;
let last = NaN;
resetREGLTime: {
const reset = regl.frame((o) => {
o.time = 0;
o.tick = 0;
reset.cancel();
});
}
const reset = regl.frame((reglContext) => {
reglContext.tick = 0;
reset.cancel();
});
this.#renderFunc = (reglContext) => {
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
if (config.once) {
tick.cancel();
this.stop();
}
const now = regl.now() * 1000;
@@ -106,6 +102,7 @@ export default class REGLRenderer extends Renderer {
if (config.useCamera) {
cameraTex(cameraCanvas);
}
const {viewportWidth, viewportHeight} = reglContext;
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {
dimensions.width = viewportWidth;
dimensions.height = viewportHeight;
@@ -119,20 +116,31 @@ export default class REGLRenderer extends Renderer {
}
drawToScreen();
});
};
const frame = this.#regl.frame(o => {
this.#renderFunc(o);
frame.cancel();
});
}
if (this.#tick != null) {
this.#tick.cancel();
stop() {
super.stop();
this.#renderFunc = null;
}
update(now) {
if (this.#renderFunc != null) {
const frame = this.#regl.frame(o => {
this.#renderFunc(o);
frame.cancel();
})
}
this.#tick = tick;
super.update(now);
}
destroy() {
if (this.destroyed) {
return;
}
this.#tick.cancel(); // stop RAF
if (this.destroyed) return;
this.#regl.destroy(); // releases all GPU resources & event listeners
super.destroy();
}