donmccurdy/glTF-Transform

Option to resolve resources from JSON while reading

javagl opened this issue · 3 comments

The fact that this functionality does not seem to exists lets me assume that 1. I'm overlooking something obvious in the API, or 2. that there is a good reason why it does not exist...


Problem description:

I have a JSON object that was parsed from a glTF JSON, as in
const gltf = JSON.parse(jsonString);

Now I want to create a Document from that.

From the JSONDocument description, that could be done with

const jsonDocument = {
  json: gltf,
  resources: {
    `buffer.bin': bufferWithDataForBuffer,
    `image.png': bufferWithDataForImage,
  }
};
const document = await io.readJSON(jsonDocument);

The problem is: I don't know what to put into the resources.


Possible solution (currently):

One could probably go through all buffer and image objects, check the uris, and fill the resources accordingly. But ...

  • that's cumbersome
  • it is impossible cover any uri that might be hidden, maybe in some extension that refers to external resources
  • it is necessary to load all data upfront and keep it in memory (EDIT: and maybe not all of this data is actually used ...)

Desired solution:

It should be possible to resolve the resources at read-time. This could roughly be achieved with a function like this in the IO classes:

public async readJsonAndResources(
    json: GLTF.IGLTF, 
    resourceResolver: (uri: string) => Promise<Buffer | undefined>
  ): Promise<Document> {
  
  // Instead of looking up each `buffer/image.uri` 
  // in the `resources` map, they are resolved with
  const buffer = await resourceResolver(imageOrBuffer.uri);
  ...
}

This could even allow to lift what appears to be a current limitation, namely that binary glTF with external resources do not seem to be supported.

Hi @javagl — I'm on board with the idea. In my own applications where I use this, like gltf.report, the user has probably dropped a folder full of files into the page, and so I just put all the available files into the resources and let the I/O utility read the ones it needs. But if you don't already have everything in memory, and need to traverse the JSON to find URIs, I can see that's probably not appealing, especially as extensions add support for more external resources.

Currently the library only reads external resources when given a file path or URL, since it needs that to resolve the resource URLs. glTF Transform does support resolving external resources in GLBs, if the GLB is loaded from a path or URL itself.

My thought is that the in-memory reading methods (readBinary and readJSON) should take an optional second argument, like...

const document = await io.readBinary(glb, resolver);

... so pretty much what you suggested above. If omitted, an error would be thrown – I don't think we want to try to guess the paths or URLS without that.

Yes, "relativizing" anything (or more generally: the whole implementation of that magic resolver function) would be left to the caller.

In many cases (particularly in a local Node.js/file based environment), this could be really simple as resolver = (uri: string) => fs.readFileSync(path.resolve(base, uri));. In other contexts, the caller will have to take care of CORS, tokens, and whatnot, but it would still be doable.

The question of error handling may require more thought. I just sketched that Promise<Buffer| undefined> there, but maybe there's a better solution. That could go the path of some "error callback", or ... just throwing immediately instead of having to deal with that undefined there...

For the type declaration, I'd go with Promise<Uint8Array> - the promise should always resolve to a Buffer / Uint8Array, and so usage would be:

// web
const document = await io.readBinary(glb, async (path) => {
  const response = await fetch(path);
  return new Uint8Array(await response.arrayBuffer());
});

// node.js
import fs from 'node:fs/promises';
const document = await io.readBinary(glb, fs.readFile);

If you did happen to return undefined instead of a rejected Promise, you'll still get an error, just probably a less helpful error. 🙂