Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSONToBinary method #746

Closed
elalish opened this issue Dec 13, 2022 · 6 comments
Closed

Add JSONToBinary method #746

elalish opened this issue Dec 13, 2022 · 6 comments
Labels
feature New enhancement or request package:core
Milestone

Comments

@elalish
Copy link

elalish commented Dec 13, 2022

Is your feature request related to a problem? Please describe.
Apart from natural symmetry to the existing binaryToJSON method, I want to build a manifoldness extension which uses sparse accessors. Since this library doesn't support writing them, I'd like to use it to make everything else, write the JSON, edit it a bit to add a sparse accessor, then convert to binary, where this function would be helpful. If I read the JSON back, I'll lose the sparse accessor information.

Describe the solution you'd like
I believe just the JSONToBinary function is needed for this workflow. I can simply create a standard accessor for the sparse data to ensure its bufferView is written to the output, then reference its index in my JSON modification.

Describe alternatives you've considered
Adding sparse accessor write support to the library would also work, but I can see why that was avoided as it complicates the API. Or I can just write the GLB myself, but I'd much rather not duplicate all the nice work you've done here.

Additional context
I'm looking to make a lossless round-trip between glTF and my manifold library of Boolean mesh operations. If you join two meshes, they must still be separate draw calls (primitives) since they have different properties/textures. This requires the intersection vertices to be duplicated, since a single position has different vertex properties on the two sides. That in turn breaks manifoldness, as those vertices are no longer associated. It cannot be reliably fixed by merging identical verts, as manifoldness can also require multiple verts at the same position that are not attached topologically.

To fix this, I want to propose an extension where a single object is represented as a single mesh of multiple primitives, but an alternative single primitive is also available that references the same data but uses a sparse accessor on the indices to merge them. This primitive will not refer to mesh properties beyond position. In this way, a renderer has access to the standard set of primitives with properties, while a CAD app can access the manifold verts without extra properties and have the relation between the two without duplication.

@elalish elalish added the feature New enhancement or request label Dec 13, 2022
@donmccurdy
Copy link
Owner

While it's not currently exposed as a separate method, packing JSON into a GLB is defined concisely here:

const header = new Uint32Array([0x46546c67, 2, 12]);
const jsonText = JSON.stringify(json);
const jsonChunkData = BufferUtils.pad(BufferUtils.encodeText(jsonText), 0x20);
const jsonChunkHeader = BufferUtils.toView(new Uint32Array([jsonChunkData.byteLength, 0x4e4f534a]));
const jsonChunk = BufferUtils.concat([jsonChunkHeader, jsonChunkData]);
header[header.length - 1] += jsonChunk.byteLength;
const binBuffer = Object.values(resources)[0];
if (!binBuffer || !binBuffer.byteLength) {
return BufferUtils.concat([BufferUtils.toView(header), jsonChunk]);
}
const binChunkData = BufferUtils.pad(binBuffer, 0x00);
const binChunkHeader = BufferUtils.toView(new Uint32Array([binChunkData.byteLength, 0x004e4942]));
const binChunk = BufferUtils.concat([binChunkHeader, binChunkData]);
header[header.length - 1] += binChunk.byteLength;
return BufferUtils.concat([BufferUtils.toView(header), jsonChunk, binChunk]);

But there is a limitation here that might matter for your context. The JSON is expected to have been generated with the Format.GLB option like this...

const { json, resources } = await this.writeJSON(doc, { format: Format.GLB });

... which packs textures and accessors into a single .bin resource for the GLB. Packing the GLB without constructing the full Document graph is going to be much harder, because it's complex to track where accessors are used (e.g. within extensions) to determine which buffer views they should be assigned. But if you're going to add another accessor to the JSON, you'd be limited to just appending bytes to the end of that buffer, unless you want to very carefully update existing accessors and buffer views.


Support for writing sparse accessors is something I'd like the library to include (see #351) but this hasn't made it into the roadmap yet.

@elalish
Copy link
Author

elalish commented Dec 13, 2022

Thanks, that's very helpful! I think this will still work because I plan to make a dedicated standard accessor for the index/value sparse pairs to make them easier to access on their own. I believe including that will pack all the needed data into the .bin, so that I can safely modify the JSON to point to those same bufferViews and run your lines above. Do I have that right?

@donmccurdy
Copy link
Owner

Right, I think you could call writeJSON as shown above and then:

  1. extend the Uint8Array buffer in resources with your accessor data
  2. add new accessor definition(s) to json.accessors
  3. add new buffer view definition(s) to json.bufferViews
  4. write to GLB

@elalish
Copy link
Author

elalish commented May 3, 2023

Your pointers were super helpful in my early prototyping, but it turns out your extensions API is plenty flexible enough to do all the sparse manipulations I need without any serious hacking. Nice architecture! Feel free to close this, since it appears to be unnecessary.

@donmccurdy donmccurdy modified the milestones: Backlog, v4 May 21, 2023
@donmccurdy
Copy link
Owner

donmccurdy commented Feb 24, 2024

I've had to decide against adding this method – see details in #1280. I implemented complete-ish code for packing JSON into a GLB there, but a fundamental problem is that some extensions (notably Meshopt) contain references to buffers, and define empty fallback buffers, which need to be repacked in GLB. It's not terribly complicated but would involve hard-coding extension-dependent logic, which I'm hoping to avoid in the /core module.

The library can handle this conversion if it parses the JSON into a Document, because the extension implementations are then initialized and handle their own resources, but a jsonToBinary method wouldn't have extension implementations, and can't cleanly deal with these buffers as a result. So I think there would be too many broken cases if I add this unfortunately.

For now the way to convert a .gltf to .glb would still be something like:

import { unpartition } from '@gltf-transform/functions';

// (A), in memory JSON
const document = io.readJSON(jsonDoc);
await document.transform(unpartition());
const glb = await io.writeBinary(document);

// (B), file on disk or network path
const document = await io.read('scene.gltf');
await document.transform(unpartition());
const glb = await io.writeBinary(document);

But this does require implementations of any extensions being used.

@donmccurdy donmccurdy closed this as not planned Won't fix, can't repro, duplicate, stale Feb 24, 2024
@elalish
Copy link
Author

elalish commented Feb 26, 2024

No prob, thanks for the explanation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New enhancement or request package:core
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants