How to switch texture in the vert function of a shader?

I’m trying to optimize my game for draw calls by combining all my materials (one for each texture) into one material. However the way I am doing it is not optimal and shifts a lot of work into the frag (or surf) function.

I’d appreciate any help with getting this working in an optimal way - for example moving the texture switching into the vert function.

Right now I’m following this guy’s idea here:

Which boils down to:

  1. On each object, set the mesh vertex colors
Mesh mesh = GetComponent<MeshFilter>().mesh;
Color[] colors = new Color[mesh.vertices.Length];
... //actually populate the colors
mesh.colors = colors;
  1. In the shader structs for output and input, define a color variable
struct vertexInput {
    ...
    half4 colors : COLOR;
    ...
}

struct vertexOutput {
    ...
    half3 vertexColor : COLOR;
    ...
}
  1. In the shader vert function, populate the color from the vertex into the output struct
vertexOutput vert(vertexInput v) {
    vertexOutput o;
    ...
    o.vertexColor = v.colors.xyz;
    ...
    return o;
}
  1. In the shader frag function (or surf function), switch textures based on the color’s x variable
fixed4 frag(vertexOutput i) : COLOR {
    ...
    fixed4 tex;
    if(i.vertexColor.x > 0.5) {
        tex = tex2D(_MainTex, uv);
    } else {
        tex = tex2D(_OtherTex, uv);
    }
    ...
}

Extrapolate that out for switching between 10 or so textures.

Even the guy in the video says that it is not optimal do do this conditional switch in the frag function since it will need to be performed once per pixel. He recommends doing it in the vert function. But if I determine in the vert function which texture I want to use, how do I pass this on to the frag function?

  • Ideally I would like to define a variable for the texture in the output struct, populate that in the vert function and use it in the frag function. But as per the answer to this question
    opengl - GLSL sampler2D in struct - Stack Overflow
    they seem to be saying that it is impossible to do this since a texture / sampler is an opaque data type.

  • It would be even better if I could have an array of textures and convert the color’s x variable into an array index using just math. Something like this:

fixed4 tex;
sampler2D textures[2] = {_MainTex, _OtherTex};
int textureIndex = (2 * vertColors.x) - 1; //assuming the x value is always 0.5 or 1.0
sampler2D myTex = textures[textureIndex];
tex = tex2D(myTex, uv);

But then I get the error “sampler array index must be a literal expression” which as per the answer to this question
https://www.gamedev.net/forums/topic/687522-is-it-possible-to-pass-an-index-to-a-texture-array-through-instancing/
seems to mean that it is also impossible for a shader to use any non-hardcoded variable to get something from an array.

  • That post winks about DirectX12 but my project is for mobile. I have also heard about unity’s Texture Arrays
    Unity - Manual: Texture arrays
    but my textures are not all of the same size.

  • I guess there is also the texture atlas + UV Mapping route if I were to go in and modify all my meshes, but my game geometry is largely built out of different sized cubes, all of which use the same mesh…

I’m exhausted! Is there a solution to this hiding around anywhere?

So he’s not wrong that it’s not optimal, but he’s kind of wrong (or at least incomplete) in his explanation of why. The conditional itself of course isn’t free, few things are in shaders, but in this situation of switching between two textures it’s very nearly so and the real cost comes from the fact you’re actually sampling from both textures and throwing alway the results from one. For complicated reasons to do with mip mapping if a tex2D is in a conditional statement, the code in that block will always run. You can work around this by using tex2Dgrad or tex2Dlod and calculating the base gradients or LOD outside of the if statement, but if you’re aiming for mobile those aren’t available in OpenGL ES 2.0, only ES 3.0+.

If you’re switching between 10 textures, you’ll be spending the GPU time sampling all 10 textures for every pixel and then throwing away all but one. If you do a chain of if statements you’ll be throwing them away one by one too, making this even slower. In the grand scheme of things sampling 10 textures isn’t really that bad, and storing the sampled colors in an array and selecting by index will be faster than using conditionals, but it’s still far from optimal.

There’s also the topic of static branching vs. dynamic branching vs. “fake branching” on hardware that doesn’t support dynamic branching. Dynamic branching is how most people expect an if statement to work if they’re familiar with CPU side scripting or programming. If X do Y else Z, and only Y or Z happens. For GPUs that support dynamic branching, this is true, but the if has a cost of somewhere between 6 and 15 instructions depending on the hardware (for modern desktop GPUs), so if the code you’re skipping is fewer instructions than that then you’re actually making your shader slower by “skipping” it. Static branching is when the dynamic branch’s condition is based on a uniform material property, in this case the cost of the branch is mostly waved and the GPU really does only do Y or Z. Then there’s the fake branching, this is what I described above where all paths are computed, then all but the wanted one is thrown away. This is also how all GPUs that don’t support dynamic branching treat all conditional statements, and OpenGL ES 2.0 devices don’t support dynamic branching. This is why conditionals are considered so expensive on mobile.

Yep. These aren’t possible for the reasons you’ve come across, and are why texture arrays were created.

As per that documentation, if you’re okay with OpenGL ES 3.0 as a min target, then you can use texture arrays. This is honestly the “best” solution, assuming that min target. While your textures may not all be the same size, you might want to ask yourself if it’s worth making them all the same size so you can use texture arrays. Yes, that’s wasting some memory, but just about every option is going to trade memory for performance. In this case having a few 64x64 textures that you’ve scaled up to 512x512 so they get stuffed into a texture array may be worthwhile in the end, and if they’re compressed it still isn’t that much memory unless you’re talking about hundreds of textures.

The texture atlas approach is the only one that is universally supported. It really does “just work” (ignoring mip mapping issues, and wrapping for repeating textures, etc.). You also don’t have to really modify you’re meshes, not exactly at least. You can use a script and additional vertex streams to modify an existing mesh’s vertex attributes at runtime without replacing the mesh. However I don’t know if static batching works with this. Also realize that if your concern is about having multiple unique meshes in your build using up memory, static batching makes every single static object’s mesh unique. That’s a big part of how it makes things faster.

2 Likes

Thanks bgolus, very informative and I appreciate being set straight. It’s fascinating to learn how the branching works since I come from a traditional coding background where you need to be able to depend on real branching otherwise the code will crash (e.g. null pointer exceptions).

As a test, I spent a couple of hours rigging up the systems outlined in the video anyway - both the frag-level switching between 10 textures and alternatively just using the actual vertex colors without textures, for comparison. On my test device the version with vertex colors stuck pretty closely to 30fps and the texture switching version spends a lot of time at half that.

So I’ll abandon my current texture switching approach and see how I go with texture arrays or a texture atlas.

Thanks again.