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 glTF exporter #3366

Merged
merged 18 commits into from
Sep 29, 2022
Merged

Add glTF exporter #3366

merged 18 commits into from
Sep 29, 2022

Conversation

willeastcott
Copy link
Contributor

@willeastcott willeastcott commented Jul 25, 2021

Implements a glTF exporter. Currently supports:

  • Meshes
  • Materials
  • Textures
  • Lights
  • Cameras
  • GLB export
  • glTF (JSON) export

API is:

new pcx.GltfExporter().build(entity).then((arrayBuffer) => {
    const blob = new Blob([arrayBuffer], { type: 'application/octet-stream' });

    // download the blob here
}).catch(console.error);

also exposed a function on the engine's BoundingBox to evaluate min and max for an array of vertices:

// this allows to easily compute min and max, instead of getting those from mesh.aabb, as due to
// storing center + extents, that is not precise enough and we get gltf validation warnings.
BoundingBox.computeMinMax()

New engine example GltfExport converts source scene created from multiple glbs:
Screenshot 2022-09-28 at 11 55 49

into a single glb (limited material & texture support)
Screenshot 2022-09-28 at 11 55 34

I confirm I have read the contributing guidelines and signed the Contributor License Agreement.

@willeastcott willeastcott added enhancement area: graphics Graphics related issue labels Jul 25, 2021
@willeastcott willeastcott self-assigned this Jul 25, 2021
@kungfooman
Copy link
Collaborator

kungfooman commented Aug 17, 2021

I try to get this to work by using the download function from here: https://github.com/playcanvas/playcanvas-gltf/blob/a96d6b307fb5c8c3a3b6c1a926e0788bc1932128/src/export-gltf.js#L48

function download(buffers) {
  var element = document.createElement('a');
  var blob = new Blob(buffers, { type: "octet/stream" });
  element.href = URL.createObjectURL(blob);
  element.download = 'scene.glb';
  element.style.display = 'none';
  document.body.appendChild(element);
  element.click();
  document.body.removeChild(element);
}
// Export as scene.glb
const exporter = new GltfExporter();
const arrayBuffer = exporter.buildGlb(entity):
download([arrayBuffer]);

I can download the generated scene.glb and the hierarchy seems correct, but when I load it into PlayCanvas Viewer, it just breaks something (e.g. skybox turns off) and I can't see any models:

image

Am I doing something wrong or does it not work for meshes yet?

Generated file: scene.zip

EDIT

Just thought about using this validator and there are quite some problems left:

https://gltf-viewer.donmccurdy.com/

image

scripts/exporters/gltf-exporter.js Outdated Show resolved Hide resolved
scripts/exporters/gltf-exporter.js Outdated Show resolved Hide resolved
Co-authored-by: Hermann Rolfes <lama12345@gmail.com>
Copy link
Collaborator

@kungfooman kungfooman left a comment

Choose a reason for hiding this comment

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

One entity can have multiple meshInstances and each meshInstance.node (GraphNode) belongs to its own JSON glTF node. So first all JSON nodes need to be written and then the resources.meshInstances can be iterated to assign the mesh ID's to the proper JSON nodes:

Comment on lines 234 to 244
if (entity.render && entity.render.enabled) {
entity.render.meshInstances.forEach((meshInstance) => {
node.mesh = resources.meshInstances.indexOf(meshInstance);
});
}

if (entity.model && entity.model.enabled) {
entity.model.meshInstances.forEach((meshInstance) => {
node.mesh = resources.meshInstances.indexOf(meshInstance);
});
}
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
if (entity.render && entity.render.enabled) {
entity.render.meshInstances.forEach((meshInstance) => {
node.mesh = resources.meshInstances.indexOf(meshInstance);
});
}
if (entity.model && entity.model.enabled) {
entity.model.meshInstances.forEach((meshInstance) => {
node.mesh = resources.meshInstances.indexOf(meshInstance);
});
}

Copy link
Contributor

Choose a reason for hiding this comment

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

