diff --git a/js/Matrix.js b/js/Matrix.js
index 06d8ad9..b0f4414 100644
--- a/js/Matrix.js
+++ b/js/Matrix.js
@@ -1,5 +1,6 @@
import React, { useEffect, useState, useRef, memo } from "react";
-import { init as initRain, formulate as refreshRain, destroy as destroyRain } from "./regl/main";
+// import { init as initRain, formulate as refreshRain, destroy as destroyRain } from "./regl/main";
+import { init as initRain, formulate as refreshRain, destroy as destroyRain } from "./webgpu/main";
import makeConfig from "./utils/config";
/**
@@ -119,7 +120,7 @@ export const Matrix = memo((props) => {
canvas.style.height = "100%";
const init = async () => {
setRain(await initRain(canvas));
- }
+ };
init();
return () => {
diff --git a/js/index.js b/js/index.js
index 692069a..d0b3015 100644
--- a/js/index.js
+++ b/js/index.js
@@ -43,7 +43,12 @@ const App = () => {
Rain
{/* */}
-
+
);
};
diff --git a/js/regl/main.js b/js/regl/main.js
index ea5e328..4c56e62 100644
--- a/js/regl/main.js
+++ b/js/regl/main.js
@@ -64,15 +64,7 @@ export const init = async (canvas) => {
return rain;
};
-export const destroy = (rain) => {
- rain.resizeObserver.disconnect();
- window.removeEventListener("resize", resize);
- window.removeEventListener("dblclick", doubleClick);
- cache.clear();
-};
-
export const formulate = async (rain, config) => {
-
const { resize, canvas, cache, regl } = rain;
rain.resolution = config.resolution;
resize();
@@ -108,11 +100,11 @@ export const formulate = async (rain, config) => {
let last = NaN;
resetREGLTime: {
- const reset = regl.frame(o => {
+ const reset = regl.frame((o) => {
o.time = 0;
o.tick = 0;
reset.cancel();
- })
+ });
}
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
@@ -160,9 +152,10 @@ export const formulate = async (rain, config) => {
rain.tick = tick;
};
-export const destroyRain = ({ regl, cache, tick, canvas }) => {
+export const destroy = ({ regl, resize, doubleClick, cache, tick, canvas }) => {
+ window.removeEventListener("resize", resize);
+ window.removeEventListener("dblclick", doubleClick);
cache.clear();
tick.cancel(); // stop RAF
regl.destroy(); // release all GPU resources & event listeners
- //canvas.remove(); // drop from the DOM
};
diff --git a/js/webgpu/bloomPass.js b/js/webgpu/bloomPass.js
index a0767fb..3b33386 100644
--- a/js/webgpu/bloomPass.js
+++ b/js/webgpu/bloomPass.js
@@ -6,6 +6,8 @@ import {
makeBindGroup,
makePass,
} from "./utils.js";
+import bloomBlurShader from "../../shaders/wgsl/bloomBlur.wgsl";
+import bloomCombineShader from "../../shaders/wgsl/bloomCombine.wgsl";
// const makePyramid = makeComputeTarget;
@@ -54,8 +56,8 @@ export default ({ config, device }) => {
}
const assets = [
- loadShader(device, "shaders/wgsl/bloomBlur.wgsl"),
- loadShader(device, "shaders/wgsl/bloomCombine.wgsl"),
+ loadShader(device, bloomBlurShader),
+ loadShader(device, bloomCombineShader),
];
const linearSampler = device.createSampler({
diff --git a/js/webgpu/endPass.js b/js/webgpu/endPass.js
index 0f25241..5eb28ac 100644
--- a/js/webgpu/endPass.js
+++ b/js/webgpu/endPass.js
@@ -1,5 +1,7 @@
import { loadShader, makeBindGroup, makePass } from "./utils.js";
+import endPassShader from "../../shaders/wgsl/endPass.wgsl";
+
// Eventually, WebGPU will allow the output of the final pass in the pipeline to be copied to the canvas texture.
// Until then, this render pass does the job.
@@ -21,7 +23,7 @@ export default ({ device, canvasFormat, canvasContext }) => {
let renderPipeline;
let renderBindGroup;
- const assets = [loadShader(device, "shaders/wgsl/endPass.wgsl")];
+ const assets = [loadShader(device, endPassShader)];
const loaded = (async () => {
const [imageShader] = await Promise.all(assets);
diff --git a/js/webgpu/imagePass.js b/js/webgpu/imagePass.js
index 1f21005..aff7896 100644
--- a/js/webgpu/imagePass.js
+++ b/js/webgpu/imagePass.js
@@ -7,15 +7,16 @@ import {
makeBindGroup,
makePass,
} from "./utils.js";
+import imagePassShader from "../../shaders/wgsl/imagePass.wgsl";
// Multiplies the rendered rain and bloom by a loaded in image
const defaultBGURL =
"https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Flammarion_Colored.jpg/917px-Flammarion_Colored.jpg";
-export default ({ config, device }) => {
+export default ({ config, cache, device }) => {
const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL;
- const assets = [loadTexture(device, bgURL), loadShader(device, "shaders/wgsl/imagePass.wgsl")];
+ const assets = [loadTexture(device, cache, bgURL), loadShader(device, imagePassShader)];
const linearSampler = device.createSampler({
magFilter: "linear",
diff --git a/js/webgpu/main.js b/js/webgpu/main.js
index 43c2699..2ec16ff 100644
--- a/js/webgpu/main.js
+++ b/js/webgpu/main.js
@@ -8,16 +8,7 @@ import makeStripePass from "./stripePass.js";
import makeImagePass from "./imagePass.js";
import makeMirrorPass from "./mirrorPass.js";
import makeEndPass from "./endPass.js";
-import { setupCamera, cameraCanvas, cameraAspectRatio, cameraSize } from "../camera.js";
-
-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);
- });
+import { setupCamera, cameraCanvas, cameraAspectRatio, cameraSize } from "../utils/camera.js";
const effects = {
none: null,
@@ -32,31 +23,52 @@ const effects = {
mirror: makeMirrorPass,
};
-export default async (canvas, config) => {
- await loadJS("lib/gl-matrix.js");
+export const init = async (canvas) => {
+ const resize = () => {
+ const devicePixelRatio = window.devicePixelRatio ?? 1;
+ canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * rain.resolution);
+ canvas.height = Math.ceil(canvas.clientHeight * devicePixelRatio * rain.resolution);
+ };
- if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
- window.ondblclick = () => {
- if (document.fullscreenElement == null) {
- if (canvas.webkitRequestFullscreen != null) {
- canvas.webkitRequestFullscreen();
- } else {
- canvas.requestFullscreen();
- }
- } else {
- document.exitFullscreen();
- }
- };
- }
+ const doubleClick = () => {
+ if (!document.fullscreenEnabled && !document.webkitFullscreenEnabled) {
+ return;
+ }
+ if (document.fullscreenElement != null) {
+ document.exitFullscreen();
+ return;
+ }
+ if (canvas.webkitRequestFullscreen != null) {
+ canvas.webkitRequestFullscreen();
+ } else {
+ canvas.requestFullscreen();
+ }
+ };
+
+ const canvasContext = canvas.getContext("webgpu");
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+
+ const cache = new Map();
+ const rain = { canvas, resize, doubleClick, cache, canvasContext, adapter, device, resolution: 1 };
+
+ window.addEventListener("dblclick", doubleClick);
+ window.addEventListener("resize", resize);
+ resize();
+
+ return rain;
+};
+
+export const formulate = async (rain, config) => {
+ const { resize, canvas, cache, canvasContext, adapter, device } = rain;
+ rain.resolution = config.resolution;
+ resize();
if (config.useCamera) {
await setupCamera();
}
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
- const adapter = await navigator.gpu.requestAdapter();
- const device = await adapter.requestDevice();
- const canvasContext = canvas.getContext("webgpu");
// console.table(device.limits);
@@ -82,6 +94,7 @@ export default async (canvas, config) => {
const context = {
config,
+ cache,
adapter,
device,
canvasContext,
@@ -127,7 +140,7 @@ export default async (canvas, config) => {
const canvasWidth = Math.ceil(canvas.clientWidth * devicePixelRatio * config.resolution);
const canvasHeight = Math.ceil(canvas.clientHeight * devicePixelRatio * config.resolution);
const canvasSize = [canvasWidth, canvasHeight];
- if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
+ if (outputs == null || canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
outputs = pipeline.build(canvasSize);
@@ -159,5 +172,19 @@ export default async (canvas, config) => {
}
};
- requestAnimationFrame(renderLoop);
+ if (rain.renderLoop != null) {
+ cancelAnimationFrame(rain.renderLoop);
+ }
+
+ renderLoop(performance.now());
+
+ rain.renderLoop = renderLoop;
+};
+
+export const destroy = ({ device, resize, doubleClick, cache, canvas }) => {
+ window.removeEventListener("resize", resize);
+ window.removeEventListener("dblclick", doubleClick);
+ cache.clear();
+ tick.cancel(); // stop RAF
+ // TODO: destroy WebGPU resources
};
diff --git a/js/webgpu/mirrorPass.js b/js/webgpu/mirrorPass.js
index e5ec465..644d361 100644
--- a/js/webgpu/mirrorPass.js
+++ b/js/webgpu/mirrorPass.js
@@ -6,6 +6,7 @@ import {
makeBindGroup,
makePass,
} from "./utils.js";
+import mirrorPassShader from "../../shaders/wgsl/mirrorPass.wgsl";
let start;
const numTouches = 5;
@@ -25,7 +26,7 @@ window.onclick = (e) => {
};
export default ({ config, device, cameraTex, cameraAspectRatio, timeBuffer }) => {
- const assets = [loadShader(device, "shaders/wgsl/mirrorPass.wgsl")];
+ const assets = [loadShader(device, mirrorPassShader)];
const linearSampler = device.createSampler({
magFilter: "linear",
diff --git a/js/webgpu/palettePass.js b/js/webgpu/palettePass.js
index 0f19caa..1ef1ec0 100644
--- a/js/webgpu/palettePass.js
+++ b/js/webgpu/palettePass.js
@@ -1,4 +1,4 @@
-import colorToRGB from "../colorToRGB.js";
+import colorToRGB from "../utils/colorToRGB.js";
import { structs } from "../../lib/gpu-buffer.js";
import {
loadShader,
@@ -7,6 +7,7 @@ import {
makeComputeTarget,
makePass,
} from "./utils.js";
+import palettePassShader from "../../shaders/wgsl/palettePass.wgsl";
// Maps the brightness of the rendered rain and bloom to colors
// in a linear gradient buffer generated from the passed-in color sequence
@@ -86,7 +87,7 @@ export default ({ config, device, timeBuffer }) => {
let output;
let screenSize;
- const assets = [loadShader(device, "shaders/wgsl/palettePass.wgsl")];
+ const assets = [loadShader(device, palettePassShader)];
const loaded = (async () => {
const [paletteShader] = await Promise.all(assets);
diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js
index 67bed55..35b7611 100644
--- a/js/webgpu/rainPass.js
+++ b/js/webgpu/rainPass.js
@@ -7,6 +7,8 @@ import {
makeBindGroup,
makePass,
} from "./utils.js";
+import { mat2, mat4, vec2, vec3 } from "gl-matrix";
+import rainPassShader from "../../shaders/wgsl/rainPass.wgsl";
const rippleTypes = {
box: 0,
@@ -29,18 +31,17 @@ const makeConfigBuffer = (device, configUniforms, config, density, gridSize, gly
};
// console.table(configData);
+ console.log(configUniforms, configData);
return makeUniformBuffer(device, configUniforms, configData);
};
-export default ({ config, device, timeBuffer }) => {
- const { mat2, mat4, vec2, vec3 } = glMatrix;
-
+export default ({ config, cache, device, timeBuffer }) => {
const assets = [
- loadTexture(device, config.glyphMSDFURL),
- loadTexture(device, config.glintMSDFURL),
- loadTexture(device, config.baseTextureURL, false, true),
- loadTexture(device, config.glintTextureURL, false, true),
- loadShader(device, "shaders/wgsl/rainPass.wgsl"),
+ loadTexture(device, cache, config.glyphMSDFURL),
+ loadTexture(device, cache, config.glintMSDFURL),
+ loadTexture(device, cache, config.baseTextureURL, false, true),
+ loadTexture(device, cache, config.glintTextureURL, false, true),
+ loadShader(device, rainPassShader),
];
// The volumetric mode multiplies the number of columns
diff --git a/js/webgpu/stripePass.js b/js/webgpu/stripePass.js
index c168c51..69f4cbd 100644
--- a/js/webgpu/stripePass.js
+++ b/js/webgpu/stripePass.js
@@ -1,4 +1,4 @@
-import colorToRGB from "../colorToRGB.js";
+import colorToRGB from "../utils/colorToRGB.js";
import { structs } from "../../lib/gpu-buffer.js";
import {
loadShader,
@@ -8,6 +8,7 @@ import {
makeComputeTarget,
makePass,
} from "./utils.js";
+import stripePassShader from "../../shaders/wgsl/stripePass.wgsl";
// Multiplies the rendered rain and bloom by a 1D gradient texture
// generated from the passed-in color sequence
@@ -68,7 +69,7 @@ export default ({ config, device, timeBuffer }) => {
let output;
let screenSize;
- const assets = [loadShader(device, "shaders/wgsl/stripePass.wgsl")];
+ const assets = [loadShader(device, stripePassShader)];
const loaded = (async () => {
const [stripeShader] = await Promise.all(assets);
diff --git a/js/webgpu/utils.js b/js/webgpu/utils.js
index bca4885..0e4d96e 100644
--- a/js/webgpu/utils.js
+++ b/js/webgpu/utils.js
@@ -1,6 +1,14 @@
-const loadTexture = async (device, url) => {
+const loadTexture = async (device, cache, url) => {
+
+ const key = url;
+ if (cache.has(key)) {
+ return cache.get(key);
+ }
+
+ let texture;
+
if (url == null) {
- return device.createTexture({
+ texture = device.createTexture({
size: [1, 1, 1],
format: "rgba8unorm",
usage:
@@ -8,23 +16,25 @@ const loadTexture = async (device, url) => {
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
+ } else {
+ const response = await fetch(url);
+ const data = await response.blob();
+ const source = await createImageBitmap(data);
+ const size = [source.width, source.height, 1];
+
+ texture = device.createTexture({
+ size,
+ format: "rgba8unorm",
+ usage:
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.COPY_DST |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ device.queue.copyExternalImageToTexture({ source, flipY: true }, { texture }, size);
}
- const response = await fetch(url);
- const data = await response.blob();
- const source = await createImageBitmap(data);
- const size = [source.width, source.height, 1];
-
- const 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);
return texture;
};
@@ -53,9 +63,9 @@ const makeComputeTarget = (device, size, mipLevelCount = 1) =>
GPUTextureUsage.STORAGE_BINDING,
});
-const loadShader = async (device, url) => {
- const response = await fetch(url);
- const code = await response.text();
+const loadShader = async (device, code /*text*/) => {
+ // const response = await fetch(url);
+ // const code = await response.text();
return {
code,
module: device.createShaderModule({ code }),
diff --git a/webpack.config.js b/webpack.config.js
index ebafb05..d3ca087 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -21,7 +21,7 @@ module.exports = {
type: "asset/resource",
},
{
- test: /\.(glsl|frag|vert)$/i,
+ test: /\.(glsl|frag|vert|wgsl)$/i,
exclude: /node_modules/,
use: ["raw-loader"],
},