Skip to content

Instantly share code, notes, and snippets.

@neftaly
Last active August 6, 2025 12:45
Show Gist options
  • Save neftaly/c346b238c09a3da03df86ce9f9781839 to your computer and use it in GitHub Desktop.
Save neftaly/c346b238c09a3da03df86ce9f9781839 to your computer and use it in GitHub Desktop.
Draco compression + gltf-transform + avif texture encoding in browser (with vite + ts)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/webp" href="/favicon.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- we have to manually download a copy of draco_encoder (or include it from three). Google does not CDN host encoder anywhere! only decoder -->
<script src="/draco_encoder.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
{
"dependencies": {
"@gltf-transform/core": "^4.2.1",
"@gltf-transform/extensions": "^4.2.1",
"@gltf-transform/functions": "^4.2.1",
"@jsquash/avif": "^2.1.1",
"@types/three": "^0.179.0",
"meshoptimizer": "^0.24.0",
"three": "^0.179.1",
"three-stdlib": "^2.36.0"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/draco3d": "^1.4.10",
"@types/node": "^24.2.0",
"eslint": "^9.30.1",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4"
}
}
import { WebIO } from "@gltf-transform/core";
import {
prune,
dedup,
draco,
join,
center,
weld,
simplify,
textureCompress,
} from "@gltf-transform/functions";
import { MeshoptSimplifier } from "meshoptimizer";
import { ALL_EXTENSIONS, EXTTextureAVIF } from "@gltf-transform/extensions";
// Preemptively download AVIF encoder
(async () => {
try {
const { encode } = await import("@jsquash/avif");
await encode(new ImageData(1, 1));
} catch (e) {
void e;
}
})();
const pngToAvif = async (png: Uint8Array) => {
// Create canvas to get ImageData from PNG
const img = new Image();
const blob = new Blob([png], { type: "image/png" });
img.src = URL.createObjectURL(blob);
await new Promise((resolve) => (img.onload = resolve));
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx?.drawImage(img, 0, 0);
const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height);
URL.revokeObjectURL(img.src);
if (!imageData) return png;
// Convert to AVIF
const { encode } = await import("@jsquash/avif");
const output = await encode(imageData, { quality: 60 });
return new Uint8Array(output);
};
const fakeSharpStub = (srcImage: Uint8Array) => {
const instance = new Proxy(
{},
{
get(_, prop) {
if (prop === "toBuffer") {
return () => pngToAvif(srcImage);
}
return () => instance;
},
},
);
return instance;
};
const io = new WebIO().registerExtensions(ALL_EXTENSIONS).registerDependencies({
"draco3d.encoder": new DracoEncoderModule(),
});
export const transformGltf = async (gltfRaw: ArrayBuffer) => {
const gltf = new Uint8Array(gltfRaw);
const document = await io.readBinary(gltf);
document.createExtension(EXTTextureAVIF);
await MeshoptSimplifier.ready;
await document.transform(
textureCompress({
encoder: fakeSharpStub,
targetFormat: "avif",
}),
center(),
dedup(),
prune(),
join({ keepNamed: false }),
weld({}),
simplify({ simplifier: MeshoptSimplifier, ratio: 0, error: 0.001 }),
draco({ method: "edgebreaker" }),
);
const glb = await io.writeBinary(document);
return glb;
};
/// <reference types="vite/client" />
declare const DracoEncoderModule: typeof import('draco3d').DracoEncoderModule;
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
optimizeDeps: {
exclude: ["@jsquash/avif"], // gives us WASM problems if we include, however now we need to manually preload
},
worker: {
format: "es",
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment