-
-
Notifications
You must be signed in to change notification settings - Fork 35.3k
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
Conversation
Nice! Haven't had the chance to review in detail but will do that soon.
I have a PR in progress elsewhere for letting GLTFLoader handle multi-material meshes as a single Mesh instance. :) |
Cool!
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. |
Exactly that, yes, the structure looks right. Although it depends on |
start = Math.max( start, geometry.drawRange.start ); | ||
count = Math.min( end, end2 ) - start; | ||
|
||
if ( count < 0 ) count = 0; | ||
|
||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 — Oops, I misread. 😑ELEMENT_ARRAY_BUFFER
should be used as the target for the index attribute, but not for vertex attributes of indexed geometry.
if ( geometry.index === null && options.forceIndices ) { | ||
|
||
var numFaces = geometry.attributes.position.count; | ||
var indices = new Uint32Array( numFaces ); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, | ||
}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened: KhronosGroup/glTF#1278
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, | ||
}; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 );
},
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ); |
There was a problem hiding this comment.
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 )
?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, thanks.
Nice work! 👏 |
Thanks! |
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 enableGLTFLoader
to handle multi-material, too.