How to Optimize Materials/Draw Calls by reducing Material Slots

Context:
I have buildings with over a dozen Material Slots per building- each using their own unique material for each slot.
I have a crap tone of tiling textures that are used on a model of a house.
(It also has some additional baked texture atlases unique to the building that I don’t understand but are also used in some material slots).

What I want to achieve:
Create one Master Material, that I can instance across different buildings, and therefore only use one Material slot per Building (I’m targeting mobile so I want to reduce draw calls as much as possible). Unless master materials are not the way to go, but I don’t know much so I wouldn’t know if there is a better approach.
Meaning, I need to find some way to replace the usage of different material slots, and condense it into one- but I have no idea how you would even approach this.


TLDR:
Is there a way to create an efficient way of texturing multiple different buildings sharing the same Material, each using their own tiling textures?

I cant think of a way how to tell the Material how to use different tiling textures on different parts of the buildings, since these aren’t unique textures specifically made for each buildings UV maps.

Generally this is done with some kind of Material ID mask, such as a texture. Usually, this is just a texture with different values that specify what material should go where. Also consider using a texture array, so that the different tiling textures can share a single sample. This can save on performance, since generally no two pixels will actually need to sample more than one of the textures at the same time.

There are lots of ways you can author such a mask. Even a grayscale texture can store 256 possible int IDs for any pixel. A bitmask node can test the value of the bit, and be used to set which index of the texture array is being samples, or if you prefer not to use an array for some reason, then it can be used as an alpha for a lerp.

How would that be done though, I don’t really understand.
If I use a texture sample, I can get like 3 channels out of it from output pins.
Is there some way that I don’t know off to get more out of it?

And what if I have multiple buildings that use a different amount of tiling textures, is there some way to automate figuring out how many different material ID’s are required for that Material Instance / Mesh?

Rather than using the channels as a mask, use the values as a mask. Presumably, you only need a binary value. Something is either brick or not… Concrete or not.
So you decide on standardized values to assign to each material - maybe you paint everything you want to be brick with a value of 1/256 (in any one channel, the other two can be used for whatever else). Then paint everything concrete with a 2/256.
Then, you plug this texture into a bitmask node. You use the same bit value as in input to the bitmask, and it will output a 0 or 1 depending on if the pixel has the bit value specified.
Now you can output up to 256 binary masks from a single texture. Set up the texture array for each building. Each texture will have a specific index in the array, which is specified by the texture coordinates vector input to the sample.
Let’s say you have roof tiles in index 3 of the array.
You also have the bitmask output from the 3/256 value bits in the texture corresponding to roof tiles. Multiply the bitmask by the index value. Now every pixel that will show the tile texture has a value of 3, and anything else has a value of 0. Repeat for as many values as needed, then add them all together.
You will now have a mask that corresponds each pixel’s value with the index of the array that you want that pixel to sample.
Take your texture coordinates, and an append vector node. Append your mask to the 3rd channel of the texture coordinates and sample the array.

This works because of the fact that we don’t need to blend any of the textures together. So there’s no reason to spend more than one bit to say do or don’t apply it there.

1 Like

In case you’re still stuck, here’s an example I made since I had some free time:


The sampler on the left is a grayscale mask. At the bottom you can see me using a bitmask to single out specific bit values.
On the top, I’m just multiplying the bits by 256 then subtracting 1 to get it into 0-255 space to act as and index. This gets appended onto the texture coordinates of a sampler with a 2D texture array that contains four tiling textures. As mentioned before, this works because no two textures will occupy the same space on the mesh.
Thanks to this method, the entire building can be textured with a single texture sample per output (plus the cheap mask) instead of four and in a single draw call.

1 Like

Thanks this is very interesting.
I want to try this approach. I am thinking of using a 2d Texture array as well.

The masking part makes a lot of sense and is a cool way to get around the limitation that I mentioned if I were to do a colour mask instead.
But what I don’t really understand is how you choose the correct material by index?
Are you just manually/arbitrarily matching the value of the mask to the index of the material in the 2d Texture array?

It’s not arbitrary, its planned. If you do not plan and make your bit values match the array index, you will either need to change the array order (which would break other assets using the array) or remap the bits using the bitmask like I described earlier. You can see how in the example, the bitmask has extracted the bit as a 1, which could be multiplied by a value corresponding to the index you want to assign. If you repeat this for each bit, then add them together you’ll get a useable index map.
But it’s cheaper and more convenient to plan ahead and make sure the bit values are what you want them to be begin with.

1 Like

Would you mind explaining to me what the “2darraylookupbyindex” is doing exactly? I am trying to wrap my head around using 2d arrays in my master material as well. I made another comment on one of Neuffex’s other posts here:

Perhaps I should start a new post for this. I got it sampling correctly but I am getting confused with the output. I think I just need to study the fundamentals of what is being output by these nodes a bit more. Perhaps what I am trying to accomplish is not possible, or I am just misunderstanding something.

Here is a video of where I am at, if anyone gets time to review it. The sub material functions are taking in a 2D texture, which I can change the inputs on, but I don’t think I want to change those because we want them applied to whatever texture we are feeding in. This texture information can be in whatever data form it needs, I just want to be able to separate the albedo, normals, etc. In this video, the material functions within the layer (Material function _ G, or “layer”)

Essentially my goal is to make a 2d array for the albedo, normal, etc. for each layer in my auto material. I also want to be able to apply material functions to the texture I sample from my array.

You do not need that node at all to sample a 2D texture array.
The R&G of the 2D array UV input work like any texture sampler. But unlike a normal sampler, it also expects a B component. This is the index of the array to sample, starting at 0.
This is why I simply appended the index value to the texture coordinates. In the above example, the index is determined by a texture file, but anything that can pass an integer into the 3rd component will do.

2 Likes

Okay! I will study your example and your response. Thank you!!

2D arrays can only sample one index per pixel at a time, which makes them less than ideal for landscape auto materials because this will create a hard seam when transitioning between one index and another.
This is as opposed to having two separate samples and lerping between them, which allows for a pixel to read both textures at the same time for true blending. If you don’t need smooth transition (like in the case of a building texture as the original topic pertains to) then this is a non issue.
You can get the appearance of a smooth blend using dithering although this does have some issues primarily around ghosting. I have a video that demos this at the 14 minute mark.

2 Likes

Very interesting, ill need to try this to wrap my head around it properly.
But it makes sense that you would need to pre-plan the index.

That’s pretty cool, I didn’t actually know that either. I will definitely remember this for the future.

What a plug haha this is awesome. You hit this one on the nail, you even mention using a VT in the array which is what I wanted to get to eventually. The overview of the triplanar really helps. Thanks for sharing! I look forward to checking out more of your work.