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

feat(gltf): extensions, encoding feature metadata #2972

Merged
merged 18 commits into from
Sep 6, 2024

Conversation

mspivak-actionengine
Copy link
Collaborator

No description provided.

*/
export function encodeExtMeshFeatures(
scenegraph: GLTFScenegraph,
featureIdArray: number[] | TypedArray,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally we would create the extension with these fields, and then call encode() on the extension.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* @returns primitive updated
* @see https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_mesh_features
*/
export function encodeExtMeshFeatures(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention in the extension files is that each extension exports decode and encode functions. That way one can work with a list of extensions and call this function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Collaborator

@belom88 belom88 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove tile-converter changes. We will change the tile-converter in separate RPs

modules/gltf/src/index.ts Outdated Show resolved Hide resolved
}

function convertBuffersToBase64(gltf, {firstBuffer = 0} = {}) {
if (gltf.buffers && gltf.buffers.length > firstBuffer) {
throw new Error('encodeGLTF: multiple buffers not yet implemented');
if (gltf.buffers && gltf.buffers.length > firstBuffer 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this logic has changed? Probably, breaking change

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the function name is "convertBuffersToBase64"? It doesn't convert anything, just check number of buffers

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/**
* Decodes feature metadata from extension.
* @param {GLTFScenegraph} scenegraph - Instance of the class for structured access to GLTF data.
* @param {GLTFLoaderOptions} options - GLTFLoader options.
*/
function decodeExtMeshFeatures(scenegraph: GLTFScenegraph, options: GLTFLoaderOptions): void {
export function decodeExtMeshFeatures(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it exported? We don't need it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Encoding data
*/

export function encodeExtMeshFeatures(scenegraph: GLTFScenegraph, options: GLTFWriterOptions) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need to export it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const typedArray = new Uint32Array(featureIds);
const tableIndex = 8;

createExtMeshFeatures(scenegraph, primitive, typedArray, tableIndex);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extensions should have interfaces "decode" and "encode". createExtMeshFeature is not necessary

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed the removing this function results in the necessity to put all input data into GLTF which complicates the implementation.


test('gltf#EXT_structural_metadata - Should encode', async (t) => {
const scenegraph = new GLTFScenegraph();
const tableIndex = createExtStructuralMetadata(scenegraph, ATTRIBUTES);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extensions should work through encode and decode. createExtStructuralMetadata is not necessary and have to be included into encode workflow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed the removing this function results in the necessity to put all input data into GLTF which complicates the implementation.

@ibgreen
Copy link
Collaborator

ibgreen commented Aug 27, 2024

@mspivak-actionengine i this ready for review?

@mspivak-actionengine
Copy link
Collaborator Author

@mspivak-actionengine i this ready for review?

@ibgreen Please give me some time to fix the Victor's comments, which might change some logic. I'll let you know when ready. Sorry for that.

Copy link
Collaborator

@belom88 belom88 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest landing EXT_MESH_FEATURES in this PR and separate EXT_STRUCTURAL_METADATA in an another one.

/**
* Creates ExtMeshFeatures, creates a featureId containing feature ids provided.
* @param scenegraph - Instance of the class for structured access to GLTF data.
* @primitive - target primitive instance that will contain the extension
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @primitive - target primitive instance that will contain the extension
* @param primitive - target primitive instance that will contain the extension

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -75,6 85,8 @@ function processMeshPrimitiveFeatures(
// Process "Feature ID by Texture Coordinates"
else if (typeof featureId.texture !== 'undefined' && options?.gltf?.loadImages) {
featureIdData = getPrimitiveTextureData(scenegraph, featureId.texture, primitive);
// Clean up input data
featureId.texture = undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this for encoding? I'd remove

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for decoding. I have removed it and changed the encoding part to make sure that the featureId object doesn't contain anything that can come from the original data (round-trip scenario as an example).

primitive.extensions[EXT_MESH_FEATURES_NAME] = extension;
}

const featureIds: GLTF_EXT_mesh_features_featureId[] = extension.featureIds;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit

Suggested change
const featureIds: GLTF_EXT_mesh_features_featureId[] = extension.featureIds;
const { featureIds } = extension;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// Search for all "_FEATURE_ID_n" attribures in the primitive provided if any.
// If there are some, e.g. "_FEATURE_ID_0", "_FEATURE_ID_1",
// we will add a new one with the name "_FEATURE_ID_2"
const attrs = Object.keys(attributes).filter((item) => item.indexOf(prefix) !== -1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we need startsWith logic that is more precise.

Suggested change
const attrs = Object.keys(attributes).filter((item) => item.indexOf(prefix) !== -1);
const attrs = Object.keys(attributes).filter((item) => item.indexOf(prefix) === 0);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

export function createExtMeshFeatures(
scenegraph: GLTFScenegraph,
primitive: GLTFMeshPrimitive,
featureIdArray: TypedArray,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Output of decode is NumericArray. Lets consume similar data here. Can we narrow to this type in GLTF_EXT_mesh_features_featureId?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (featureId.data) {
const {accessorKey, index} = createAccessorKey(primitive.attributes);
featureId.attribute = index;
const typedArray = new Uint32Array(featureId.data as number[]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const typedArray = new Uint32Array(featureId.data as number[]);
const typedArray = new Uint32Array(featureId.data as NumericArray);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

*/
export function createExtStructuralMetadata(
scenegraph: GLTFScenegraph,
typedFeatureAttributes: TypedFeatureAttribute[],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extension should work with encode function (similar to EXT_MESH_FEATURES).
We need th create... function but locate it after encode.
featureAttributes is an I3S entities. We need something more generic. Look at the propertyTable which is an output of this extension decode.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be addressed in a separate PR for the EXT_structural _metadata.

Copy link
Collaborator

@belom88 belom88 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON.parse(JSON.stringify is a hack. Try to avoid it in tests.

Comment on lines 175 to 177
// Clean up featureId object.
// Everything that could come from the original extension in case of round-trip decode/encode operations should be deleted.
// We need make sure the featureId object is clean.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What this comment for? You say "clean up" and then add new objects to the gltf. Forgot a real clean up?


test('gltf#EXT_mesh_features - Should decode', async (t) => {
const options = {gltf: {loadImages: true, loadBuffers: true}};
const gltf = JSON.parse(JSON.stringify(GLTF_WITH_EXTENSION));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid affecting test data, the better solution is making a function wrapper:

  const getGltfWithExtension = () => ({
    buffers: [
      {
        arrayBuffer: [],
        byteOffset: 0,
        byteLength: 128
      }
    ],
    images: [
    ....
  })

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks for the idea.

const gltf = JSON.parse(JSON.stringify(GLTF_WITH_EXTENSION));
gltf.buffers[0].arrayBuffer = new Uint8Array(binaryBufferDataAlligned).buffer;
await decodeExtensions(gltf, options);
// The clone of GLTF_WITH_EXTENSION has been modified
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment before an empty line is confusing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

encodeExtensions(scenegraph.gltf, {});

// The clone of GLTF_WITH_EXTENSION has been modified
t.deepEqual(JSON.stringify(primitive), JSON.stringify(PRIMITIVE_EXPECTED));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deepEqual should work without JSON.stringify

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// The clone of GLTF_WITH_EXTENSION has been modified
const gltfBin = encodeExtensions(gltf, options);

const json = deleteUndefined(gltfBin.json);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add undefined values to the expected object, probably

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

modules/gltf/src/gltf-writer.ts Outdated Show resolved Hide resolved
@mspivak-actionengine
Copy link
Collaborator Author

@ibgreen Hi Ib. The PR is ready. Could you please review it?

Copy link
Collaborator

@ibgreen ibgreen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conditionally approved.

Required

  • Update docs and release notes

Nice to have

  • A bit more comments in the code to guide a user who is not familiar with the extension.

return;
}
const featureIds: GLTF_EXT_mesh_features_featureId[] = extension.featureIds;
featureIds.forEach((featureId, ind) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type out variable names

Suggested change
featureIds.forEach((featureId, ind) => {
featureIds.forEach((featureId, index) => {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (!extension) {
return;
}
const featureIds: GLTF_EXT_mesh_features_featureId[] = extension.featureIds;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to clone the feature ids so that we don't mutate the existing data structure?

Suggested change
const featureIds: GLTF_EXT_mesh_features_featureId[] = extension.featureIds;
const featureIds: GLTF_EXT_mesh_features_featureId[] = {...extension.featureIds};

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid not. While encoding/decoding extensions we use temporal fields in the structure and change the data accordingly.

});
}

function createAccessorKey(attributes: {}) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add documentation and types to every function

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Collaborator

@belom88 belom88 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic of scenegraph was:

  1. Create scenegraph;
  2. Make changes;
  3. Call createBinaryChunk in the current scope in order to save buffer changes

The logic of sceengraph now is not obvious. You have to do

  scenegraph.gltf.buffers.push({
        arrayBuffer: typedArray.buffer,
        byteOffset: typedArray.byteOffset,
        byteLength: typedArray.byteLength
      });

that is outside of scenegraph in the extension. Whithout this piece of code your approach won't work.
I'd preserve the logic from our previous inspection to save time. We can support deferred call of createBinaryChunk in a separate PR because we should think a little bit more on it.

Comment on lines 40 to 42
const scenegraph = new GLTFScenegraph(gltf);
scenegraph.createBinaryChunk();
const gltfToEncode = scenegraph.gltf;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need createBinaryChunk here. It's done in the extension already.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 25 to 28
// export function encode(gltfData: {json: GLTF}, options: GLTFWriterOptions): void {
// const scenegraph = new GLTFScenegraph(gltfData);
// encodeExtMeshFeatures(scenegraph, options);
// }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean up

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@mspivak-actionengine mspivak-actionengine merged commit f0c1a28 into master Sep 6, 2024
1 check passed
@mspivak-actionengine mspivak-actionengine deleted the ms/ext-mesh-i3s-to-3dtiles branch September 6, 2024 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants