KhronosGroup / glTF-Sample-Assets

To store all models and other assets related to glTF
257 stars 30 forks source link

Optimized iridescence spheres #6

Closed javagl closed 10 months ago

javagl commented 1 year ago

The IridescenceDielectricSpheres and IridescenceMetallicSpheres models contain 343 spheres that only differ in the material that is applied. The .bin file of these assets had >10MB, because the geometry data for these spheres was stored 343 times.

This PR replaces the asset with one where the .bin file contains the geometry data only once, and only has 41 KB (!). There is no visual difference between the assets, as far as I can tell.

The assets have been created with the following snippet, using the dedup() function of glTF-Transform:

import path from "path";
import fs from "fs";
import { NodeIO } from "@gltf-transform/core";
import { dedup } from "@gltf-transform/functions";
import { KHRONOS_EXTENSIONS } from "@gltf-transform/extensions";

const baseDir = "C:/glTF-Sample-Assets/Models/";

function ensureDirectoryExists(fileName: string) {
  const directory = path.dirname(fileName);
  if (!fs.existsSync(directory)) {
    fs.mkdirSync(directory, { recursive: true });
  }
}

async function runOptimize(modelName: string) {
  const inputDir = baseDir + modelName + "/glTF/";
  const outputDir = baseDir + modelName + "/glTF-Optimized/";

  const inputFileName = inputDir + modelName + ".gltf";
  const outputFileName = outputDir + modelName + ".gltf";

  const io = new NodeIO().registerExtensions(KHRONOS_EXTENSIONS);
  const document = await io.read(inputFileName);
  await document.transform(dedup());
  const jsonDocument = await io.writeJSON(document);

  ensureDirectoryExists(outputFileName);
  fs.writeFileSync(outputFileName, JSON.stringify(jsonDocument.json, null, 2));
  for (const uri of Object.keys(jsonDocument.resources)) {
    const resource = jsonDocument.resources[uri];
    const resourceFileName = path.join(outputDir, uri);
    ensureDirectoryExists(resourceFileName);
    fs.writeFileSync(resourceFileName, resource);
  }
}

async function run() {
  await runOptimize("IridescenceDielectricSpheres");
  await runOptimize("IridescenceMetallicSpheres");
}

run();
echadwick-artist commented 1 year ago

Thanks for these optimizations!

Both have several issues reported in the glTF Validator:

    "issues": {
        "numErrors": 1,
        "numWarnings": 0,
        "numInfos": 343,
        "numHints": 0,
        "messages": [
            {
                "code": "UNUSED_OBJECT",
                "message": "This object may be unused.",
                "severity": 2,
                "pointer": "/meshes/0/primitives/0/attributes/TEXCOORD_0"
            },
...
            {
                "code": "IO_ERROR",
                "message": "Resource not found (textures/guides.png).",
                "severity": 0,
                "pointer": "/images/0/uri"
            }
        ],

I wonder if the texcoords could be removed, and perhaps packing the asset into a GLB might solve the texture error?

javagl commented 1 year ago

That texture error surprises me a bit. This is actually not caused by an error in the model. It is caused by the fact that the textures are contained in a /textures subdirectory. I thought that the validator was able to handle that.

When dragging-and-dropping the asset (i.e. the .gltf and .bin file, together with the textures directory) into https://gltf-viewer.donmccurdy.com/ , then the validation report does not contain this error.

I opened https://github.com/KhronosGroup/glTF-Validator/issues/208 for tracking this.


The point about the UNUSED_OBJECT messages is valid. To be fair: Before this optimization, the models caused a whopping 343 infos and 1040 (!) hints in the validation report, so that already was an improvement. But when I'm already at it anyhow, I also removed the unnecessary texture coordinates. With the new state, the models should cause no issues and no warnings (and of course, no errors, e.g. when they are dragged into the https://gltf-viewer.donmccurdy.com/ that properly resolves the textures).


For completeness, here's the modified code (with the somewhat hacky part to remove the TEXCOORD_0 attributes) to create the optimized versions from the original ones.

import path from "path";
import fs from "fs";
import { NodeIO } from "@gltf-transform/core";
import { dedup, prune } from "@gltf-transform/functions";
import { KHRONOS_EXTENSIONS } from "@gltf-transform/extensions";

const baseDir = "C:/glTF-Sample-Assets/Models/";

function ensureDirectoryExists(fileName: string) {
  const directory = path.dirname(fileName);
  if (!fs.existsSync(directory)) {
    fs.mkdirSync(directory, { recursive: true });
  }
}

async function runOptimize(modelName: string) {
  const inputDir = baseDir + modelName + "/glTF/";
  const outputDir = baseDir + modelName + "/glTF-Optimized/";

  const inputFileName = inputDir + modelName + ".gltf";
  const outputFileName = outputDir + modelName + ".gltf";

  const io = new NodeIO().registerExtensions(KHRONOS_EXTENSIONS);
  const document = await io.read(inputFileName);
  await document.transform(dedup());
  await document.transform(prune());
  const meshes = document.getRoot().listMeshes();
  for (const mesh of meshes) {
    const primitives = mesh.listPrimitives();
    for (const primitive of primitives) {
      if (primitive.getMaterial()?.getBaseColorTexture() === null) {
        primitive.setAttribute("TEXCOORD_0", null);
      }
    }
  }
  const jsonDocument = await io.writeJSON(document);

  ensureDirectoryExists(outputFileName);
  fs.writeFileSync(outputFileName, JSON.stringify(jsonDocument.json, null, 2));
  for (const uri of Object.keys(jsonDocument.resources)) {
    const resource = jsonDocument.resources[uri];
    const resourceFileName = path.join(outputDir, uri);
    ensureDirectoryExists(resourceFileName);
    fs.writeFileSync(resourceFileName, resource);
  }
}

async function run() {
  await runOptimize("IridescenceDielectricSpheres");
  await runOptimize("IridescenceMetallicSpheres");
}

run();