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

GLTFExporter: Multi-material support #13536

Merged
merged 8 commits into from
Mar 14, 2018

Conversation

takahirox
Copy link
Collaborator

@takahirox takahirox commented Mar 8, 2018

This PR adds multi-material support to GLTFExporter. I'm happy if you try with your scene because I haven't verified this change with many enough scenes yet.

The exporter exports a mesh with multi-material as a mesh including the primitives which share the same attributes but have distinct material and indices.

Note that GLTFLoader doesn't build a mesh with multi-material from it yet but builds multi meshes in a group. We should update and enable GLTFLoader to handle multi-material, too.

@donmccurdy
Copy link
Collaborator

Nice! Haven't had the chance to review in detail but will do that soon.

We should update and enable GLTFLoader to handle multi-material, too.

I have a PR in progress elsewhere for letting GLTFLoader handle multi-material meshes as a single Mesh instance. :)

@takahirox
Copy link
Collaborator Author

takahirox commented Mar 8, 2018

Cool!

a mesh including the primitives which share the same attributes but have distinct material and indices.

Can the loader with your PR build a single mesh with multi-material from this data structure? Let me know if you have any requests to this PR for handling multi-material in the loader.

@donmccurdy
Copy link
Collaborator

Exactly that, yes, the structure looks right. Although it depends on BufferGeometryUtils.mergeBufferGeometries(). So it would sort of be a conditional upgrade when that utility is present.

@takahirox takahirox mentioned this pull request Mar 9, 2018
87 tasks
start = Math.max( start, geometry.drawRange.start );
count = Math.min( end, end2 ) - start;

if ( count < 0 ) count = 0;

}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Slightly related question for @fernandojsg — in what situations would the truncateDrawRange option be used?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@donmccurdy in apps where you are creating a very big buffer but just fill a small part of it (eg: A-Painter) you would like to export just the buffer that actually contains real data, not the whole buffer filled with zeros.

var bufferViewTarget;

