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.

Revisions

  1. neftaly renamed this gist Aug 6, 2025. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion gistfile1.txt → index.html
    Original file line number Diff line number Diff line change
    @@ -4,7 +4,6 @@
    <meta charset="UTF-8" />
    <link rel="icon" type="image/webp" href="/favicon.webp" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Woodcutter</title>
    <!-- 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>
  2. neftaly created this gist Aug 6, 2025.
    15 changes: 15 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    <!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" />
    <title>Woodcutter</title>
    <!-- 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>
    22 changes: 22 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,22 @@
    {
    "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"
    }
    }
    89 changes: 89 additions & 0 deletions transformGltf.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,89 @@
    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;
    };
    3 changes: 3 additions & 0 deletions vite-env.d.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    /// <reference types="vite/client" />

    declare const DracoEncoderModule: typeof import('draco3d').DracoEncoderModule;
    13 changes: 13 additions & 0 deletions vite.config.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@
    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",
    },
    });