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"], },