-
Notifications
You must be signed in to change notification settings - Fork 15
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
Adds m-material element to MML #181
base: main
Are you sure you want to change the base?
Conversation
…ll textures correctly
+ Added m-material to the MML schema
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 is awesome and a very welcome PR. Thanks!
Aside from the specific lines I've commented on there are two issues I've noticed that might require small refactors:
-
The way that the
m-material
attaches to the parent currently uses a property check formesh
which is difficult to discover and make work across different affected elements. It could instead us the same mechanism of elements likem-attr-anim
and use theaddSideEffectChild
of the parentMElement
which can then choose to use this childm-material
. -
The primitive elements also modify the material that their mesh uses (e.g.
Cube
modifiesthis.material.color
andthis.material.opacity
directly. This means that the ordering and precedence isn't predictable. I think we should make the behaviour that if an element has an overriding material then the color and opacity can't be modified directly.
There are also two wider concepts related to materials that are worth noting so we can design now for their future implementation:
-
Reusing materials across elements by creating an
m-material
with anid
that can then be referenced by multiple elements. This in particular means that you don't want elements to be able to modify the material. -
The ability to override specific materials of an
m-model
.
I don't think it's necessary to get all of that in a single addition, but we'd prefer to avoid changing the behaviour to accommodate those features.
envMapRotation: this.props.envMapRotation, | ||
envMapIntensity: this.props.envMapIntensity, | ||
wireframe: this.props.wireframe, | ||
wireframeLinewidth: this.props.wireframeLinewidth, |
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.
wireframeLinewidth
is limited on most platforms to 1
so we probably shouldn't include it as the behaviour can't be implemented consistently.
instance.materialManager | ||
.loadTexture(instance.props.displacementMap, instance) | ||
.then((texture) => { | ||
instance.material!.displacementMap = texture; |
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.
The displacementMap
is an interesting one because it allows rendering the vertices of the element visually outside the detectable bounds which violates the m-frame
min-/max-
attributes. I'd propose omitting it.
if (value) { | ||
const texture = await this.materialManager.loadTexture(value.toString(), this); | ||
if (texture && this.material && key in this.material) { | ||
(this.material[key] as unknown as THREE.Texture) = texture; |
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 is prone to race conditions if the loading of a first texture (A) takes long enough that a new texture (B) has been set and loaded.
The first texture (A) will finish loading and overwrite B.
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.
Does fixing the issue with populating the cache after load also fix this issue? Now element A will kick off the promise, and element B will wait on it.
return cacheItem.texture; | ||
} | ||
const texture = await this.textureLoader.loadAsync(src); | ||
this.textureCache.set(src, { |
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.
Populating the cache only after loading means that two or more materials using the same map that are created at the same time will each load the texture and then all populate the cache. The alternative is to populate the cache with the promise.
This is awesome!! We have been on about something like this for a while <3 |
…ctly + Primitive elements can't modify their child materials anymore though color and opacity + Cache is set to promise when loading new texture to avoid race condition + Properly catches error when loading textures and returns null + Removed the wireframeLinewidth, displacementMap, displacementScale and displacementBias attributes
+ Adds m-material-priority-test
…ing a shared material on connection + The loaded event is dispatched on the material now and the disconnected event is dispatched on each registered user element
+ Replaced primitives-test assets
@MarcusLongmuir Thank you again for the feedback. I think I have addressed most of your concerns so far. The attributes were removed and the textureCache now stores a promise. Can I assume this also fixes the other issue you brought up here?
I've also switched to using addSideEffectChild and I'm preventing the attributeHandlers from modifying the material if it is from a material element. Your first suggestion seemed simple enough, so I decided to implement it. Now you can define a shared material by creating a m-material element with an id. It will automatically be registered in the MaterialManager and call addSideEffectChild on any elements with the attribute material-id set to that id. <m-plane width="20" height="20" rx="-90" material-id="my-material"></m-plane>
<m-cube y="2" x="0" material-id="my-material"></m-cube>
<m-material id="my-material" map="http://localhost:7079/assets/test-image.jpg"></m-material> Finally, are you able to provide more details on how you would like to override m-model materials? I'm confident I can get that done as well once we figure out the best way to specify which materials should be replaced. |
@GwilymIO Happy to help! I would love to get more involved and work with the MML team to bring more of these features to life. |
Great work. I need to read the code that you've added, but there are a few points that I've noted whilst using the behaviour and thinking about this:
|
@MarcusLongmuir That's a good point. Shared materials should be scoped to their documents/frames so that they can't affect/be affected by elements outside of it. I haven't looked into this issue yet, but I'm assuming that I can check if an element is a child of a m-frame like so: const remoteDocument = this.getRemoteDocument();
if (remoteDocument?.parentElement instanceof Frame) {
// scoped to this m-frame
} else {
// scoped to root
} My first thought is to use the frame/document pointer as a key to map elements to their scope. If you have other considerations or a better approach please let me know. |
I think you can just use the |
+ The element's remote document is used to scope the shared material to that document + Elements will not be able to use shared materials if they are not in the same remote document
+ Adds checks before overwriting the material texture to ensure the value has not changed since the load started
@MarcusLongmuir Finally got some free time this week to implement the changes we discussed. I went with a scoped key instead of using the m-frame pointer. I create a key by appending the id of the material to its remote address (e.g. ws:localhost:7079/document.html/#my-shared-id) and keep using one map for everything. Lastly, I fixed the race condition when loading textures. I was able to create a test to verify the issue and it is working perfectly for me now. |
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 seems that there are quite a few places that are responsible for applying the logic of which material should be applied to an element given the mutation being handled.
Whilst the cues to do this check and modification can come from multiple places I think this would be far easier to comprehend/avoid regressions/ensure correctness if the logic for applying order of precedence and changing materials was in one function.
That might be easier to do as a simplification pass though after we get the correctness in this PR.
I'm not confident that we know how we'll handle multiple materials applied to an m-model
. Until we have a design for that it's a bit of a risk to land this as-is because we may realise the material-id
approach is incompatible. Have you had any thoughts about that?
let sharedMaterial = this.sharedMaterials.get(scopedId); | ||
|
||
if (sharedMaterial && sharedMaterial.material?.getMaterial()) { | ||
const conflictingNodes = sharedMaterial.remoteDocument?.querySelectorAll( |
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.
Using querySelectorAll
here could result in finding m-material
nodes that aren't owned by this particular remote document and are instead inside m-frame
s within the remote document.
} | ||
|
||
registerSharedMaterial(remoteAddress: string, id: string, material: Material) { | ||
const scopedId = MaterialManager.getScopedMaterialKey(remoteAddress, id); |
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.
Using the address of the document avoids most conflicts, but it's not 100% correct because you can include the same document url in multiple m-frame
s and then the materials collide and it's possible that those two connections to the same address present inconsistent m-material
s to the client.
This PR adds a new element
m-material
to themml-web
package and the MML schema.This element allows users to replace the material of any primitive (e.g. cube, cylinder, plane, sphere) with more granular control of the PBR material that is created for the parent element.
For this implementation, textures are cached and re-used across elements to minimize the performance and memory overhead.
m-material-test.mp4
Example:
What kind of changes does your PR introduce? (check at least one)
Does your PR introduce a breaking change? (check one)
If yes, please describe its impact and migration path for existing applications:
Does your PR fulfill the following requirements?