Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Fastest way to swap textures in an atlas?

Discussion in 'Shaders' started by Spaniel, Jun 14, 2018.

  1. Spaniel

    Spaniel

    Joined:
    Dec 23, 2012
    Posts:
    52
    I am rendering old school BSP meshes. I have written a texture array shader and have extended the UVs to be a Vector3 with the third component being the index into the texture array. I need to now implement animated textures, which is essentially swapping in a new texture where the old one was and then looping it.

    The old way I did this was in a MonoBehaviour where after the delay, I would send the material the next texture in the animation. This worked fine.

    Now I am trying to do this but I need to consider that I am using a Texture2DArray.

    I have narrowed it down to two approaches:

    1. Keep track of indices that belong to a specific texture and every time it changes (every 400ms or so for example), I can resubmit the new UVs which are vector3s, the third component being the index in the array. Potentially slow on large meshes.

    2. Second option, is replacing the texture in the array. Pretty sure this involves some heavy lifting but maybe I'm wrong.

    Any other ideas are appreciated!
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    1. You can also update a mesh using AdditionalVertexStreams so you only have to update the UVs and not the rest of the mesh from script (though the whole mesh will be uploaded to the GPU). Unity uploads massive meshes to the GPU all the time though, as that's how dynamic batching works.

    2. You can also replace a texture in the array at runtime using CopyTexture(), just like you do when creating the array to begin with. That should be quite fast to do.

    The other option is if you really are just swapping textures out, and the vertex UVs x & y positions are otherwise not changing, you could use some indirection by having the z value be the "surface index", and then pass an array to the material that is the actual texture array index. Now you can have a single texture array for everything that never has to change, and you never have to update the mesh UVs.

    Basically it'd be something like this:

    // uniform declaration
    int _TextureIndex[64];

    // fragment shader
    fixed4 col = UNITY_SAMPLE_TEX2DARRAY(_TexArray, float3(i.uv.xy, _TextureIndex[(int)i.uv.z]));
     
  3. Spaniel

    Spaniel

    Joined:
    Dec 23, 2012
    Posts:
    52
    I ended up profiling both the UV updating and the texture updating using SetPixels (CPU, I know ;)). They are slow enough that I'd like to avoid them.

    The CopyTexture sounds good but for the life of me can't find out how or any examples for getting a texture copied into a Texture2DArray. Are there any examples of this? How do I access the textures inside of the array to even set it.

    Love the last idea, will go with that if CopyTexture proves to be too slow.
     
  4. Spaniel

    Spaniel

    Joined:
    Dec 23, 2012
    Posts:
    52
    Figured it out. And yes, it looks to be a bit too slow for what I want. Last question for you (and thanks for being super helpful in several threads), with your uniform int array above, how do I actually send this to the material? I see Material.SetFloatArray and Material.SetInt but not IntArray...

    Thanks :)
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Material.SetInt() is actually just an alias for Material.SetFloat().

    Material.SetInt("_MyVal", intValue);
    is internally doing
    Material.SetFloat("_MyVal", (float)intValue);

    In fact SetBool("_MyVal", boolValue) is also just SetFloat("_MyVal", boolValue ? 1f : 0f)!

    On the shader side Unity will convert all of those float values to the appropriate type defined in the shader.

    TLDR; use SetFloatArray() for your in shader float, int, and bool arrays, but they have to be stored as float arrays or lists in c#.
     
  6. Spaniel

    Spaniel

    Joined:
    Dec 23, 2012
    Posts:
    52
    Great! Thank you. By the way, total knucklehead moment. I read that CopyTexture was fast and was GPU only but I saw that the CPU still had about a 1ms overhead. And then it hit me... I was sending the GPU a texture rather than copying one that was already on the GPU which defeats the WHOLE purpose. Virtually no delay now. Thanks again for your fantastic help.
     
  7. Spaniel

    Spaniel

    Joined:
    Dec 23, 2012
    Posts:
    52
    Looks like I am running into an issue when trying the TextureIndex method you posted above. My rendering ends up looking like this - distortion and "static" between two textures - and lines that shift with the camera:



    This is what it's supposed to look like:




    I wasn't able to find any information on it but clearly, it looks like the fragment shader is sampling different textures when it should be sampling one. It doesn't really make sense though as if there really were different texture indices at each vertex, that would show me an issue regardless if I am adding this additional method. I have also tried casting the third component to an int and no luck...

    Code (CSharp):
    1.     float4 _MainTex_var = UNITY_SAMPLE_TEX2DARRAY(_MainTexArr, float3(i.uv0.x, i.uv0.y, _TextureIndex[i.uv0.z]));
    2.  
    Not sure what is going on here. Any ideas? And I have tried sending the output fragment shader color the sample from the array (to rule out other processing) and the issue persists.

    The code to send the material the array is quite simple:

    Code (CSharp):
    1.         _material.SetFloatArray("_TextureIndex", new List<float>() { 0f, 1f, 2f, 3f, 4f, 5f});
    2.  
    I do this once in the start method.
     
    Last edited: Jun 18, 2018
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Try:

    _TextureIndex[(int)(i.uv.z + 0.5)]

    edit: I couldn't tell you exactly the cause btw, but my guess is the interpolated uv.z value is for some reason not quite an integer value. When used as the input to the array this isn't a problem since I believe the z component of the UV is rounded to the closest integer to pick the array layer. ie: 0.0 to <0.5 is layer 0, >0.5 to <1.5 is layer 1, etc. But when that same not-quite-a-constant-integer-value z value is slightly less than the integer value it'll get floored to the lower integer. Adding 0.5 should fix that. You could also do this in the vertex shader so the i.uv.z passed to the fragment doesn't have to do a per pixel look up.
     
    Last edited: Jun 18, 2018
  9. Spaniel

    Spaniel

    Joined:
    Dec 23, 2012
    Posts:
    52
    This did the trick! But I am still at a loss for why there is any interpolation at all in the z value of the uvs. I made sure I rebuilt the whole mesh to not share vertices if there are different textures. Strange...
     
  10. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Because there's always interpolation, and because floating point math is a funny thing.

    When you have 3 vertices with the same value, the GPU is still doing barycentric interpolation of that value across the tri (unless you use the nointerpolation modifier). Barycentric interpolation is basically this:

    upload_2018-6-18_9-26-43.png
    https://en.m.wikipedia.org/wiki/Barycentric_coordinate_system

    At the center the values from all three corners are averaged equally with something like this:

    V0 * 1/3 + V1 * 1/3 + V2 * 1/3 = interpolated value

    If all three V values are equal then algebraically that will mean the interpolated value is equal as well. Unfortunately floating point math isn't so consistent. 1/3 will be a floating point value of 0.3333333432674407958984375, which is close but not quite the same. Adding that value together 3 times will produce a value slightly above 1 (though with floating point it's a value that ends up being exactly 1.0), but if you're slightly off of that perfect middle point you'll have values that when added together may produce a value just above or just below 1.0. But being just below 1.0 means the integer value is now "0.0"
     
    SugoiDev likes this.
  11. Spaniel

    Spaniel

    Joined:
    Dec 23, 2012
    Posts:
    52
    Fantastic breakdown. Makes complete sense and explains adding the 0.5 before casting it to an integer. Thanks again!
     
    Last edited: Jun 19, 2018