From ef24ed0e9cf5cde4e52c8b1a30a71e475660b3fe Mon Sep 17 00:00:00 2001 From: Noeri Huisman <8823461+mrxz@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:16:23 +0200 Subject: [PATCH] Use OffscreenCanvas and WebGL2 context to decode webp images (#90) --- package-lock.json | 16 --------------- package.json | 1 - src/pcsogs.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 876334b..5e07270 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.4", "license": "MIT", "dependencies": { - "@jsquash/webp": "^1.5.0", "fflate": "^0.8.2" }, "devDependencies": { @@ -707,15 +706,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@jsquash/webp": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jsquash/webp/-/webp-1.5.0.tgz", - "integrity": "sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw==", - "license": "Apache-2.0", - "dependencies": { - "wasm-feature-detect": "^1.2.11" - } - }, "node_modules/@microsoft/api-extractor": { "version": "7.52.8", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.8.tgz", @@ -2900,12 +2890,6 @@ "dev": true, "license": "MIT" }, - "node_modules/wasm-feature-detect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", - "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", - "license": "Apache-2.0" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 66ed0f5..e8fbafe 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "spark-internal-rs": "file:rust/spark-internal-rs/pkg" }, "dependencies": { - "@jsquash/webp": "^1.5.0", "fflate": "^0.8.2" }, "keywords": ["3d", "three.js", "gsplats", "3dgs", "gaussian", "splats"] diff --git a/src/pcsogs.ts b/src/pcsogs.ts index 8435d0f..9b73fa6 100644 --- a/src/pcsogs.ts +++ b/src/pcsogs.ts @@ -1,4 +1,3 @@ -import { decode as decodeWebp } from "@jsquash/webp"; import type { PcSogsJson } from "./SplatLoader"; import { computeMaxSplats, @@ -149,9 +148,56 @@ export async function unpackPcSogs( return { packedArray, numSplats, extra }; } +// WebGL context for reading raw pixel data of WebP images +let offscreenGlContext: WebGL2RenderingContext | null = null; + async function decodeImage(fileBytes: ArrayBuffer) { - const { data: rgba, width, height } = await decodeWebp(fileBytes); - return { rgba, width, height }; + if (!offscreenGlContext) { + const canvas = new OffscreenCanvas(1, 1); + offscreenGlContext = canvas.getContext("webgl2"); + if (!offscreenGlContext) { + throw new Error("Failed to create WebGL2 context"); + } + } + + const imageBlob = new Blob([fileBytes]); + const bitmap = await createImageBitmap(imageBlob, { + premultiplyAlpha: "none", + }); + + const gl = offscreenGlContext; + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + + const framebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + texture, + 0, + ); + + const data = new Uint8Array(bitmap.width * bitmap.height * 4); + gl.readPixels( + 0, + 0, + bitmap.width, + bitmap.height, + gl.RGBA, + gl.UNSIGNED_BYTE, + data, + ); + + gl.deleteTexture(texture); + gl.deleteFramebuffer(framebuffer); + + return { rgba: data, width: bitmap.width, height: bitmap.height }; } async function decodeImageRgba(fileBytes: ArrayBuffer) {