This will be addressed in follow up PRs

});
}
}

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
assignMeshIDs(resources, json) {
var meshInstances = resources.meshInstances;
// Assign meshes to JSON nodes based on meshInstance.node
meshInstances.forEach(meshInstance => {
const entityIndex = resources.entities.indexOf(meshInstance.node);
if (entityIndex != -1) {
json.nodes[entityIndex].mesh = meshInstances.indexOf(meshInstance);
} else {
console.warn('GltfExporter#assignMeshIDs> meshInstance referring to unexported json node');
}
});
}

Copy link

@CynthiaXJia CynthiaXJia Nov 5, 2021

Choose a reason for hiding this comment

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

Since there is still only one node for each entity, this will result in each meshInstance in an entity overwriting json.nodes[entityIndex].mesh as we loop through them, right? Should we be checking for meshInstances in models/renders in writeNodes and creating a new json node for each?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you're right, @CynthiaXJia, there is a problem here. I'm not 100% sure of the right solution at the moment, but what you suggest sounds sensible.

Copy link
Contributor

Choose a reason for hiding this comment

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

This will be addressed in follow up PRs

Copy link
Contributor

Choose a reason for hiding this comment

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

this is addressed here now: #4683

this.writeBuffers(resources, json);
this.writeBufferViews(resources, json);
this.writeCameras(resources, json);
this.writeNodes(resources, json);
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
this.writeNodes(resources, json);
this.writeNodes(resources, json);
this.assignMeshIDs(resources, json);

@kungfooman
Copy link
Collaborator

kungfooman commented Aug 22, 2021

I reworked the code to deinterleave the vertex buffer data and tested it on SeeMore (as non-trivial test case):

The glTF can be opened in Blender (for some reason there are still errors that prevent full loading):

image

Or in PlayCanvas Viewer:

image

SeeMore demo: https://playcanv.as/p/MflWvdTW/

SeeMore.glb file: Seemore_1629624802680_7.zip

JavaScript code for quick testing: https://github.com/KILLTUBE/gltf/blob/master/src/gltf-exporter.js (just pick frame in devtools and copy&paste)

It would be nice to get to the point of a fully working and error free SeeMore glTF/glb file, currently I can't even get an error report from KhronosGroup/glTF-Validator (running out of memory)

@willeastcott
Copy link
Contributor Author

Cool to see you experimenting with this. I was wondering why in your version you are de-interleaving the vertex buffer data. In my version, I'm trying to leave the vertex buffer data untouched. Ideally, it will handle both cases. Interleaved vertex buffer data is generally considered to be more efficient.

@kungfooman
Copy link
Collaborator

Thanks! I de-interleaved the vertex buffer mostly because I just wanted it to work and then I am mostly using playcanvas-gltf which still has problems parsing interleaved data: playcanvas/playcanvas-gltf#7

Once it generated a GLB file it can just be loaded into Blender and then Blender can do the interleaving and draco compression if necessary. I also found the stride logic somewhat confusing, so I just wanted to make it easier to understand

@ghost
Copy link

ghost commented Sep 13, 2021

Hi Will,
Any rough estimates on when we could have a working version of the gltf-exporter (the mesh and the materials) in the engine?
Sorry, it's just that there is something in our project we can REALLY use this for but I don't have the required skills to help debug this to be able to contribute.

@yaustar
Copy link
Contributor

yaustar commented Oct 5, 2021

@tanaydimriepigraph Unfortunately, Will is backlogged on some internal stuff and is not able to recommence work on this just yet. He is eager to get back to it though.

@mvaligursky mvaligursky marked this pull request as ready for review September 28, 2022 10:58
@mvaligursky mvaligursky requested a review from a team September 28, 2022 10:58
@mvaligursky mvaligursky self-assigned this Sep 28, 2022
@mvaligursky mvaligursky merged commit ab6cb9f into main Sep 29, 2022
@mvaligursky mvaligursky deleted the gltf-exporter branch September 29, 2022 08:22
mvaligursky pushed a commit that referenced this pull request Sep 29, 2022
* Add glTF exporter

