From eea341f50c237e60205ea51174c4fda68ae6bebb Mon Sep 17 00:00:00 2001 From: Rezmason Date: Tue, 6 May 2025 12:59:02 -0700 Subject: [PATCH] Exploring ways to preserve the vanilla JS browser demo without compromising on the bundle. Experimenting with embedding images in the bundle as data URIs --- TODO.txt | 5 ++ js/Matrix.js | 92 ++++++++++++++++++++++++++++--- js/fetchLibraries.js | 20 +++++++ js/index.js | 1 - js/main.js | 12 +++- js/regl/bloomPass.js | 20 +++---- js/regl/imagePass.js | 6 +- js/regl/main.js | 20 +++---- js/regl/mirrorPass.js | 8 +-- js/regl/palettePass.js | 10 ++-- js/regl/rainPass.js | 43 ++++++++------- js/regl/stripePass.js | 13 +++-- js/regl/utils.js | 1 - js/utils/config.js | 56 ++++++++----------- js/webgpu/bloomPass.js | 6 +- js/webgpu/endPass.js | 4 +- js/webgpu/imagePass.js | 6 +- js/webgpu/main.js | 18 +++++- js/webgpu/mirrorPass.js | 3 +- js/webgpu/palettePass.js | 3 +- js/webgpu/rainPass.js | 7 +-- js/webgpu/stripePass.js | 3 +- js/webgpu/utils.js | 7 +-- package-lock.json | 116 +++++++++++++++++++++++++++++++++++++++ package.json | 2 + rollup.config.mjs | 5 +- webpack.config.js | 22 ++++---- 27 files changed, 372 insertions(+), 137 deletions(-) create mode 100644 js/fetchLibraries.js diff --git a/TODO.txt b/TODO.txt index 1371c67..1a1c0d4 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,4 +1,9 @@ TODO: + TTF --> MSDF + Isolate fun stuff from core + Separate configs + Separate assets + Live config update roadmap Modify regl pass diff --git a/js/Matrix.js b/js/Matrix.js index b0f4414..68c33da 100644 --- a/js/Matrix.js +++ b/js/Matrix.js @@ -1,8 +1,87 @@ +import highPassFrag from "../shaders/glsl/bloomPass.highPass.frag.glsl"; +import blurFrag from "../shaders/glsl/bloomPass.blur.frag.glsl"; +import combineFrag from "../shaders/glsl/bloomPass.combine.frag.glsl"; +import imagePassFrag from "../shaders/glsl/imagePass.frag.glsl"; +import mirrorPassFrag from "../shaders/glsl/mirrorPass.frag.glsl"; +import palettePassFrag from "../shaders/glsl/palettePass.frag.glsl"; +import rainPassIntro from "../shaders/glsl/rainPass.intro.frag.glsl"; +import rainPassRaindrop from "../shaders/glsl/rainPass.raindrop.frag.glsl"; +import rainPassSymbol from "../shaders/glsl/rainPass.symbol.frag.glsl"; +import rainPassEffect from "../shaders/glsl/rainPass.effect.frag.glsl"; +import rainPassVert from "../shaders/glsl/rainPass.vert.glsl"; +import rainPassFrag from "../shaders/glsl/rainPass.frag.glsl"; +import stripePassFrag from "../shaders/glsl/stripePass.frag.glsl"; +import msdfCoptic from "../assets/coptic_msdf.png"; +import msdfGothic from "../assets/gothic_msdf.png"; +import msdfMatrixCode from "../assets/matrixcode_msdf.png"; +import msdfRes from "../assets/resurrections_msdf.png"; +// import megacity from "../assets/megacity_msdf.png"; +import msdfResGlint from "../assets/resurrections_glint_msdf.png"; +// import msdfHuberfishA from "../assets/huberfish_a_msdf.png"; +// import msdfHuberfishD from "../assets/huberfish_d_msdf.png"; +// import msdfGtargTenretni from "../assets/gtarg_tenretniolleh_msdf.png"; +// import msdfGtargAlienText from "../assets/gtarg_alientext_msdf.png"; +// import msdfNeoMatrixology from "../assets/neomatrixology_msdf.png"; +// import texSand from "../assets/sand.png"; +// import texPixels from "../assets/pixel_grid.png"; +import texMesh from "../assets/mesh.png"; +import texMetal from "../assets/metal.png"; +import bloomBlurShader from "../shaders/wgsl/bloomBlur.wgsl"; +import bloomCombineShader from "../shaders/wgsl/bloomCombine.wgsl"; +import endPassShader from "../shaders/wgsl/endPass.wgsl"; +import imagePassShader from "../shaders/wgsl/imagePass.wgsl"; +import mirrorPassShader from "../shaders/wgsl/mirrorPass.wgsl"; +import palettePassShader from "../shaders/wgsl/palettePass.wgsl"; +import rainPassShader from "../shaders/wgsl/rainPass.wgsl"; +import stripePassShader from "../shaders/wgsl/stripePass.wgsl"; + +const inclusion = [ + highPassFrag, + blurFrag, + combineFrag, + imagePassFrag, + mirrorPassFrag, + palettePassFrag, + rainPassIntro, + rainPassRaindrop, + rainPassSymbol, + rainPassEffect, + rainPassVert, + rainPassFrag, + stripePassFrag, + msdfCoptic, + msdfGothic, + msdfMatrixCode, + msdfRes, + // megacity, + msdfResGlint, + // msdfHuberfishA, + // msdfHuberfishD, + // msdfGtargTenretni, + // msdfGtargAlienText, + // msdfNeoMatrixology, + // texSand, + // texPixels, + texMesh, + texMetal, + bloomBlurShader, + bloomCombineShader, + endPassShader, + imagePassShader, + mirrorPassShader, + palettePassShader, + rainPassShader, + stripePassShader, +].reduce((i, s) => s.length + i, 0); +if (inclusion === 0) console.log("!"); + 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 "./webgpu/main"; +import * as reglRenderer from "./regl/main"; +import * as webgpuRenderer from "./webgpu/main"; import makeConfig from "./utils/config"; +console.log(webgpuRenderer.init, webgpuRenderer.formulate, webgpuRenderer.destroy); + /** * @typedef {object} Colour * @property {"hsl"|"rgb"} space @@ -18,7 +97,7 @@ import makeConfig from "./utils/config"; * "classic" | "megacity" | "neomatrixology" | "operator" | * "nightmare" | "paradise" | "resurrections" | "trinity" | * "morpheus" | "bugs" | "palimpsest" | "twilight" | - * "holoplay" | "3d" | "throwback" | "updated" | + * "3d" | "throwback" | "updated" | * "1999" | "2003" | "2021" | string /* custom * / * ), * font?: keyof typeof fonts, // "matrixcode", … @@ -36,7 +115,6 @@ import makeConfig from "./utils/config"; * renderer?: "regl" | "three" | string, * suppressWarnings?: boolean, * useHalfFloat?: boolean, - * useHoloplay?: boolean, * isometric?: boolean, * * /* ------------- glyph appearance ------------- * / @@ -119,12 +197,12 @@ export const Matrix = memo((props) => { canvas.style.width = "100%"; canvas.style.height = "100%"; const init = async () => { - setRain(await initRain(canvas)); + setRain(await reglRenderer.init(canvas)); }; init(); return () => { - destroyRain(rain); + reglRenderer.destroy(rain); setRain(null); }; }, []); @@ -134,7 +212,7 @@ export const Matrix = memo((props) => { return; } const refresh = async () => { - await refreshRain(rain, makeConfig({ ...rest })); + await reglRenderer.formulate(rain, makeConfig({ ...rest })); }; refresh(); }, [props, rain]); diff --git a/js/fetchLibraries.js b/js/fetchLibraries.js new file mode 100644 index 0000000..c68bd01 --- /dev/null +++ b/js/fetchLibraries.js @@ -0,0 +1,20 @@ +export default async () => { + let glMatrix, createREGL; + + try { + glMatrix = await import("gl-matrix"); + createREGL = (await import("regl")).default; + } catch { + const loadJS = (src) => + new Promise((resolve, reject) => { + const tag = document.createElement("script"); + [tag.onload, tag.onerror, tag.src] = [resolve, reject, src]; + document.body.appendChild(tag); + }); + await Promise.all([loadJS("lib/regl.min.js"), loadJS("lib/gl-matrix.js")]); + glMatrix = globalThis.glMatrix; + createREGL = globalThis.createREGL; + } + + return { glMatrix, createREGL }; +}; diff --git a/js/index.js b/js/index.js index d0b3015..415b392 100644 --- a/js/index.js +++ b/js/index.js @@ -14,7 +14,6 @@ const versions = [ "paradise", "resurrections", "operator", - "holoplay", "throwback", "updated", "1999", diff --git a/js/main.js b/js/main.js index 5001a9f..63c9b2a 100644 --- a/js/main.js +++ b/js/main.js @@ -1,4 +1,4 @@ -import makeConfig from "./config.js"; +import makeConfig from "./utils/config.js"; const canvas = document.createElement("canvas"); document.body.appendChild(canvas); @@ -27,6 +27,12 @@ document.body.onload = async () => { const useWebGPU = (await supportsWebGPU()) && ["webgpu"].includes(config.renderer?.toLowerCase()); const solution = import(`./${useWebGPU ? "webgpu" : "regl"}/main.js`); + const initialize = async (canvas, config) => { + const { init, formulate } = await solution; + const rain = await init(canvas); + await formulate(rain, config); + }; + if (isRunningSwiftShader() && !config.suppressWarnings) { const notice = document.createElement("notice"); notice.innerHTML = `
@@ -41,11 +47,11 @@ document.body.onload = async () => { config.suppressWarnings = true; urlParams.set("suppressWarnings", true); history.replaceState({}, "", "?" + unescape(urlParams.toString())); - (await solution).default(canvas, config); + await initialize(canvas, config); canvas.style.display = "unset"; document.body.removeChild(notice); }); } else { - (await solution).default(canvas, config); + await initialize(canvas, config); } }; diff --git a/js/regl/bloomPass.js b/js/regl/bloomPass.js index d56a335..a9a3963 100644 --- a/js/regl/bloomPass.js +++ b/js/regl/bloomPass.js @@ -1,7 +1,4 @@ -import { makePassFBO, makePass } from "./utils"; -import highPassFrag from "../../shaders/glsl/bloomPass.highPass.frag.glsl"; -import blurFrag from "../../shaders/glsl/bloomPass.blur.frag.glsl"; -import combineFrag from "../../shaders/glsl/bloomPass.combine.frag.glsl"; +import { loadText, makePassFBO, makePass } from "./utils.js"; // The bloom pass is basically an added high-pass blur. // The blur approximation is the sum of a pyramid of downscaled, blurred textures. @@ -20,7 +17,7 @@ const resizePyramid = (pyramid, vw, vh, scale) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index)), ); -export default ({ regl, config }, inputs) => { +export default ({ regl, cache, config }, inputs) => { const { bloomStrength, bloomSize, highPassThreshold } = config; const enabled = bloomSize > 0 && bloomStrength > 0; @@ -39,6 +36,7 @@ export default ({ regl, config }, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); // The high pass restricts the blur to bright things in our input texture. + const highPassFrag = loadText(cache, "shaders/glsl/bloomPass.highPass.frag.glsl"); const highPass = regl({ frag: regl.prop("frag"), uniforms: { @@ -53,6 +51,7 @@ export default ({ regl, config }, inputs) => { // by blurring them all, this basic blur approximates a more complex gaussian: // https://web.archive.org/web/20191124072602/https://software.intel.com/en-us/articles/compute-shader-hdr-and-bloom + const blurFrag = loadText(cache, "shaders/glsl/bloomPass.blur.frag.glsl"); const blur = regl({ frag: regl.prop("frag"), uniforms: { @@ -65,6 +64,7 @@ export default ({ regl, config }, inputs) => { }); // The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture + const combineFrag = loadText(cache, "shaders/glsl/bloomPass.combine.frag.glsl"); const combine = regl({ frag: regl.prop("frag"), uniforms: { @@ -79,7 +79,7 @@ export default ({ regl, config }, inputs) => { primary: inputs.primary, bloom: output, }, - Promise.all([highPassFrag.loaded, blurFrag.loaded]), + Promise.all([highPassFrag.loaded, blurFrag.loaded, combineFrag.loaded]), (w, h) => { // The blur pyramids can be lower resolution than the screen. resizePyramid(highPassPyramid, w, h, bloomSize); @@ -98,14 +98,14 @@ export default ({ regl, config }, inputs) => { const vBlurFBO = vBlurPyramid[i]; highPass({ fbo: highPassFBO, - frag: highPassFrag, + frag: highPassFrag.text(), tex: i === 0 ? inputs.primary : highPassPyramid[i - 1], }); - blur({ fbo: hBlurFBO, frag: blurFrag, tex: highPassFBO, direction: [1, 0] }); - blur({ fbo: vBlurFBO, frag: blurFrag, tex: hBlurFBO, direction: [0, 1] }); + blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] }); + blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] }); } - combine({ frag: combineFrag }); + combine({ frag: combineFrag.text() }); }, ); }; diff --git a/js/regl/imagePass.js b/js/regl/imagePass.js index 0bd861b..5aaef6b 100644 --- a/js/regl/imagePass.js +++ b/js/regl/imagePass.js @@ -1,5 +1,4 @@ import { loadImage, loadText, makePassFBO, makePass } from "./utils.js"; -import imagePassFrag from "../../shaders/glsl/imagePass.frag.glsl"; // Multiplies the rendered rain and bloom by a loaded in image @@ -10,6 +9,7 @@ export default ({ regl, cache, config }, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL; const background = loadImage(cache, regl, bgURL); + const imagePassFrag = loadText(cache, "shaders/glsl/imagePass.frag.glsl"); const render = regl({ frag: regl.prop("frag"), uniforms: { @@ -23,11 +23,11 @@ export default ({ regl, cache, config }, inputs) => { { primary: output, }, - Promise.all([background.loaded]), + Promise.all([background.loaded, imagePassFrag.loaded]), (w, h) => output.resize(w, h), (shouldRender) => { if (shouldRender) { - render({ frag: imagePassFrag }); + render({ frag: imagePassFrag.text() }); } }, ); diff --git a/js/regl/main.js b/js/regl/main.js index 753eb6f..4ea3bc5 100644 --- a/js/regl/main.js +++ b/js/regl/main.js @@ -1,5 +1,5 @@ import { makeFullScreenQuad, makePipeline } from "./utils.js"; -import createREGL from "regl"; +import fetchLibraries from "../fetchLibraries.js"; import makeRain from "./rainPass.js"; import makeBloomPass from "./bloomPass.js"; import makePalettePass from "./palettePass.js"; @@ -21,7 +21,13 @@ const effects = { mirror: makeMirrorPass, }; +let createREGL, glMatrix; + export const init = async (canvas) => { + const libraries = await fetchLibraries(); + createREGL = libraries.createREGL; + glMatrix = libraries.glMatrix; + const resize = () => { const devicePixelRatio = window.devicePixelRatio ?? 1; canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * rain.resolution); @@ -74,18 +80,12 @@ export const formulate = async (rain, config) => { } const cameraTex = regl.texture(cameraCanvas); - const lkg = await getLKG(config.useHoloplay, true); // 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, lkg, cameraTex, cameraAspectRatio }; - const pipeline = makePipeline(context, [ - makeRain, - makeBloomPass, - effects[effectName], - makeQuiltPass, - ]); + const context = { regl, cache, config, cameraTex, cameraAspectRatio, glMatrix }; + const pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName]]); const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary }; const drawToScreen = regl({ uniforms: screenUniforms }); @@ -150,7 +150,7 @@ export const formulate = async (rain, config) => { rain.tick = tick; }; -export const destroy = ({ regl, resize, doubleClick, cache, tick, canvas }) => { +export const destroy = ({ regl, cache, resize, doubleClick, tick, canvas }) => { window.removeEventListener("resize", resize); window.removeEventListener("dblclick", doubleClick); cache.clear(); diff --git a/js/regl/mirrorPass.js b/js/regl/mirrorPass.js index f225825..5a63d6a 100644 --- a/js/regl/mirrorPass.js +++ b/js/regl/mirrorPass.js @@ -1,5 +1,4 @@ import { loadText, makePassFBO, makePass } from "./utils.js"; -import mirrorPassFrag from "../../shaders/glsl/mirrorPass.frag.glsl"; let start; const numClicks = 5; @@ -14,8 +13,9 @@ window.onclick = (e) => { index = (index + 1) % numClicks; }; -export default ({ regl, config, cameraTex, cameraAspectRatio }, inputs) => { +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({ frag: regl.prop("frag"), uniforms: { @@ -36,14 +36,14 @@ export default ({ regl, config, cameraTex, cameraAspectRatio }, inputs) => { { primary: output, }, - null, // No async loading, glsl bundled and loaded into memory at document load + Promise.all([mirrorPassFrag.loaded]), (w, h) => { output.resize(w, h); aspectRatio = w / h; }, (shouldRender) => { if (shouldRender) { - render({ frag: mirrorPassFrag }); + render({ frag: mirrorPassFrag.text() }); } }, ); diff --git a/js/regl/palettePass.js b/js/regl/palettePass.js index 228e8bb..024627e 100644 --- a/js/regl/palettePass.js +++ b/js/regl/palettePass.js @@ -1,6 +1,5 @@ -import colorToRGB from "../utils/colorToRGB"; -import { make1DTexture, makePassFBO, makePass } from "./utils.js"; -import palettePassFrag from "../../shaders/glsl/palettePass.frag.glsl"; +import colorToRGB from "../utils/colorToRGB.js"; +import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js"; // Maps the brightness of the rendered rain and bloom to colors // in a 1D gradient palette texture generated from the passed-in color sequence @@ -55,7 +54,7 @@ const makePalette = (regl, entries) => { // won't persist across subsequent frames. This is a safe trick // in screen space. -export default ({ regl, config }, inputs) => { +export default ({ regl, cache, config }, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); const paletteTex = makePalette(regl, config.palette); const { @@ -67,6 +66,7 @@ export default ({ regl, config }, inputs) => { ditherMagnitude, } = config; + const palettePassFrag = loadText(cache, "shaders/glsl/palettePass.frag.glsl"); const render = regl({ frag: regl.prop("frag"), @@ -92,7 +92,7 @@ export default ({ regl, config }, inputs) => { (w, h) => output.resize(w, h), (shouldRender) => { if (shouldRender) { - render({ frag: palettePassFrag }); + render({ frag: palettePassFrag.text() }); } }, ); diff --git a/js/regl/rainPass.js b/js/regl/rainPass.js index 446a468..177f62d 100644 --- a/js/regl/rainPass.js +++ b/js/regl/rainPass.js @@ -1,11 +1,4 @@ -import { loadImage, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js"; -import { mat4, vec3 } from "gl-matrix"; -import rainPassIntro from "../../shaders/glsl/rainPass.intro.frag.glsl"; -import rainPassRaindrop from "../../shaders/glsl/rainPass.raindrop.frag.glsl"; -import rainPassSymbol from "../../shaders/glsl/rainPass.symbol.frag.glsl"; -import rainPassEffect from "../../shaders/glsl/rainPass.effect.frag.glsl"; -import rainPassVert from "../../shaders/glsl/rainPass.vert.glsl"; -import rainPassFrag from "../../shaders/glsl/rainPass.frag.glsl"; +import { loadImage, loadText, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js"; const extractEntries = (src, keys) => Object.fromEntries(Array.from(Object.entries(src)).filter(([key]) => keys.includes(key))); @@ -37,7 +30,8 @@ const blVert = [1, 0]; const brVert = [1, 1]; const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert]; -export default ({ regl, cache, config, lkg }) => { +export default ({ regl, cache, config, glMatrix }) => { + const { mat4, vec3 } = glMatrix; // The volumetric mode multiplies the number of columns // to reach the desired density, and then overlaps them const volumetric = config.volumetric; @@ -69,6 +63,7 @@ export default ({ regl, cache, config, lkg }) => { }; const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns); + const rainPassIntro = loadText(cache, "shaders/glsl/rainPass.intro.frag.glsl"); const introUniforms = { ...commonUniforms, @@ -85,6 +80,7 @@ export default ({ regl, cache, config, lkg }) => { }); const raindropDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); + const rainPassRaindrop = loadText(cache, "shaders/glsl/rainPass.raindrop.frag.glsl"); const raindropUniforms = { ...commonUniforms, @@ -108,6 +104,7 @@ export default ({ regl, cache, config, lkg }) => { }); const symbolDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); + const rainPassSymbol = loadText(cache, "shaders/glsl/rainPass.symbol.frag.glsl"); const symbolUniforms = { ...commonUniforms, @@ -125,6 +122,7 @@ export default ({ regl, cache, config, lkg }) => { }); const effectDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); + const rainPassEffect = loadText(cache, "shaders/glsl/rainPass.effect.frag.glsl"); const effectUniforms = { ...commonUniforms, @@ -224,8 +222,6 @@ export default ({ regl, cache, config, lkg }) => { screenSize: regl.prop("screenSize"), }, - viewport: regl.prop("viewport"), - attributes: { aPosition: quadPositions, aCorner: Array(numQuads).fill(quadVertices), @@ -237,7 +233,6 @@ export default ({ regl, cache, config, lkg }) => { // Camera and transform math for the volumetric mode const screenSize = [1, 1]; - //const { mat4, vec3 } = glMatrix; const transform = mat4.create(); if (volumetric && config.isometric) { mat4.rotateX(transform, transform, (Math.PI * 1) / 8); @@ -253,7 +248,17 @@ export default ({ regl, cache, config, lkg }) => { { primary: output, }, - Promise.all([glyphMSDF.loaded, glintMSDF.loaded, baseTexture.loaded, glintTexture.loaded]), + Promise.all([ + glyphMSDF.loaded, + glintMSDF.loaded, + baseTexture.loaded, + glintTexture.loaded, + rainPassIntro.loaded, + rainPassRaindrop.loaded, + rainPassSymbol.loaded, + rainPassVert.loaded, + rainPassFrag.loaded, + ]), (w, h) => { output.resize(w, h); const aspectRatio = w / h; @@ -276,10 +281,10 @@ export default ({ regl, cache, config, lkg }) => { [screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; }, (shouldRender) => { - intro({ frag: rainPassIntro }); - raindrop({ frag: rainPassRaindrop }); - symbol({ frag: rainPassSymbol }); - effect({ frag: rainPassEffect }); + intro({ frag: rainPassIntro.text() }); + raindrop({ frag: rainPassRaindrop.text() }); + symbol({ frag: rainPassSymbol.text() }); + effect({ frag: rainPassEffect.text() }); if (shouldRender) { regl.clear({ @@ -292,8 +297,8 @@ export default ({ regl, cache, config, lkg }) => { transform, camera, screenSize, - vert: rainPassVert, - frag: rainPassFrag, + vert: rainPassVert.text(), + frag: rainPassFrag.text(), glyphTransform: [1, 0, 0, 1], }); } diff --git a/js/regl/stripePass.js b/js/regl/stripePass.js index ebde1e7..3708618 100644 --- a/js/regl/stripePass.js +++ b/js/regl/stripePass.js @@ -1,6 +1,5 @@ -import colorToRGB from "../utils/colorToRGB"; -import { make1DTexture, makePassFBO, makePass } from "./utils"; -import stripePassFrag from "../../shaders/glsl/stripePass.frag.glsl"; +import colorToRGB from "../utils/colorToRGB.js"; +import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js"; // Multiplies the rendered rain and bloom by a 1D gradient texture // generated from the passed-in color sequence @@ -28,7 +27,7 @@ const prideStripeColors = [ .map((color) => Array(2).fill(color)) .flat(); -export default ({ regl, config }, inputs) => { +export default ({ regl, cache, config }, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); const { @@ -52,6 +51,8 @@ export default ({ regl, config }, inputs) => { stripeColors.map((color) => [...colorToRGB(color), 1]), ); + const stripePassFrag = loadText(cache, "shaders/glsl/stripePass.frag.glsl"); + const render = regl({ frag: regl.prop("frag"), @@ -73,11 +74,11 @@ export default ({ regl, config }, inputs) => { { primary: output, }, - null, + stripePassFrag.loaded, (w, h) => output.resize(w, h), (shouldRender) => { if (shouldRender) { - render({ frag: stripePassFrag }); + render({ frag: stripePassFrag.text() }); } }, ); diff --git a/js/regl/utils.js b/js/regl/utils.js index 557b5c8..a5a28b2 100644 --- a/js/regl/utils.js +++ b/js/regl/utils.js @@ -29,7 +29,6 @@ const makeDoubleBuffer = (regl, props) => { const isPowerOfTwo = (x) => Math.log2(x) % 1 == 0; const loadImage = (cache, regl, url, mipmap) => { - const key = `${url}_${mipmap}`; if (cache.has(key)) { return cache.get(key); diff --git a/js/utils/config.js b/js/utils/config.js index 99c449b..75d6918 100644 --- a/js/utils/config.js +++ b/js/utils/config.js @@ -1,84 +1,71 @@ -import msdfCoptic from "../../assets/coptic_msdf.png"; -import msdfGothic from "../../assets/gothic_msdf.png"; -import msdfMatrixCode from "../../assets/matrixcode_msdf.png"; -import msdfRes from "../../assets/resurrections_msdf.png"; -import megacity from "../../assets/megacity_msdf.png"; -import msdfResGlint from "../../assets/resurrections_glint_msdf.png"; -import msdfHuberfishA from "../../assets/huberfish_a_msdf.png"; -import msdfHuberfishD from "../../assets/huberfish_d_msdf.png"; -import msdfGtargTenretni from "../../assets/gtarg_tenretniolleh_msdf.png"; -import msdfGtargAlienText from "../../assets/gtarg_alientext_msdf.png"; -import msdfNeoMatrixology from "../../assets/neomatrixology_msdf.png"; - -import texSand from "../../assets/sand.png"; -import texPixels from "../../assets/pixel_grid.png"; -import texMesh from "../../assets/mesh.png"; -import texMetal from "../../assets/metal.png"; - const fonts = { coptic: { // The script the Gnostic codices were written in - glyphMSDFURL: msdfCoptic, + glyphMSDFURL: "assets/coptic_msdf.png", glyphSequenceLength: 32, glyphTextureGridSize: [8, 8], }, gothic: { // The script the Codex Argenteus was written in - glyphMSDFURL: msdfGothic, + glyphMSDFURL: "assets/gothic_msdf.png", glyphSequenceLength: 27, glyphTextureGridSize: [8, 8], }, matrixcode: { // The glyphs seen in the film trilogy - glyphMSDFURL: msdfMatrixCode, + glyphMSDFURL: "assets/matrixcode_msdf.png", glyphSequenceLength: 57, glyphTextureGridSize: [8, 8], }, + /* megacity: { // The glyphs seen in the film trilogy - glyphMSDFURL: megacity, + glyphMSDFURL: "assets/megacity_msdf.png", glyphSequenceLength: 64, glyphTextureGridSize: [8, 8], }, + */ resurrections: { // The glyphs seen in the film trilogy - glyphMSDFURL: msdfRes, - glintMSDFURL: msdfResGlint, + glyphMSDFURL: "assets/resurrections_msdf.png", + glintMSDFURL: "assets/resurrections_glint_msdf.png", glyphSequenceLength: 135, glyphTextureGridSize: [13, 12], }, + /* huberfishA: { - glyphMSDFURL: msdfHuberfishA, + glyphMSDFURL: "assets/huberfish_a_msdf.png", glyphSequenceLength: 34, glyphTextureGridSize: [6, 6], }, huberfishD: { - glyphMSDFURL: msdfHuberfishD, + glyphMSDFURL: "assets/huberfish_d_msdf.png", glyphSequenceLength: 34, glyphTextureGridSize: [6, 6], }, gtarg_tenretniolleh: { - glyphMSDFURL: msdfGtargTenretni, + glyphMSDFURL: "assets/gtarg_tenretniolleh_msdf.png", glyphSequenceLength: 36, glyphTextureGridSize: [6, 6], }, gtarg_alientext: { - glyphMSDFURL: msdfGtargAlienText, + glyphMSDFURL: "assets/gtarg_alientext_msdf.png", glyphSequenceLength: 38, glyphTextureGridSize: [8, 5], }, neomatrixology: { - glyphMSDFURL: msdfNeoMatrixology, + glyphMSDFURL: "assets/neomatrixology_msdf.png", glyphSequenceLength: 12, glyphTextureGridSize: [4, 4], }, + */ }; const textureURLs = { - sand: texSand, - pixels: texPixels, - mesh: texMesh, - metal: texMetal, + // sand: "assets/sand.png", + // pixels: "assets/pixel_grid.png", + mesh: "assets/mesh.png", + metal: "assets/metal.png", }; const hsl = (...values) => ({ space: "hsl", values }); @@ -149,6 +136,7 @@ const defaults = { const versions = { classic: {}, + /* megacity: { font: "megacity", animationSpeed: 0.5, @@ -167,6 +155,7 @@ const versions = { cursorColor: hsl(0.167, 1, 0.75), cursorIntensity: 2, }, + */ operator: { cursorColor: hsl(0.375, 1, 0.66), cursorIntensity: 3, @@ -279,6 +268,7 @@ const versions = { raindropLength: 0.3, density: 0.75, }, + /* morpheus: { font: "resurrections", glintTexture: "mesh", @@ -368,7 +358,7 @@ const versions = { // { color: hsl(0.1, 1.0, 0.9), at: 1.0 }, ], }, - + */ ["3d"]: { volumetric: true, fallSpeed: 0.5, diff --git a/js/webgpu/bloomPass.js b/js/webgpu/bloomPass.js index 3b33386..a0767fb 100644 --- a/js/webgpu/bloomPass.js +++ b/js/webgpu/bloomPass.js @@ -6,8 +6,6 @@ import { makeBindGroup, makePass, } from "./utils.js"; -import bloomBlurShader from "../../shaders/wgsl/bloomBlur.wgsl"; -import bloomCombineShader from "../../shaders/wgsl/bloomCombine.wgsl"; // const makePyramid = makeComputeTarget; @@ -56,8 +54,8 @@ export default ({ config, device }) => { } const assets = [ - loadShader(device, bloomBlurShader), - loadShader(device, bloomCombineShader), + loadShader(device, "shaders/wgsl/bloomBlur.wgsl"), + loadShader(device, "shaders/wgsl/bloomCombine.wgsl"), ]; const linearSampler = device.createSampler({ diff --git a/js/webgpu/endPass.js b/js/webgpu/endPass.js index 5eb28ac..0f25241 100644 --- a/js/webgpu/endPass.js +++ b/js/webgpu/endPass.js @@ -1,7 +1,5 @@ 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. @@ -23,7 +21,7 @@ export default ({ device, canvasFormat, canvasContext }) => { let renderPipeline; let renderBindGroup; - const assets = [loadShader(device, endPassShader)]; + const assets = [loadShader(device, "shaders/wgsl/endPass.wgsl")]; const loaded = (async () => { const [imageShader] = await Promise.all(assets); diff --git a/js/webgpu/imagePass.js b/js/webgpu/imagePass.js index aff7896..f75de8d 100644 --- a/js/webgpu/imagePass.js +++ b/js/webgpu/imagePass.js @@ -7,7 +7,6 @@ import { makeBindGroup, makePass, } from "./utils.js"; -import imagePassShader from "../../shaders/wgsl/imagePass.wgsl"; // Multiplies the rendered rain and bloom by a loaded in image @@ -16,7 +15,10 @@ const defaultBGURL = export default ({ config, cache, device }) => { const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL; - const assets = [loadTexture(device, cache, bgURL), loadShader(device, imagePassShader)]; + const assets = [ + loadTexture(device, cache, bgURL), + loadShader(device, "shaders/wgsl/imagePass.wgsl"), + ]; const linearSampler = device.createSampler({ magFilter: "linear", diff --git a/js/webgpu/main.js b/js/webgpu/main.js index 2ec16ff..669f4e7 100644 --- a/js/webgpu/main.js +++ b/js/webgpu/main.js @@ -1,5 +1,6 @@ import { structs } from "../../lib/gpu-buffer.js"; import { makeUniformBuffer, makePipeline } from "./utils.js"; +import fetchLibraries from "../fetchLibraries.js"; import makeRain from "./rainPass.js"; import makeBloomPass from "./bloomPass.js"; @@ -23,7 +24,12 @@ const effects = { mirror: makeMirrorPass, }; +let glMatrix; + export const init = async (canvas) => { + const libraries = await fetchLibraries(); + glMatrix = libraries.glMatrix; + const resize = () => { const devicePixelRatio = window.devicePixelRatio ?? 1; canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * rain.resolution); @@ -50,7 +56,16 @@ export const init = async (canvas) => { const device = await adapter.requestDevice(); const cache = new Map(); - const rain = { canvas, resize, doubleClick, cache, canvasContext, adapter, device, resolution: 1 }; + const rain = { + canvas, + resize, + doubleClick, + cache, + canvasContext, + adapter, + device, + resolution: 1, + }; window.addEventListener("dblclick", doubleClick); window.addEventListener("resize", resize); @@ -103,6 +118,7 @@ export const formulate = async (rain, config) => { cameraTex, cameraAspectRatio, cameraSize, + glMatrix, }; const effectName = config.effect in effects ? config.effect : "palette"; diff --git a/js/webgpu/mirrorPass.js b/js/webgpu/mirrorPass.js index 644d361..e5ec465 100644 --- a/js/webgpu/mirrorPass.js +++ b/js/webgpu/mirrorPass.js @@ -6,7 +6,6 @@ import { makeBindGroup, makePass, } from "./utils.js"; -import mirrorPassShader from "../../shaders/wgsl/mirrorPass.wgsl"; let start; const numTouches = 5; @@ -26,7 +25,7 @@ window.onclick = (e) => { }; export default ({ config, device, cameraTex, cameraAspectRatio, timeBuffer }) => { - const assets = [loadShader(device, mirrorPassShader)]; + const assets = [loadShader(device, "shaders/wgsl/mirrorPass.wgsl")]; const linearSampler = device.createSampler({ magFilter: "linear", diff --git a/js/webgpu/palettePass.js b/js/webgpu/palettePass.js index 1ef1ec0..3aca409 100644 --- a/js/webgpu/palettePass.js +++ b/js/webgpu/palettePass.js @@ -7,7 +7,6 @@ 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 @@ -87,7 +86,7 @@ export default ({ config, device, timeBuffer }) => { let output; let screenSize; - const assets = [loadShader(device, palettePassShader)]; + const assets = [loadShader(device, "shaders/wgsl/palettePass.wgsl")]; const loaded = (async () => { const [paletteShader] = await Promise.all(assets); diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index 35b7611..6e4143c 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -7,8 +7,6 @@ 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, @@ -35,13 +33,14 @@ const makeConfigBuffer = (device, configUniforms, config, density, gridSize, gly return makeUniformBuffer(device, configUniforms, configData); }; -export default ({ config, cache, device, timeBuffer }) => { +export default ({ config, glMatrix, cache, device, timeBuffer }) => { + const { mat2, mat4, vec2, vec3 } = glMatrix; const assets = [ 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), + loadShader(device, "shaders/wgsl/rainPass.wgsl"), ]; // The volumetric mode multiplies the number of columns diff --git a/js/webgpu/stripePass.js b/js/webgpu/stripePass.js index 69f4cbd..79ab6f3 100644 --- a/js/webgpu/stripePass.js +++ b/js/webgpu/stripePass.js @@ -8,7 +8,6 @@ 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 @@ -69,7 +68,7 @@ export default ({ config, device, timeBuffer }) => { let output; let screenSize; - const assets = [loadShader(device, stripePassShader)]; + const assets = [loadShader(device, "shaders/wgsl/stripePass.wgsl")]; const loaded = (async () => { const [stripeShader] = await Promise.all(assets); diff --git a/js/webgpu/utils.js b/js/webgpu/utils.js index 0e4d96e..2e286f2 100644 --- a/js/webgpu/utils.js +++ b/js/webgpu/utils.js @@ -1,5 +1,4 @@ const loadTexture = async (device, cache, url) => { - const key = url; if (cache.has(key)) { return cache.get(key); @@ -63,9 +62,9 @@ const makeComputeTarget = (device, size, mipLevelCount = 1) => GPUTextureUsage.STORAGE_BINDING, }); -const loadShader = async (device, code /*text*/) => { - // const response = await fetch(url); - // const code = await response.text(); +const loadShader = async (device, url) => { + const response = await fetch(url); + const code = await response.text(); return { code, module: device.createShaderModule({ code }), diff --git a/package-lock.json b/package-lock.json index cd46033..229165f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,10 +19,12 @@ "@babel/preset-react": "^7.22.5", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-image": "^3.0.3", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-url": "^8.0.2", "babel-loader": "^9.1.3", + "copy-webpack-plugin": "^13.0.0", "html-webpack-plugin": "^5.5.3", "prettier": "^3.5.3", "raw-loader": "^4.0.2", @@ -1966,6 +1968,28 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@rollup/plugin-image": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-image/-/plugin-image-3.0.3.tgz", + "integrity": "sha512-qXWQwsXpvD4trSb8PeFPFajp8JLpRtqqOeNYRUKnEQNHm7e5UP7fuSRcbjQAJ7wDZBbnJvSdY5ujNBQd9B1iFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "mini-svg-data-uri": "^1.4.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", @@ -3406,6 +3430,43 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, + "node_modules/copy-webpack-plugin": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz", + "integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/core-js-compat": { "version": "3.32.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.0.tgz", @@ -5190,6 +5251,16 @@ "node": ">=6" } }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -6711,6 +6782,51 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index ea85bc8..debad07 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,12 @@ "@babel/preset-react": "^7.22.5", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-image": "^3.0.3", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-url": "^8.0.2", "babel-loader": "^9.1.3", + "copy-webpack-plugin": "^13.0.0", "html-webpack-plugin": "^5.5.3", "prettier": "^3.5.3", "raw-loader": "^4.0.2", diff --git a/rollup.config.mjs b/rollup.config.mjs index 8293306..3ca14f9 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -6,6 +6,7 @@ import url from "@rollup/plugin-url"; import { visualizer } from "rollup-plugin-visualizer"; // <- size report import terser from "@rollup/plugin-terser"; import { string } from "rollup-plugin-string"; +import image from "@rollup/plugin-image"; export default { input: "js/Matrix.js", @@ -14,7 +15,8 @@ export default { peerDepsExternal(), // auto-exclude peerDeps nodeResolve(), // so Rollup can find deps in node_modules string({ include: ["**/*.glsl"] }), - url({ include: ["**/*.png"], limit: 0 }), + string({ include: ["**/*.wgsl"] }), + image({ include: ["**/*.png"], limit: 0 }), babel({ exclude: "node_modules/**", // transpile JSX babelHelpers: "bundled", @@ -34,6 +36,7 @@ export default { ], output: [ { + inlineDynamicImports: true, file: "dist/index.cjs.js", format: "cjs", exports: "named", diff --git a/webpack.config.js b/webpack.config.js index d3ca087..7887383 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,7 @@ const webpack = require("webpack"); const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); +const CopyPlugin = require("copy-webpack-plugin"); module.exports = { mode: "development", @@ -12,19 +13,14 @@ module.exports = { exclude: /node_modules/, use: ["babel-loader"], }, + // { + // test: /\.css$/, + // use: ["style-loader", "css-loader"], + // }, { - test: /\.css$/, - use: ["style-loader", "css-loader"], - }, - { - test: /\.(png|j?g|svg|gif)?$/, + test: /\.(png|jpe?g|svg|glsl|wgsl)?$/, type: "asset/resource", }, - { - test: /\.(glsl|frag|vert|wgsl)$/i, - exclude: /node_modules/, - use: ["raw-loader"], - }, ], }, resolve: { @@ -41,6 +37,12 @@ module.exports = { template: path.resolve(__dirname, "public/index.html"), filename: "index.html", }), + new CopyPlugin({ + patterns: [ + { from: "assets", to: "assets" }, + { from: "shaders", to: "shaders" }, + ], + }), new webpack.HotModuleReplacementPlugin(), ], devServer: {