// If geometry isn't provided, don't infer the target usage of the bufferView. For
// animation samplers, target must not be set.
if ( geometry !== undefined ) {

var isVertexAttributes = componentType === WEBGL_CONSTANTS.FLOAT;
bufferViewTarget = isVertexAttributes ? WEBGL_CONSTANTS.ARRAY_BUFFER : WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER;
bufferViewTarget = attribute === geometry.index ? WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER : WEBGL_CONSTANTS.ARRAY_BUFFER;
Copy link
Collaborator

@donmccurdy donmccurdy Mar 10, 2018

Choose a reason for hiding this comment

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

This doesn't look right — ELEMENT_ARRAY_BUFFER should be used as the target for the index attribute, but not for vertex attributes of indexed geometry. Oops, I misread. 😑

if ( geometry.index === null && options.forceIndices ) {

var numFaces = geometry.attributes.position.count;
var indices = new Uint32Array( numFaces );
Copy link
Collaborator

@donmccurdy donmccurdy Mar 10, 2018

Choose a reason for hiding this comment

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

nit: this should probably be called numIndices rather than numFaces

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 prefer

var indices = new Uint32Array( geometry.attributes.position.count );

for ( var i = 0, il = indices.length; i < il; i ++ ) {

	indices[ i ] = i;

}

because we don't need to mind var name hehe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated, thanks!

var primitive = {
mode: mode,
attributes: attributes,
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting interesting.... this is not how I expected primitives to be used 😅, but it looks totally valid too. I was thinking that each primitive would have different accessors, based on the start/count subslice of the mesh's attributes. And then in GLTFLoader, we would have to concatenate them all back together.

What you're doing seems more efficient with indexed geometry, or if you want to re-draw the entire mesh with different materials. But it also assumes the loader does not do what I was planning, concatenating all of the accessors. Or if forceIndices had been false, and each group had different draw range, that's not handled here.

If you look at the BrainStem.gltf model, it has a bunch of primitives, all with different accessors. I think we should start an issue on the glTF repo to mention these different use cases and make sure they're all OK.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

@takahirox takahirox Mar 10, 2018

Choose a reason for hiding this comment

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

Agreed with discussing best practive in glTF thread.

I'm sure this style is very fitting to Three.js structure and requires less changes. But yea, I've missed no-index case... Hm...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added an temporal workaround so far for no-indexed geometry, force index for no-indexed geometry with multi-material. I don't think this's a good solution so just temporal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But it also assumes the loader does not do what I was planning, concatenating all of the accessors.

Hm... probably I guess GLTFLoader ends up supporting both shared primitive accessors and different primitive accessors because the both are valid (depending on the discussion in glTF thread tho).

Copy link
Collaborator

Choose a reason for hiding this comment

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

GLTFLoader should be fine for now, since it still creates a mesh for each primitive. At least it will reuse the same Geometry in this case.

Copy link
Collaborator Author

@takahirox takahirox Mar 11, 2018

Choose a reason for hiding this comment

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

Yup. I think this change is ok once I fix materials loop I commented below. The exporter exports valid glTF and I confirmed GLTFLoader and FB can load.

So my plan is we merge this change for now, and make another PR switching shared accessors to different accessors if necessary.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks good! 👍

if ( ! forceIndices && isMultiMaterial ) {

// temporal workaround.
console.warn( 'THREE.GLTFExporter: Force index for a mesh with multi-material.', mesh );
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's change this message to:

THREE.GLTFExporter: Creating index for non-indexed multi-material mesh.

("Force" sounds like a we are telling the user to do something, rather than telling them what we are doing)

And, right now this message will appear if the mesh is already indexed.. let's hide the message in that case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oops, I've missed adding geometry.index === null.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated, thanks.

var materials = isMultiMaterial ? mesh.material : [ mesh.material ] ;
var groups = isMultiMaterial ? mesh.geometry.groups : [ { materialIndex: 0, start: undefined, count: undefined } ];

// assuming materials.length === groups.length
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's add some more detail in this comment, since it might not be obvious based on my spec questions below. Suggested:

Export one primitive for each material on the mesh. Only a subset of cases are currently handled:

  • [×] Single material, no geometry groups
  • [×] N materials, N geometry groups each using full draw range
  • [ ] N materials, N geometry groups with different draw ranges
  • [ ] N materials, M > 1 geometry groups

^ If that looks right? I'm trying to list cases that are valid in three.js, but not everything that glTF might allow.

Copy link
Collaborator Author

@takahirox takahirox Mar 11, 2018

Choose a reason for hiding this comment

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

Oops, this logic isn't correct. I should loop with groups and we don't need to assume materials.length === groups.length.

for ( var i = 0, il = groups.length; i < il; i ++ ) {

I'll update. With the update, this'll be fine

[ ] N materials, M > 1 geometry groups

BTW, I think this's ok even without the update.

[ ] N materials, N geometry groups with different draw ranges

I'm not really sure if I understand correctly that item, would you please explain with example?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated, thanks.

var primitive = {
mode: mode,
attributes: attributes,
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

GLTFLoader should be fine for now, since it still creates a mesh for each primitive. At least it will reuse the same Geometry in this case.

start = Math.max( start, geometry.drawRange.start );
count = Math.min( end, end2 ) - start;

if ( count < 0 ) count = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

quick thought, no changes needed for this PR: the code here would be a bit simpler if we didn't have to think about .groups and .drawRange separately... might be interesting to look at implementing setDrawRange differently:

setDrawRange: function ( start, count ) {

	this.clearGroups();
	this.addGroup( start, count, 0 );

},

Copy link
Collaborator Author

@takahirox takahirox Mar 12, 2018

Choose a reason for hiding this comment

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

Hm, for me .groups would be more like model-author-defined for multi-material while .drawRange is runtime user controllable (maybe?) so better not to combine? For example, imagine user wanna narrow drawRange of multi-material mesh runtime for little time and then reset the range. Can't reset original .groups info because it has been lost if we combine.

Copy link
Collaborator

@donmccurdy donmccurdy Mar 12, 2018

Choose a reason for hiding this comment

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

Yeah, I think if we combined them it would be with the eventual goal of deprecating setDrawRange() entirely... it's not clear to me what happens when .drawRange and .groups are both set, so merging might just make things clearer. But just an idea, I don't have much preference on this.

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 not clear to me what happens when .drawRange and .groups are both set

You can try mesh.geometry.setDrawRange(start, count) on console on mmd example

https://threejs.org/examples/webgl_loader_mmd.html

This model has multi-material and groups.


}

geometry.index = new THREE.Uint32BufferAttribute( indices, 1 );
Copy link
Collaborator

Choose a reason for hiding this comment

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

minor: is it preferred to use geometry.setIndex( foo )?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh, I didn't know .setIndex() method exists. Updated, thanks.

I haven't replaced index reference with .getIndex() because it seems .getIndex() isn't used even in core.


}

var forcedIndex = false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

minor: just to avoid confusing forceIndices and forcedIndex, could you rename this one to didForceIndices or something like that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated, thanks.

@donmccurdy
Copy link
Collaborator

Nice work! 👏

@mrdoob mrdoob added this to the r91 milestone Mar 14, 2018
@mrdoob mrdoob merged commit 37ae761 into mrdoob:dev Mar 14, 2018
@mrdoob
Copy link
Owner

mrdoob commented Mar 14, 2018

Thanks!

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.

4 participants