* Lint fixes

* Update scripts/exporters/gltf-exporter.js

Co-authored-by: Hermann Rolfes <lama12345@gmail.com>

* Use pc.math.roundUp

* Switch from forEach and push to map

* updated to the same format as usdz exporter, added example

* gltf exporter returns a promise to match usdz exporter

* handling all formats / types / semantics, fixes bench export (index buffer format was the issue here)

* updated example

* cleanup

* small cleanup

* evaluate min & max with better precision

* fix based on comment

* a dirty solution to get non-interleaved VBs working (with validation errors)

Co-authored-by: Hermann Rolfes <lama12345@gmail.com>
Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
Comment on lines +36 to +53
const getSemantic = (engineSemantic) => {
switch (engineSemantic) {
case pc.SEMANTIC_POSITION: return 'POSITION';
case pc.SEMANTIC_NORMAL: return 'NORMAL';
case pc.SEMANTIC_TANGENT: return 'TANGENT';
case pc.SEMANTIC_COLOR: return 'COLOR_0';
case pc.SEMANTIC_BLENDINDICES: return 'JOINTS_0';
case pc.SEMANTIC_BLENDWEIGHT: return 'WEIGHTS_0';
case pc.SEMANTIC_TEXCOORD0: return 'TEXCOORD_0';
case pc.SEMANTIC_TEXCOORD1: return 'TEXCOORD_1';
case pc.SEMANTIC_TEXCOORD2: return 'TEXCOORD_2';
case pc.SEMANTIC_TEXCOORD3: return 'TEXCOORD_3';
case pc.SEMANTIC_TEXCOORD4: return 'TEXCOORD_4';
case pc.SEMANTIC_TEXCOORD5: return 'TEXCOORD_5';
case pc.SEMANTIC_TEXCOORD6: return 'TEXCOORD_6';
case pc.SEMANTIC_TEXCOORD7: return 'TEXCOORD_7';
}
};
Copy link
Collaborator

@kungfooman kungfooman Sep 29, 2022

Choose a reason for hiding this comment

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

In glb-parser.js there is:

const gltfToEngineSemanticMap = {
    'POSITION': SEMANTIC_POSITION,
    'NORMAL': SEMANTIC_NORMAL,
    'TANGENT': SEMANTIC_TANGENT,
    'COLOR_0': SEMANTIC_COLOR,
    'JOINTS_0': SEMANTIC_BLENDINDICES,
    'WEIGHTS_0': SEMANTIC_BLENDWEIGHT,
    'TEXCOORD_0': SEMANTIC_TEXCOORD0,
    'TEXCOORD_1': SEMANTIC_TEXCOORD1,
    'TEXCOORD_2': SEMANTIC_TEXCOORD2,
    'TEXCOORD_3': SEMANTIC_TEXCOORD3,
    'TEXCOORD_4': SEMANTIC_TEXCOORD4,
    'TEXCOORD_5': SEMANTIC_TEXCOORD5,
    'TEXCOORD_6': SEMANTIC_TEXCOORD6,
    'TEXCOORD_7': SEMANTIC_TEXCOORD7
};

An object in itself is shorter, but since it's basically the same object (just inverted), I started to think these variables could be exported and then just automatically inverted, for example:

function objectInvert(obj) {
    const ret = {};
    for (const key in obj) {
        const val = obj[key];
        ret[val] = key;
    }
    return ret;
}
export const engineToGltfSemanticMap = objectInvert(gltfToEngineSemanticMap);

(but maybe this is just overcomplicating things)

Copy link
Contributor

Choose a reason for hiding this comment

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

I started with the object initially .. but the engine needs to be loaded before the pcx (extras) .. and so a static time access to pc constants is not possible here - and so I added a function instead, which executes after the engine has been loaded.
When we have full tree-shaking, and make this a proper module, we'd definitely do something else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: graphics Graphics related issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants