Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question What is cheaper: swapping a texture from scripts, or switching between multiple textures in a shader

Discussion in 'General Graphics' started by cvbattum, May 17, 2023.

  1. cvbattum

    cvbattum

    Joined:
    Apr 20, 2018
    Posts:
    12
    For the game I'm working on, we want to show a visual on a quad. This visual is either of three textures, with an eventual second texture overlaid on top.

    My initial approach was to create a shader that has four texture properties, which are set on the material in advance, and have another property based on which the shader selects which texture is actually sampled. Then I thought of another approach, which is to have a script on the gameobject for the quad which sets the right texture using a property block.

    I started wondering what the better approach would be. I suppose in the shader-based method, all three textures are always loaded to the GPU, regardless of which one is shown, so it consumes a bit more memory. It also takes up sampler slots but that is no issue for this particular use case. This would all be a tradeoff for having to swap the actual texture property using a property block, which I'm assuming deallocates the old texture and reallocates the new texture, which costs frame time.

    I am not super well-versed in how material and shaders exactly work, so I was wondering if these assumptions are correct.
     
  2. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    560
    I would say, usually you'd have multiple materials with different textures but it depends a bit what your use case is. There is not a single best approach.

    With a single quad, it won't matter what you do but if you have hundreds or thousands of quads or meshes, it does start to matter.

    The cost for a draw call is relatively high in Unity. Combining multiple materials into one is one way to reduce the number of draw calls at the expense of some additional per-pixel cost for dynamic branching. However, in URP and HDRP there is a new SRP Batcher which reduces the cost of material changes, so this is no longer as big of an issue as it used to be. I would say, don't do any premature optimization.

    Regarding texture memory, Unity won't unload textures immediately. They are only unloaded when there are no references anymore and you either load a new scene or call UnloadUnusedAssets manually (slow!). Textures are loaded when the objects that reference them are loaded. Let's say you call Resources.Load on a mesh, then all the materials of the mesh are loaded as well and all the textures are loaded that are referenced in those materials.

    If you are worried about texture memory, read up on texture streaming
    https://docs.unity3d.com/2019.1/Documentation/Manual/TextureStreaming.html

    PS: If you really just have one quad that shows exactly one texture at a time (with maybe a second on top), switch them at runtime in C# (you can either change them on the material, in a MaterialPropertyBlock or change the entire material). Having an if in the shader has a per-pixel cost for dynamic branching and there will be only one draw call either way since it's just a single quad with a single material.
     
    Last edited: May 17, 2023
  3. cvbattum

    cvbattum

    Joined:
    Apr 20, 2018
    Posts:
    12
    I suppose in my use case, it really doesn't matter too much either way, and in no way this would be an actual measurable optimization. I was asking more out of curiosity to be honest. The textures are less than a megabyte each, and at most I'll be rendering 6 of the quads in a single frame (that in fact is a big optimization, because before I used to render all my tiles as separate object; in other words, I easily had thousands of draw calls).

    So as I'm understanding, there is barely any actual overhead for setting textures through script nowadays (with URP/HDRP)?
     
  4. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    560
    Should be almost free since you are just changing the reference to the texture.

    There is only a cost for creating the material or making a copy of the material but that is a one time cost.
    For example, the first time you call Renderer.material, a copy of the material will be made (which you have to destroy!). Creating a MaterialPropertyBlock is also not free but you only need one for all instances of the object.

    One thing you can optimize is how you set the texture. Instead of calling
    material.SetTexture("_MyTexture", texture); // needs to call Shader.PropertyToId internally every time
    you should cache the shader property id.
    static int _myTextureId = Shader.PropertyToId("_MyTexture");
    material.SetTexture(_myTextureId, texture);

    PS: Whether MaterialPropertyBlocks are a good idea depends on the rendering pipeline that you use. MPBs break the SRPBatcher in URP & HDRP but are a great tool for instancing in the BiRP. But you don't have to worry about this if you just have a single quad.
     
    Last edited: May 18, 2023
  5. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    AFAIK (and IANAUD), if the Texture2D object exists in Unity, the Texture2D has some sort of API-level object behind it (on D312, that might be a ID3D12Resource plus a ShaderResourceView). These things are already on the GPU (or ready to be swapped there by the OS in a way Unity doesn't control)*. If you already have a set of textures set on your object (or materials which reference those textures), then it's just setting some pointers in internal buffers which it does on every draw call anyways. So it's essentially zero-cost to swap them.

    At that point, it comes down to how you load/manage the textures (or materials). If they're referenced in your scene, then they're initialized when you start the scene, and will persist in memory until the scene is unloaded. If you're calling
    Resources.Load()
    , using an Addressable, using an AssetBundle, etc, then that's where the load is going to happen.

    (* I think that's not always 100% true in the Editor since the Editor has its own ways of loading and unloading things. And, as c0d3_m0nk3y mentioned, texture streaming means that you can set it up so that only some mipmaps are loaded, although I wouldn't recommend that except for environment textures in mostly-static worlds with unconstrained 3D cameras)