mirror of
https://github.com/Rezmason/matrix.git
synced 2026-04-14 12:29:30 -07:00
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:
20
TODO.txt
20
TODO.txt
@@ -1,22 +1,16 @@
|
|||||||
TODO:
|
TODO:
|
||||||
WebGPU formulate is expensive
|
Minimum react requirement?
|
||||||
Mirror pass clicks bug
|
|
||||||
|
|
||||||
Minify bundles
|
Minify bundles
|
||||||
Naming "matrix" for the github repo, "digital-rain" and "DigitalRain" for everything else
|
Naming "matrix" for the github repo, "digital-rain" and "DigitalRain" for everything else
|
||||||
Minimum react requirement?
|
|
||||||
Retire fetchLibraries?
|
Retire fetchLibraries?
|
||||||
|
Preserve time across configure calls, move times into renderer and uniforms
|
||||||
Move off of regl
|
Move off of regl
|
||||||
Unify implementations?
|
Unify implementations?
|
||||||
Responsive changes
|
Reshape all passes to react to config changes, ie. "configure"
|
||||||
Move start time to rain object
|
simple deltas only require updating the uniforms
|
||||||
Matrix component should record, then overwrite it
|
return boolean of whether all deltas are simple
|
||||||
Reshape all passes to react to config changes, ie. "configure"
|
Resource changes are simple if they're cached and loaded, false otherwise
|
||||||
main.js "formulate" --> "configure"
|
remake the pipeline if anything returns false
|
||||||
simple deltas only require updating the uniforms
|
|
||||||
return boolean of whether all deltas are simple
|
|
||||||
Resource changes are simple if they're cached and loaded, false otherwise
|
|
||||||
remake the pipeline if anything returns false
|
|
||||||
Core vs full
|
Core vs full
|
||||||
core
|
core
|
||||||
One embedded MSDF, combined from the two main glyph sets and their configs
|
One embedded MSDF, combined from the two main glyph sets and their configs
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import makeConfig from "./utils/config";
|
|||||||
* volumetric?: boolean,
|
* volumetric?: boolean,
|
||||||
* loops?: boolean,
|
* loops?: boolean,
|
||||||
* skipIntro?: boolean,
|
* skipIntro?: boolean,
|
||||||
* renderer?: "regl" | "three" | string,
|
* renderer?: "regl" | "webgpu" | string,
|
||||||
* suppressWarnings?: boolean,
|
* suppressWarnings?: boolean,
|
||||||
* useHalfFloat?: boolean,
|
* useHalfFloat?: boolean,
|
||||||
* isometric?: boolean,
|
* isometric?: boolean,
|
||||||
@@ -109,9 +109,10 @@ export const Matrix = memo((props) => {
|
|||||||
const elProps = { style, className };
|
const elProps = { style, className };
|
||||||
const domElement = useRef(null);
|
const domElement = useRef(null);
|
||||||
const [rRenderer, setRenderer] = useState(null);
|
const [rRenderer, setRenderer] = useState(null);
|
||||||
|
const rendererType = useRef(null);
|
||||||
const [rSize, setSize] = useState([1, 1]);
|
const [rSize, setSize] = useState([1, 1]);
|
||||||
const [rConfig, setConfig] = useState(makeConfig({}));
|
const [rConfig, setConfig] = useState(makeConfig({}));
|
||||||
const rendererClasses = {};
|
const rendererModules = {};
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(entries => {
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -126,14 +127,6 @@ export const Matrix = memo((props) => {
|
|||||||
resizeObserver.observe(domElement.current);
|
resizeObserver.observe(domElement.current);
|
||||||
}, [domElement]);
|
}, [domElement]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setConfig(makeConfig({
|
|
||||||
...Object.fromEntries(
|
|
||||||
Object.entries(rawConfigProps).filter(([_, value]) => value != null),
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
const supportsWebGPU = () => {
|
const supportsWebGPU = () => {
|
||||||
return (
|
return (
|
||||||
window.GPUQueue != null &&
|
window.GPUQueue != null &&
|
||||||
@@ -142,6 +135,18 @@ export const Matrix = memo((props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const config = makeConfig({
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(rawConfigProps).filter(([_, value]) => value != null),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if (config.renderer === "webgpu" && !supportsWebGPU()) {
|
||||||
|
config.renderer = "regl";
|
||||||
|
}
|
||||||
|
setConfig(config);
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (rRenderer == null) return;
|
if (rRenderer == null) return;
|
||||||
rRenderer.canvas.remove();
|
rRenderer.canvas.remove();
|
||||||
@@ -150,37 +155,40 @@ export const Matrix = memo((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const useWebGPU = supportsWebGPU() && rConfig.renderer === "webgpu";
|
rendererType.current = rConfig.renderer;
|
||||||
const isWebGPU = rRenderer?.type === "webgpu";
|
let rendererModule;
|
||||||
|
if (rConfig.renderer === "webgpu") {
|
||||||
|
rendererModules.webgpu ??= import("./webgpu/renderer.js");
|
||||||
|
rendererModule = rendererModules.webgpu;
|
||||||
|
} else {
|
||||||
|
rendererModules.regl ??= import("./regl/renderer.js");
|
||||||
|
rendererModule = rendererModules.regl;
|
||||||
|
}
|
||||||
|
|
||||||
const loadRain = async () => {
|
(async () => {
|
||||||
let renderer;
|
const rendererClass = (await rendererModule).default;
|
||||||
if (useWebGPU) {
|
if (rendererType.current !== rConfig.renderer) return;
|
||||||
rendererClasses.webgpu ??= (await import("./webgpu/renderer.js")).default;
|
const renderer = new rendererClass();
|
||||||
renderer = new (rendererClasses.webgpu)();
|
|
||||||
} else {
|
|
||||||
rendererClasses.regl ??= (await import("./regl/renderer.js")).default;
|
|
||||||
renderer = new (rendererClasses.regl)();
|
|
||||||
}
|
|
||||||
setRenderer(renderer);
|
|
||||||
await renderer.ready;
|
await renderer.ready;
|
||||||
|
if (rendererType.current !== rConfig.renderer) {
|
||||||
|
console.warn("Destroyed a redundant renderer late.");
|
||||||
|
renderer.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
setRenderer(renderer);
|
||||||
const canvas = renderer.canvas;
|
const canvas = renderer.canvas;
|
||||||
canvas.style.width = "100%";
|
canvas.style.width = "100%";
|
||||||
canvas.style.height = "100%";
|
canvas.style.height = "100%";
|
||||||
domElement.current.appendChild(canvas);
|
domElement.current.appendChild(canvas);
|
||||||
};
|
})();
|
||||||
|
|
||||||
if (rRenderer == null || useWebGPU !== isWebGPU) {
|
|
||||||
cleanup();
|
|
||||||
loadRain();
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [rConfig.renderer]);
|
}, [rConfig.renderer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rRenderer?.destroyed ?? true) return;
|
if (rRenderer?.destroyed ?? true) return;
|
||||||
rRenderer.formulate(rConfig);
|
rRenderer.configure(rConfig);
|
||||||
}, [rRenderer, rConfig]);
|
}, [rRenderer, rConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ document.body.onload = async () => {
|
|||||||
renderer.fullscreen = !renderer.fullscreen;
|
renderer.fullscreen = !renderer.fullscreen;
|
||||||
});
|
});
|
||||||
document.body.appendChild(renderer.canvas);
|
document.body.appendChild(renderer.canvas);
|
||||||
await renderer.formulate(config);
|
await renderer.configure(config);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isRunningSwiftShader() && !config.suppressWarnings) {
|
if (isRunningSwiftShader() && !config.suppressWarnings) {
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { loadText, makePassFBO, makePass } from "./utils.js";
|
import { loadText, makePassFBO, makePass } from "./utils.js";
|
||||||
|
|
||||||
let start;
|
export default ({ regl, canvas, cache, config, cameraTex, cameraAspectRatio }, inputs) => {
|
||||||
const numClicks = 5;
|
|
||||||
const clicks = Array(numClicks).fill([0, 0, -Infinity]).flat();
|
|
||||||
let aspectRatio = 1;
|
|
||||||
|
|
||||||
let index = 0;
|
let start;
|
||||||
window.onclick = (e) => {
|
const numClicks = 5;
|
||||||
clicks[index * 3 + 0] = 0 + e.clientX / e.srcElement.clientWidth;
|
const clicks = Array(numClicks).fill().map(_ => ([0, 0, -Infinity]));
|
||||||
clicks[index * 3 + 1] = 1 - e.clientY / e.srcElement.clientHeight;
|
let aspectRatio = 1;
|
||||||
clicks[index * 3 + 2] = (Date.now() - start) / 1000;
|
|
||||||
index = (index + 1) % numClicks;
|
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 output = makePassFBO(regl, config.useHalfFloat);
|
||||||
const mirrorPassFrag = loadText(cache, "shaders/glsl/mirrorPass.frag.glsl");
|
const mirrorPassFrag = loadText(cache, "shaders/glsl/mirrorPass.frag.glsl");
|
||||||
const render = regl({
|
const render = regl({
|
||||||
@@ -23,14 +25,19 @@ export default ({ regl, cache, config, cameraTex, cameraAspectRatio }, inputs) =
|
|||||||
tex: inputs.primary,
|
tex: inputs.primary,
|
||||||
bloomTex: inputs.bloom,
|
bloomTex: inputs.bloom,
|
||||||
cameraTex,
|
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,
|
aspectRatio: () => aspectRatio,
|
||||||
cameraAspectRatio,
|
cameraAspectRatio,
|
||||||
},
|
},
|
||||||
framebuffer: output,
|
framebuffer: output,
|
||||||
});
|
});
|
||||||
|
|
||||||
start = Date.now();
|
start = performance.now();
|
||||||
|
|
||||||
return makePass(
|
return makePass(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const effects = {
|
|||||||
|
|
||||||
export default class REGLRenderer extends Renderer {
|
export default class REGLRenderer extends Renderer {
|
||||||
|
|
||||||
#tick;
|
#renderFunc;
|
||||||
#regl;
|
#regl;
|
||||||
#glMatrix;
|
#glMatrix;
|
||||||
|
|
||||||
@@ -43,26 +43,24 @@ export default class REGLRenderer extends Renderer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async formulate(config) {
|
async configure(config) {
|
||||||
await super.formulate(config);
|
await super.configure(config);
|
||||||
|
|
||||||
const canvas = this.canvas;
|
|
||||||
const cache = this.cache;
|
|
||||||
const regl = this.#regl;
|
|
||||||
const glMatrix = this.#glMatrix;
|
|
||||||
|
|
||||||
const dimensions = { width: 1, height: 1 };
|
|
||||||
|
|
||||||
if (config.useCamera) {
|
if (config.useCamera) {
|
||||||
await setupCamera();
|
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);
|
const cameraTex = regl.texture(cameraCanvas);
|
||||||
|
|
||||||
// All this takes place in a full screen quad.
|
// All this takes place in a full screen quad.
|
||||||
const fullScreenQuad = makeFullScreenQuad(regl);
|
const fullScreenQuad = makeFullScreenQuad(regl);
|
||||||
const effectName = config.effect in effects ? config.effect : "palette";
|
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 pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName]]);
|
||||||
|
|
||||||
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
|
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
|
||||||
@@ -75,17 +73,15 @@ export default class REGLRenderer extends Renderer {
|
|||||||
const targetFrameTimeMilliseconds = 1000 / config.fps;
|
const targetFrameTimeMilliseconds = 1000 / config.fps;
|
||||||
let last = NaN;
|
let last = NaN;
|
||||||
|
|
||||||
resetREGLTime: {
|
const reset = regl.frame((reglContext) => {
|
||||||
const reset = regl.frame((o) => {
|
reglContext.tick = 0;
|
||||||
o.time = 0;
|
reset.cancel();
|
||||||
o.tick = 0;
|
});
|
||||||
reset.cancel();
|
|
||||||
});
|
this.#renderFunc = (reglContext) => {
|
||||||
}
|
|
||||||
|
|
||||||
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
|
|
||||||
if (config.once) {
|
if (config.once) {
|
||||||
tick.cancel();
|
this.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = regl.now() * 1000;
|
const now = regl.now() * 1000;
|
||||||
@@ -106,6 +102,7 @@ export default class REGLRenderer extends Renderer {
|
|||||||
if (config.useCamera) {
|
if (config.useCamera) {
|
||||||
cameraTex(cameraCanvas);
|
cameraTex(cameraCanvas);
|
||||||
}
|
}
|
||||||
|
const {viewportWidth, viewportHeight} = reglContext;
|
||||||
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {
|
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {
|
||||||
dimensions.width = viewportWidth;
|
dimensions.width = viewportWidth;
|
||||||
dimensions.height = viewportHeight;
|
dimensions.height = viewportHeight;
|
||||||
@@ -119,20 +116,31 @@ export default class REGLRenderer extends Renderer {
|
|||||||
}
|
}
|
||||||
drawToScreen();
|
drawToScreen();
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const frame = this.#regl.frame(o => {
|
||||||
|
this.#renderFunc(o);
|
||||||
|
frame.cancel();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.#tick != null) {
|
stop() {
|
||||||
this.#tick.cancel();
|
super.stop();
|
||||||
|
this.#renderFunc = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(now) {
|
||||||
|
if (this.#renderFunc != null) {
|
||||||
|
const frame = this.#regl.frame(o => {
|
||||||
|
this.#renderFunc(o);
|
||||||
|
frame.cancel();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
super.update(now);
|
||||||
this.#tick = tick;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.destroyed) {
|
if (this.destroyed) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#tick.cancel(); // stop RAF
|
|
||||||
this.#regl.destroy(); // releases all GPU resources & event listeners
|
this.#regl.destroy(); // releases all GPU resources & event listeners
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default class Renderer {
|
|||||||
#fullscreen = false;
|
#fullscreen = false;
|
||||||
#cache = new Map();
|
#cache = new Map();
|
||||||
#destroyed = false;
|
#destroyed = false;
|
||||||
|
#running = false;
|
||||||
|
|
||||||
constructor(type, ready) {
|
constructor(type, ready) {
|
||||||
this.#type = type;
|
this.#type = type;
|
||||||
@@ -18,48 +19,46 @@ export default class Renderer {
|
|||||||
this.#ready = Renderer.libraries.then(libraries => {
|
this.#ready = Renderer.libraries.then(libraries => {
|
||||||
this.#cache = new Map(libraries.staticAssets);
|
this.#cache = new Map(libraries.staticAssets);
|
||||||
}).then(ready);
|
}).then(ready);
|
||||||
|
this.#ready.then(() => this.start());
|
||||||
}
|
}
|
||||||
|
|
||||||
get canvas() {
|
get running() { return this.#running; }
|
||||||
return this.#canvas;
|
|
||||||
|
start() {
|
||||||
|
this.#running = true;
|
||||||
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
get cache() {
|
stop() {
|
||||||
return this.#cache;
|
this.#running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get type () {
|
update(now) {
|
||||||
return this.#type;
|
if (!this.#running) return;
|
||||||
|
requestAnimationFrame(now => this.update(now));
|
||||||
}
|
}
|
||||||
|
|
||||||
get ready () {
|
get canvas() { return this.#canvas; }
|
||||||
return this.#ready;
|
|
||||||
}
|
|
||||||
|
|
||||||
get size() {
|
get cache() { return this.#cache; }
|
||||||
return [this.#width, this.#height];
|
|
||||||
}
|
get type () { return this.#type; }
|
||||||
|
|
||||||
|
get ready () { return this.#ready; }
|
||||||
|
|
||||||
|
get size() { return ([this.#width, this.#height]); }
|
||||||
|
|
||||||
set size([width, height]) {
|
set size([width, height]) {
|
||||||
[width, height] = [Math.ceil(width), Math.ceil(height)];
|
[width, height] = [Math.ceil(width), Math.ceil(height)];
|
||||||
if (width === this.#width && height === this.#height) {
|
if (width === this.#width && height === this.#height) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
[this.#canvas.width, this.#canvas.height] = [this.#width, this.#height] = [width, height];
|
[this.#canvas.width, this.#canvas.height] = [this.#width, this.#height] = [width, height];
|
||||||
}
|
}
|
||||||
|
|
||||||
get fullscreen() {
|
get fullscreen() { return this.#fullscreen; }
|
||||||
return this.#fullscreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
set fullscreen(value) {
|
set fullscreen(value) {
|
||||||
if (!!value === this.#fullscreen) {
|
if (!!value === this.#fullscreen) return;
|
||||||
return;
|
if (!document.fullscreenEnabled && !document.webkitFullscreenEnabled) return;
|
||||||
}
|
|
||||||
|
|
||||||
if (!document.fullscreenEnabled && !document.webkitFullscreenEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#fullscreen = value;
|
this.#fullscreen = value;
|
||||||
if (document.fullscreenElement != null) {
|
if (document.fullscreenElement != null) {
|
||||||
@@ -74,18 +73,17 @@ export default class Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async formulate(config) {
|
async configure(config) {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
if (this.destroyed) {
|
if (this.destroyed) {
|
||||||
throw new Error("Cannot formulate a destroyed rain instance.");
|
throw new Error("Cannot configure a destroyed rain instance.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get destroyed() {
|
get destroyed() { return this.#destroyed; }
|
||||||
return this.#destroyed;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this.stop();
|
||||||
this.#destroyed = true;
|
this.#destroyed = true;
|
||||||
this.#cache.clear();
|
this.#cache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const makePyramid = (device, size, pyramidHeight) =>
|
|||||||
.map((_, index) =>
|
.map((_, index) =>
|
||||||
makeComputeTarget(
|
makeComputeTarget(
|
||||||
device,
|
device,
|
||||||
size.map((x) => Math.floor(x * 2 ** -index)),
|
size.map((x) => Math.max(1, Math.floor(x * 2 ** -index))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ export default ({ config, device, cache }) => {
|
|||||||
|
|
||||||
const build = (screenSize, inputs) => {
|
const build = (screenSize, inputs) => {
|
||||||
// Since the bloom is blurry, we downscale everything
|
// Since the bloom is blurry, we downscale everything
|
||||||
scaledScreenSize = screenSize.map((x) => Math.floor(x * bloomSize));
|
scaledScreenSize = screenSize.map((x) => Math.max(1, Math.floor(x * bloomSize)));
|
||||||
|
|
||||||
destroyPyramid(hBlurPyramid);
|
destroyPyramid(hBlurPyramid);
|
||||||
hBlurPyramid = makePyramid(device, scaledScreenSize, pyramidHeight);
|
hBlurPyramid = makePyramid(device, scaledScreenSize, pyramidHeight);
|
||||||
@@ -169,8 +169,8 @@ export default ({ config, device, cache }) => {
|
|||||||
computePass.setPipeline(blurPipeline);
|
computePass.setPipeline(blurPipeline);
|
||||||
for (let i = 0; i < pyramidHeight; i++) {
|
for (let i = 0; i < pyramidHeight; i++) {
|
||||||
const dispatchSize = [
|
const dispatchSize = [
|
||||||
Math.ceil(Math.floor(scaledScreenSize[0] * 2 ** -i) / 32),
|
Math.max(1, Math.ceil(Math.floor(scaledScreenSize[0] * 2 ** -i) / 32)),
|
||||||
Math.floor(Math.floor(scaledScreenSize[1] * 2 ** -i)),
|
Math.max(1, Math.floor(Math.floor(scaledScreenSize[1] * 2 ** -i))),
|
||||||
1,
|
1,
|
||||||
];
|
];
|
||||||
computePass.setBindGroup(0, hBlurBindGroups[i]);
|
computePass.setBindGroup(0, hBlurBindGroups[i]);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default ({ device, cache, canvasFormat, canvasContext }) => {
|
|||||||
nearestSampler,
|
nearestSampler,
|
||||||
inputs.primary.createView(),
|
inputs.primary.createView(),
|
||||||
]);
|
]);
|
||||||
return null;
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = (encoder, shouldRender) => {
|
const run = (encoder, shouldRender) => {
|
||||||
|
|||||||
@@ -7,24 +7,7 @@ import {
|
|||||||
makePass,
|
makePass,
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
|
|
||||||
let start;
|
export default ({ config, device, canvas, cache, cameraTex, cameraAspectRatio, timeBuffer }) => {
|
||||||
const numTouches = 5;
|
|
||||||
const touches = Array(numTouches)
|
|
||||||
.fill()
|
|
||||||
.map((_) => [0, 0, -Infinity, 0]);
|
|
||||||
let aspectRatio = 1;
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
let touchesChanged = true;
|
|
||||||
window.onclick = (e) => {
|
|
||||||
touches[index][0] = 0 + e.clientX / e.srcElement.clientWidth;
|
|
||||||
touches[index][1] = 1 - e.clientY / e.srcElement.clientHeight;
|
|
||||||
touches[index][2] = (Date.now() - start) / 1000;
|
|
||||||
index = (index + 1) % numTouches;
|
|
||||||
touchesChanged = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ({ config, device, cache, cameraTex, cameraAspectRatio, timeBuffer }) => {
|
|
||||||
const assets = [loadShader(device, cache, "shaders/wgsl/mirrorPass.wgsl")];
|
const assets = [loadShader(device, cache, "shaders/wgsl/mirrorPass.wgsl")];
|
||||||
|
|
||||||
const linearSampler = device.createSampler({
|
const linearSampler = device.createSampler({
|
||||||
@@ -32,6 +15,24 @@ export default ({ config, device, cache, cameraTex, cameraAspectRatio, timeBuffe
|
|||||||
minFilter: "linear",
|
minFilter: "linear",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let start;
|
||||||
|
const numTouches = 5;
|
||||||
|
const touches = Array(numTouches)
|
||||||
|
.fill()
|
||||||
|
.map((_) => [0, 0, -Infinity, 0]);
|
||||||
|
let aspectRatio = 1;
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let touchesChanged = true;
|
||||||
|
canvas.onmousedown = (e) => {
|
||||||
|
const rect = e.srcElement.getBoundingClientRect();
|
||||||
|
touches[index][0] = 0 + (e.clientX - rect.x) / rect.width;
|
||||||
|
touches[index][1] = 1 - (e.clientY - rect.y) / rect.height;
|
||||||
|
touches[index][2] = (performance.now() - start) / 1000;
|
||||||
|
index = (index + 1) % numTouches;
|
||||||
|
touchesChanged = true;
|
||||||
|
};
|
||||||
|
|
||||||
let computePipeline;
|
let computePipeline;
|
||||||
let configBuffer;
|
let configBuffer;
|
||||||
let sceneUniforms;
|
let sceneUniforms;
|
||||||
@@ -109,7 +110,7 @@ export default ({ config, device, cache, cameraTex, cameraAspectRatio, timeBuffe
|
|||||||
computePass.end();
|
computePass.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
start = Date.now();
|
start = performance.now();
|
||||||
|
|
||||||
return makePass("Mirror", loaded, build, run);
|
return makePass("Mirror", loaded, build, run);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,152 +27,185 @@ const effects = {
|
|||||||
export default class REGLRenderer extends Renderer {
|
export default class REGLRenderer extends Renderer {
|
||||||
|
|
||||||
#glMatrix;
|
#glMatrix;
|
||||||
#canvasContext;
|
|
||||||
#adapter;
|
|
||||||
#device;
|
#device;
|
||||||
#renderLoop;
|
#canvasContext;
|
||||||
|
#canvasFormat;
|
||||||
|
#renderFunc;
|
||||||
|
|
||||||
|
#renewingDevice;
|
||||||
|
#configureIndex = 0;
|
||||||
|
#rebuildingPipeline;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("webgpu", async () => {
|
super("webgpu", async () => {
|
||||||
const libraries = await Renderer.libraries;
|
const libraries = await Renderer.libraries;
|
||||||
this.#glMatrix = libraries.glMatrix;
|
this.#glMatrix = libraries.glMatrix;
|
||||||
this.#canvasContext = this.canvas.getContext("webgpu");
|
|
||||||
this.#adapter = await navigator.gpu.requestAdapter();
|
|
||||||
this.#device = await this.#adapter.requestDevice();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async formulate(config) {
|
async configure(config) {
|
||||||
await super.formulate(config);
|
const index = ++this.#configureIndex;
|
||||||
|
await super.configure(config);
|
||||||
const canvas = this.canvas;
|
|
||||||
const cache = this.cache;
|
|
||||||
const canvasContext = this.#canvasContext;
|
|
||||||
const adapter = this.#adapter;
|
|
||||||
const device = this.#device;
|
|
||||||
const glMatrix = this.#glMatrix;
|
|
||||||
|
|
||||||
if (config.useCamera) {
|
if (config.useCamera) {
|
||||||
await setupCamera();
|
await setupCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
|
if (this.#rebuildingPipeline != null) {
|
||||||
|
await this.#rebuildingPipeline;
|
||||||
// console.table(device.limits);
|
|
||||||
|
|
||||||
canvasContext.configure({
|
|
||||||
device,
|
|
||||||
format: canvasFormat,
|
|
||||||
alphaMode: "opaque",
|
|
||||||
usage:
|
|
||||||
// GPUTextureUsage.STORAGE_BINDING |
|
|
||||||
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST,
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeUniforms = structs.from(`struct Time { seconds : f32, frames : i32, };`).Time;
|
|
||||||
const timeBuffer = makeUniformBuffer(device, timeUniforms);
|
|
||||||
const cameraTex = device.createTexture({
|
|
||||||
size: cameraSize,
|
|
||||||
format: "rgba8unorm",
|
|
||||||
usage:
|
|
||||||
GPUTextureUsage.TEXTURE_BINDING |
|
|
||||||
GPUTextureUsage.COPY_DST |
|
|
||||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
||||||
});
|
|
||||||
|
|
||||||
const context = {
|
|
||||||
config,
|
|
||||||
cache,
|
|
||||||
adapter,
|
|
||||||
device,
|
|
||||||
canvasContext,
|
|
||||||
timeBuffer,
|
|
||||||
canvasFormat,
|
|
||||||
cameraTex,
|
|
||||||
cameraAspectRatio,
|
|
||||||
cameraSize,
|
|
||||||
glMatrix,
|
|
||||||
};
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
if (isNaN(start)) {
|
|
||||||
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 size = this.size;
|
|
||||||
const [width, height] = size;
|
|
||||||
if (outputs == null || canvas.width !== width || canvas.height !== height) {
|
|
||||||
[canvas.width, canvas.height] = size;
|
|
||||||
outputs = pipeline.build(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.useCamera) {
|
|
||||||
device.queue.copyExternalImageToTexture(
|
|
||||||
{ source: cameraCanvas },
|
|
||||||
{ texture: cameraTex },
|
|
||||||
cameraSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
device.queue.writeBuffer(
|
|
||||||
timeBuffer,
|
|
||||||
0,
|
|
||||||
timeUniforms.toBuffer({ seconds: (now - start) / 1000, frames }),
|
|
||||||
);
|
|
||||||
frames++;
|
|
||||||
|
|
||||||
const encoder = device.createCommandEncoder();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.#renderLoop != null) {
|
|
||||||
cancelAnimationFrame(this.#renderLoop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoop(performance.now());
|
const oldDevice = this.#device;
|
||||||
this.#renderLoop = renderLoop;
|
|
||||||
|
if (this.#renewingDevice == null) {
|
||||||
|
this.#renewingDevice = (async () => {
|
||||||
|
this.#canvasContext = this.canvas.getContext("webgpu");
|
||||||
|
this.#canvasFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
const adapter = await navigator.gpu.requestAdapter();
|
||||||
|
this.#device = await adapter.requestDevice();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
await this.#renewingDevice;
|
||||||
|
this.#renewingDevice = null;
|
||||||
|
|
||||||
|
if (this.#configureIndex !== index || this.destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#rebuildingPipeline = (async () => {
|
||||||
|
|
||||||
|
const glMatrix = this.#glMatrix;
|
||||||
|
const canvas = this.canvas;
|
||||||
|
const cache = this.cache;
|
||||||
|
const device = this.#device;
|
||||||
|
const canvasContext = this.#canvasContext;
|
||||||
|
const canvasFormat = this.#canvasFormat;
|
||||||
|
const dimensions = { width: 1, height: 1 };
|
||||||
|
|
||||||
|
const timeUniforms = structs.from(`struct Time { seconds : f32, frames : i32, };`).Time;
|
||||||
|
const timeBuffer = makeUniformBuffer(device, timeUniforms);
|
||||||
|
const cameraTex = device.createTexture({
|
||||||
|
size: cameraSize,
|
||||||
|
format: "rgba8unorm",
|
||||||
|
usage:
|
||||||
|
GPUTextureUsage.TEXTURE_BINDING |
|
||||||
|
GPUTextureUsage.COPY_DST |
|
||||||
|
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
glMatrix,
|
||||||
|
config,
|
||||||
|
cache,
|
||||||
|
device,
|
||||||
|
canvas,
|
||||||
|
canvasContext,
|
||||||
|
canvasFormat,
|
||||||
|
timeBuffer,
|
||||||
|
cameraTex,
|
||||||
|
cameraAspectRatio,
|
||||||
|
cameraSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const effectName = config.effect in effects ? config.effect : "palette";
|
||||||
|
const pipeline = await makePipeline(context, [
|
||||||
|
makeRain,
|
||||||
|
makeBloomPass,
|
||||||
|
effects[effectName],
|
||||||
|
makeEndPass,
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.#canvasContext.configure({
|
||||||
|
device: this.#device,
|
||||||
|
format: this.#canvasFormat,
|
||||||
|
alphaMode: "opaque",
|
||||||
|
usage:
|
||||||
|
// GPUTextureUsage.STORAGE_BINDING |
|
||||||
|
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST,
|
||||||
|
});
|
||||||
|
|
||||||
|
dimensions.width = canvas.width;
|
||||||
|
dimensions.height = canvas.height;
|
||||||
|
const targetFrameTimeMilliseconds = 1000 / config.fps;
|
||||||
|
let frames = 0;
|
||||||
|
let start = NaN;
|
||||||
|
let last = NaN;
|
||||||
|
let outputs;
|
||||||
|
|
||||||
|
this.#renderFunc = (now) => {
|
||||||
|
if (config.once) {
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(start)) {
|
||||||
|
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 size = this.size;
|
||||||
|
const [width, height] = size;
|
||||||
|
if (outputs == null || dimensions.width !== width || dimensions.height !== height) {
|
||||||
|
[dimensions.width, dimensions.height] = size;
|
||||||
|
outputs = pipeline.build(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.useCamera) {
|
||||||
|
device.queue.copyExternalImageToTexture(
|
||||||
|
{ source: cameraCanvas },
|
||||||
|
{ texture: cameraTex },
|
||||||
|
cameraSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
device.queue.writeBuffer(
|
||||||
|
timeBuffer,
|
||||||
|
0,
|
||||||
|
timeUniforms.toBuffer({ seconds: (now - start) / 1000, frames }),
|
||||||
|
);
|
||||||
|
frames++;
|
||||||
|
|
||||||
|
const encoder = device.createCommandEncoder();
|
||||||
|
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()]);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
await this.#rebuildingPipeline;
|
||||||
|
this.#renderFunc(performance.now());
|
||||||
|
if (oldDevice != null) {
|
||||||
|
oldDevice.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
super.stop();
|
||||||
|
this.#renderFunc = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(now) {
|
||||||
|
if (this.#renderFunc != null) {
|
||||||
|
this.#renderFunc(now);
|
||||||
|
}
|
||||||
|
super.update(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.destroyed) {
|
if (this.destroyed) return;
|
||||||
return;
|
if (this.#device != null) {
|
||||||
|
this.#device.destroy(); // This also destroys any objects created with the device
|
||||||
|
this.#device = null;
|
||||||
}
|
}
|
||||||
cancelAnimationFrame(this.#renderLoop); // stop RAF
|
|
||||||
this.#device.destroy(); // This also destroys any objects created with the device
|
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
const loadTexture = async (device, cache, url) => {
|
const loadTexture = async (device, cache, url) => {
|
||||||
const key = url;
|
|
||||||
if (cache.has(key)) {
|
|
||||||
return cache.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let texture;
|
const format = "rgba8unorm";
|
||||||
|
const usage =
|
||||||
|
GPUTextureUsage.TEXTURE_BINDING |
|
||||||
|
GPUTextureUsage.COPY_DST |
|
||||||
|
GPUTextureUsage.RENDER_ATTACHMENT;
|
||||||
|
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
texture = device.createTexture({
|
return device.createTexture({
|
||||||
size: [1, 1, 1],
|
size: [1, 1, 1],
|
||||||
format: "rgba8unorm",
|
format,
|
||||||
usage:
|
usage,
|
||||||
GPUTextureUsage.TEXTURE_BINDING |
|
|
||||||
GPUTextureUsage.COPY_DST |
|
|
||||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let source;
|
||||||
|
const key = url;
|
||||||
|
if (cache.has(key)) {
|
||||||
|
source = cache.get(key);
|
||||||
} else {
|
} else {
|
||||||
let imageURL;
|
let imageURL;
|
||||||
if (typeof cache.get(`url::${url}`) === "function") {
|
if (typeof cache.get(`url::${url}`) === "function") {
|
||||||
@@ -25,23 +28,17 @@ const loadTexture = async (device, cache, url) => {
|
|||||||
|
|
||||||
const response = await fetch(imageURL);
|
const response = await fetch(imageURL);
|
||||||
const data = await response.blob();
|
const data = await response.blob();
|
||||||
const source = await createImageBitmap(data);
|
source = await createImageBitmap(data);
|
||||||
const size = [source.width, source.height, 1];
|
cache.set(key, source);
|
||||||
|
|
||||||
texture = device.createTexture({
|
|
||||||
size,
|
|
||||||
format: "rgba8unorm",
|
|
||||||
usage:
|
|
||||||
GPUTextureUsage.TEXTURE_BINDING |
|
|
||||||
GPUTextureUsage.COPY_DST |
|
|
||||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
||||||
});
|
|
||||||
|
|
||||||
device.queue.copyExternalImageToTexture({ source, flipY: true }, { texture }, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.set(key, texture);
|
const size = [source.width, source.height, 1];
|
||||||
|
const texture = device.createTexture({
|
||||||
|
size,
|
||||||
|
format,
|
||||||
|
usage,
|
||||||
|
});
|
||||||
|
device.queue.copyExternalImageToTexture({ source, flipY: true }, { texture }, size);
|
||||||
return texture;
|
return texture;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,14 +68,16 @@ const makeComputeTarget = (device, size, mipLevelCount = 1) =>
|
|||||||
|
|
||||||
const loadShader = async (device, cache, url) => {
|
const loadShader = async (device, cache, url) => {
|
||||||
const key = url;
|
const key = url;
|
||||||
if (cache.has(key)) {
|
|
||||||
return cache.get(key);
|
|
||||||
}
|
|
||||||
let code;
|
let code;
|
||||||
if (typeof cache.get(`raw::${url}`) === "function") {
|
if (cache.has(key)) {
|
||||||
code = (await cache.get(`raw::${url}`)()).default;
|
code = cache.get(key);
|
||||||
} else {
|
} else {
|
||||||
code = await (await fetch(url)).text();
|
if (typeof cache.get(`raw::${url}`) === "function") {
|
||||||
|
code = (await cache.get(`raw::${url}`)()).default;
|
||||||
|
} else {
|
||||||
|
code = await (await fetch(url)).text();
|
||||||
|
}
|
||||||
|
cache.set(key, code);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test": "npm run format && npm run build && vite --config tools/dev.config.js --open /tools/test/index.html",
|
||||||
"dev": "npm run format && vite --config tools/dev.config.js",
|
"dev": "npm run format && vite --config tools/dev.config.js",
|
||||||
"build": "npm run format && rm -rf ./dist/* && vite build --config tools/build/core.config.js && vite build --config tools/build/full.config.js",
|
"build": "npm run format && rm -rf ./dist/* && vite build --config tools/build/core.config.js && vite build --config tools/build/full.config.js",
|
||||||
"format": "eslint . && prettier --write --no-error-on-unmatched-pattern 'src/**/*.{js,jsx,mjs,json}' '*.{js,jsx,mjs,json,html}' 'assets/**/*.{json,css}'"
|
"format": "eslint . && prettier --write --no-error-on-unmatched-pattern 'src/**/*.{js,jsx,mjs,json}' '*.{js,jsx,mjs,json,html}' 'assets/**/*.{json,css}'"
|
||||||
|
|||||||
@@ -283,7 +283,6 @@ fn computeSymbol (simTime : f32, isFirstFrame : bool, glyphPos : vec2<f32>, scre
|
|||||||
var symbol = previousSymbol;
|
var symbol = previousSymbol;
|
||||||
if (time.frames % config.cycleFrameSkip == 0) {
|
if (time.frames % config.cycleFrameSkip == 0) {
|
||||||
age += cycleSpeed * f32(config.cycleFrameSkip);
|
age += cycleSpeed * f32(config.cycleFrameSkip);
|
||||||
var advance = floor(age);
|
|
||||||
if (age > 1.0) {
|
if (age > 1.0) {
|
||||||
symbol = floor(config.glyphSequenceLength * randomFloat(screenPos + simTime));
|
symbol = floor(config.glyphSequenceLength * randomFloat(screenPos + simTime));
|
||||||
age = fract(age);
|
age = fract(age);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
const renderer = new RendererClass();
|
const renderer = new RendererClass();
|
||||||
await renderer.ready;
|
await renderer.ready;
|
||||||
document.querySelector("#test-core-bundled").appendChild(renderer.canvas);
|
document.querySelector("#test-core-bundled").appendChild(renderer.canvas);
|
||||||
await renderer.formulate(makeConfig({once: false}));
|
await renderer.configure(makeConfig({once: false}));
|
||||||
</script>
|
</script>
|
||||||
</details>
|
</details>
|
||||||
<details id="test-core-react-bundled">
|
<details id="test-core-react-bundled">
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
const renderer = new RendererClass();
|
const renderer = new RendererClass();
|
||||||
await renderer.ready;
|
await renderer.ready;
|
||||||
document.querySelector("#test-full-bundled").appendChild(renderer.canvas);
|
document.querySelector("#test-full-bundled").appendChild(renderer.canvas);
|
||||||
await renderer.formulate(makeConfig({once: false, version: "twilight"}));
|
await renderer.configure(makeConfig({once: false, version: "twilight"}));
|
||||||
</script>
|
</script>
|
||||||
</details>
|
</details>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const App = () => {
|
|||||||
const [resolution, setResolution] = useState(0.75);
|
const [resolution, setResolution] = useState(0.75);
|
||||||
const [cursorColor, setCursorColor] = useState(null);
|
const [cursorColor, setCursorColor] = useState(null);
|
||||||
const [backgroundColor, setBackgroundColor] = useState("0,0,0");
|
const [backgroundColor, setBackgroundColor] = useState("0,0,0");
|
||||||
const [rendererType, setRendererType] = useState(null);
|
const [rendererType, setRendererType] = useState("webgpu");
|
||||||
const [density, setDensity] = useState(2);
|
const [density, setDensity] = useState(2);
|
||||||
const [destroyed, setDestroyed] = useState(false);
|
const [destroyed, setDestroyed] = useState(false);
|
||||||
const onVersionButtonClick = () => {
|
const onVersionButtonClick = () => {
|
||||||
@@ -68,7 +68,7 @@ const App = () => {
|
|||||||
setBackgroundColor(null);
|
setBackgroundColor(null);
|
||||||
};
|
};
|
||||||
const onRendererButtonClick = () => {
|
const onRendererButtonClick = () => {
|
||||||
setRendererType(() => (rendererType === "webgpu" ? "regl" : "webgpu"));
|
setRendererType(rendererType === "webgpu" ? "regl" : "webgpu");
|
||||||
};
|
};
|
||||||
const onDestroyButtonClick = () => {
|
const onDestroyButtonClick = () => {
|
||||||
setDestroyed(true);
|
setDestroyed(true);
|
||||||
@@ -76,54 +76,60 @@ const App = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button onClick={onVersionButtonClick}>Version: "{version}"</button>
|
<div>
|
||||||
<button onClick={onEffectButtonClick}>Effect: "{effect}"</button>
|
<button onClick={onVersionButtonClick}>Version: "{version}"</button>
|
||||||
<button onClick={onRendererButtonClick}>Renderer: {rendererType ?? "default (regl)"}</button>
|
<button onClick={onEffectButtonClick}>Effect: "{effect}"</button>
|
||||||
<button onClick={onDestroyButtonClick}>Destroy</button>
|
<button onClick={onRendererButtonClick}>Renderer: {rendererType ?? "default (regl)"}</button>
|
||||||
<label htmlFor="cursor-color">Cursor color: </label>
|
<button onClick={onDestroyButtonClick}>Destroy</button>
|
||||||
<input
|
</div>
|
||||||
name="cursor-color"
|
<div>
|
||||||
type="color"
|
<label htmlFor="cursor-color">Cursor color: </label>
|
||||||
onChange={(e) => {
|
<input
|
||||||
const values = e.target.value
|
name="cursor-color"
|
||||||
.match(/[\da-fA-F]{2}/g)
|
type="color"
|
||||||
.map((s) => parseInt(s, 16) / 0xff)
|
onChange={(e) => {
|
||||||
.join(",");
|
const values = e.target.value
|
||||||
setCursorColor(values);
|
.match(/[\da-fA-F]{2}/g)
|
||||||
}}
|
.map((s) => parseInt(s, 16) / 0xff)
|
||||||
/>
|
.join(",");
|
||||||
<label htmlFor="background-color">Background color: </label>
|
setCursorColor(values);
|
||||||
<input
|
}}
|
||||||
name="background-color"
|
/>
|
||||||
type="color"
|
<label htmlFor="background-color">Background color: </label>
|
||||||
onChange={(e) => {
|
<input
|
||||||
const values = e.target.value
|
name="background-color"
|
||||||
.match(/[\da-fA-F]{2}/g)
|
type="color"
|
||||||
.map((s) => parseInt(s, 16) / 0xff)
|
onChange={(e) => {
|
||||||
.join(",");
|
const values = e.target.value
|
||||||
setBackgroundColor(values);
|
.match(/[\da-fA-F]{2}/g)
|
||||||
}}
|
.map((s) => parseInt(s, 16) / 0xff)
|
||||||
/>
|
.join(",");
|
||||||
<label htmlFor="num-columns"># of columns:</label>
|
setBackgroundColor(values);
|
||||||
<input
|
}}
|
||||||
name="num-columns"
|
/>
|
||||||
type="range"
|
</div>
|
||||||
value={numColumns}
|
<div>
|
||||||
min="10"
|
<label htmlFor="num-columns"># of columns:</label>
|
||||||
max="160"
|
<input
|
||||||
step="1"
|
name="num-columns"
|
||||||
onInput={(e) => setNumColumns(parseInt(e.target.value))}
|
type="range"
|
||||||
/>
|
value={numColumns}
|
||||||
<label htmlFor="resolution">resolution:</label>
|
min="10"
|
||||||
<input
|
max="160"
|
||||||
name="resolution"
|
step="1"
|
||||||
type="range"
|
onInput={(e) => setNumColumns(parseInt(e.target.value))}
|
||||||
value={resolution}
|
/>
|
||||||
min="0"
|
<label htmlFor="resolution">resolution:</label>
|
||||||
max="1"
|
<input
|
||||||
step="0.01"
|
name="resolution"
|
||||||
onInput={(e) => setResolution(parseFloat(e.target.value))}
|
type="range"
|
||||||
/>
|
value={resolution}
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
onInput={(e) => setResolution(parseFloat(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!destroyed && (
|
{!destroyed && (
|
||||||
<Matrix
|
<Matrix
|
||||||
|
|||||||
Reference in New Issue
Block a user