Why not have the Material property always be an array?

Hey all,

I apologize if this question has already been asked or addressed, I looked around and could not find it. If it has and you have a link, please share it and I will delete this post.

I am curious why we use two different approaches to handling materials depending on whether the geometry is grouped? Why not have the material property always be an array and if the geometry is not separated by groups, we only have a single object in the array?

I know this slightly complicates the API for single material objects but having to take into account two separate means of accessing and modifying materials depending on the scenario is a bit annoying when trying to build out new tools.

I know changing this would be a pain to implement and there is probably a very good reason why we are handling materials like this in the first place. For now I am working on a new Material class which extends the JS Array object and uses a proxy to provide a more convenient API.

I am just curious why materials are like this, in case there are some pitfalls I might need to watch out for going forward. Whenever I have these “why didnt they…” ideas, it almost always turns out I am being an idiot and there is a very good reason.

Thanks,
-Nate

To the contrary - as per this thread - there really isn’t much use for .material property as an array - if anything, it could be simplified to just always being a single Material at some point.

Mapping multiple materials on a single geometry would not only be not practical but also pretty much unmanageable, unless it’s just a cube / plane geometry :smiling_face_with_tear:

If I am understanding your example, from an API standpoint, you’re sort of right here. However, that seems like it could be really resource intensive for large scenes if you start splitting each geometry group into a separate mesh and then grouping it together. You are instantiating a ton of extra objects. While this has no real effect on the efficiency of the draw calls, like you mentioned, it seems like it will quickly eat up memory.

The approach I am suggesting does not map multiple materials to a single ungrouped geometry. It just says that if there are no groups, there will still only be one material and it will be located at the [0] index of the material array.

As it stands, almost every script I have that deals with materials has to start with

if(Array.isArray(obj.material)) //... iterate through the array and apply method
else //... apply method directly to material object.

All I am suggesting is that we should always use an array to describe materials and all geometries should have a default group of {start: 0, end: attr.count, materialIndex: 0}

then we could always just assume that materials will always be in array format and just use a forEach loop without constantly checking.

(Data complexity) Ă— (Code complexity) = const

  • If you want simpler code, you need more complex data.
  • If you want simpler data, you need more complex code.

Most likely the reason for this implementation of materials is a combination of some historical legacy and the idea of keeping simple things simple. Honestly, the majority of use-cases are happy with single-materials. It would need a really solid argumentation for making the simple cases more complex in order to simplify few of the complex cases.

If I need to simplify the runtime access to materials by assuming the property is always an array, I would:

  • create only objects with array materials
  • arrayize the materials of all imported models.

Then the rest of the code will never deal with two types of material.

1 Like

You’re correct, and I have essentially done that. I modified the prototype chain of the BufferGeometry class so that it adds an extra step and if there are no geometry groups, it adds one that spans all points. This forces the materials to be an array.

I have also developed a custom material API that uses a proxy which provides a useful way to apply the same property to all materials, or to pick out a single material and modify it.

So if you say

// set all materials in the array to orange.
_myMaterial.color.setHex(0xffaa00); 

// returns color of first material in array
const color = _myMaterial.color

// set color of 3rd material in array to blue. 
_myMaterial[2].color.setHex(0x00aaff); 

The proxy is incredibly simple and I do not think there will be significant performance implications, but I will be testing before sharing it.

Let me know what you think of this approach.
-Nate

I’m not an expert in this area, but it looks like you are trading performance for convenience. In most cases a few proxies will do no harm. But I’m not sure about complex cases … when people usually sacrifice own convenience in order to squeeze some more performance for others.

What about this idea:

  • keep material property as it is
  • add materials which is either [material] if material is not an array, or it is the same as material, if it is already an array. This materials could be a property, or a method, or a pair of getter/setter.

So, when you need to work with single and multiple materials with the same piece of code, you just use materials.

BTW there is some discrepancy in how you treat _myMaterial.color:

  • when reading, it gives the first value
  • when writing, it alters all values

Also, materials may have dozens of properties. Will you do the same for all of them?

Just wanted to say your suggestions are good and I think I have a good compromise. Will share when I am done! Thanks for the feedback.