More than 16 2D samplers?

I’m working on shader that works similar to terrain. I want it to be PBR, and uses base + 4 layers.
If I want to use Albedo, Normal and Metallic then its 5x3 textures + 1 control texture = 16 2d samplers.

As expected i have error “maximum ps_4_0 sampler register index (16) exceeded”.

But my question is how some shaders have more than 16 2d samplers? I see that Uber standard shader have 27 samplers in StandardInput.cginc. I added to Uber Standard Shader 19 new textures, 19 2dsamplers and added it to albedo calculations and it still works with no errors. How?

I know I can use GrabPass to get colors from previous pass to split samplers into 2 other passes.

Also I worked on mod for RTS game from 2007 and its terrain shader contains 10 albedo + 10 normals + few control textures and it worked on Shaders 3.0.

Is there any other, pro tip to work with more samplers?

Yes you’ll have to split your shader into multiple passes and combine the results. Alternatively, you could use texture atlases to alleviate the limit.

You could make use of all channels of a texture to store additional data, to reduce the number of samplers. Basically merge different textures into one texture by using multiple channels. Artists are often not much of a fan of that, but you can write tools/importers to do the merge automatically.

For the Normal texture, you could use 2 texture channels only and reconstruct the 3rd vector component on the fly. If Metallic is also just a grayscale value, you could store it in another channel of the Normal texture. If you need another one-channel information, like Height or so, store it in the 4th channel of that Normal texture.

For example, the Normal texture could store RG=Normal.xy and B=Metallic. Your shader would be pretty hard-coded to this layout then (and several Unity macros won’t work anymore, you shader would be incompatible to other shaders out there, etc). Not sure if it’s a problem for you. I do this quite often, because it often yields performance benefits.

Storing different data to multiple channels of the same texture is actually a common approach to optimize for sampler usage, no need to have eg 4x one-channel texture samplers if you can read the same information using one sampler only.

To answer your original question of how it works in the standard shader even though the file lists a ton of samplers not all of them get used in every pass or every setup. The shader compiler is smart about ignoring ones that don’t get used. You can also sample a single texture multiple times with out running into the limit and there are ways in DX11 to separate the texture and the sampler so you can use even more textures.

You could just removed D3D9 from your Player settings graphics API list and use GLCore instead, where the limit is 32 I believe. I made a similar shader that’s 4 layers of norm/specular/albedo, and a base normal and blend map + ao, resulting in 14 samplers, but unfortunately Unity uses 3 samplers for different things so the limit is actually 13 if you want to be able to use light cookies and GI. Switching to GLCore and D3D11 is fine. I believe GLCore will run on the same hardware/OS as D3D9 would have anyways.

1 Like

I found a way to do it, but its not working on all platforms. Unity allow to declare 2d texture without sampler and then sample texture using other 2dsampler. So I can have 3 2d samplers and use 15 textures with them.

Check “Texture / Sampler declaration macros”

My example:

Shader "Level/PlaneTerrain" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _LowerTex ("Albedo 0 (RGB)", 2D) = "white" {}
        _LowerBumpMap ("Normal 0", 2D) = "white" {}
        _LowerMRA ("Data 0 (R - metal, G - smoothnes, B - ambient", 2D) = "blue" {}
        _Scale0("UV Scale 0", Range(0,40)) = 1

        _MainTex ("Albedo 1 (RGB)", 2D) = "white" {}
        _MainTex2 ("Albedo 2 (RGB)", 2D) = "white" {}
        _MainTex3 ("Albedo 3 (RGB)", 2D) = "white" {}
        _MainTex4 ("Albedo 4 (RGB)", 2D) = "white" {}

        _BumpMap ("Normal 1", 2D) = "white" {}
        _BumpMap2 ("Normal 2", 2D) = "white" {}
        _BumpMap3 ("Normal 3", 2D) = "white" {}
        _BumpMap4 ("Normal 4", 2D) = "white" {}

        _MRA1 ("Data 1", 2D) = "blue" {}
        _MRA2 ("Data 2", 2D) = "blue" {}
        _MRA3 ("Data 3", 2D) = "blue" {}
        _MRA4 ("Data 4", 2D) = "white" {}

        _Scale1("UV Scale 1", Range(0,40)) = 1
        _Scale2("UV Scale 2", Range(0,40)) = 1
        _Scale3("UV Scale 3", Range(0,40)) = 1
        _Scale4("UV Scale 4", Range(0,40)) = 1

        //_Glossiness ("Smoothness", Range(0,1)) = 0.5
        //_Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
      
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0


        //sampler2D _BumpMap, _BumpMap2, _BumpMap3;
        //sampler2D _LowerMRA, _MRA1, _MRA2, _MRA3;
// Instead of declaring sampler2D for every texture we can declare some text2d with samplers and other with no sampler

        UNITY_DECLARE_TEX2D(_LowerTex);
        UNITY_DECLARE_TEX2D(_LowerBumpMap);
        UNITY_DECLARE_TEX2D(_LowerMRA);

        UNITY_DECLARE_TEX2D_NOSAMPLER(_MainTex);
        UNITY_DECLARE_TEX2D_NOSAMPLER(_MainTex2);
        UNITY_DECLARE_TEX2D_NOSAMPLER(_MainTex3);
        UNITY_DECLARE_TEX2D_NOSAMPLER(_MainTex4);

        UNITY_DECLARE_TEX2D_NOSAMPLER(_BumpMap);
        UNITY_DECLARE_TEX2D_NOSAMPLER(_BumpMap2);
        UNITY_DECLARE_TEX2D_NOSAMPLER(_BumpMap3);
        UNITY_DECLARE_TEX2D_NOSAMPLER(_BumpMap4);

        UNITY_DECLARE_TEX2D_NOSAMPLER(_MRA1);
        UNITY_DECLARE_TEX2D_NOSAMPLER(_MRA2);
        UNITY_DECLARE_TEX2D_NOSAMPLER(_MRA3);
        UNITY_DECLARE_TEX2D_NOSAMPLER(_MRA4);
        half _Scale0, _Scale1, _Scale2, _Scale3, _Scale4;

        struct Input {
            float2 uv_MainTex;
            float4 color: Color; // Vertex color
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;


        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo
            fixed4 c0 = UNITY_SAMPLE_TEX2D(_LowerTex, IN.uv_MainTex * _Scale0);
            fixed4 c1 = UNITY_SAMPLE_TEX2D_SAMPLER (_MainTex, _LowerTex, IN.uv_MainTex * _Scale1);
            fixed4 c2 = UNITY_SAMPLE_TEX2D_SAMPLER (_MainTex2, _LowerTex, IN.uv_MainTex * _Scale2);
            fixed4 c3 = UNITY_SAMPLE_TEX2D_SAMPLER (_MainTex3, _LowerTex, IN.uv_MainTex * _Scale3);
            fixed4 c4 = UNITY_SAMPLE_TEX2D_SAMPLER (_MainTex4, _LowerTex, IN.uv_MainTex * _Scale4);

            fixed4 c = c0;
            c = lerp(c, c1, IN.color.r);
            c = lerp(c, c2, IN.color.g);
            c = lerp(c, c3, IN.color.b);
            c = lerp(c, c4, IN.color.a);

            c *= _Color;

            // Normal
            fixed3 nor0 = UnpackNormal(UNITY_SAMPLE_TEX2D(_LowerBumpMap, IN.uv_MainTex * _Scale0));
            fixed3 nor1 = UnpackNormal(UNITY_SAMPLE_TEX2D_SAMPLER(_BumpMap, _LowerBumpMap, IN.uv_MainTex * _Scale1));
            fixed3 nor2 = UnpackNormal(UNITY_SAMPLE_TEX2D_SAMPLER(_BumpMap2, _LowerBumpMap, IN.uv_MainTex * _Scale2));
            fixed3 nor3 = UnpackNormal(UNITY_SAMPLE_TEX2D_SAMPLER(_BumpMap3, _LowerBumpMap, IN.uv_MainTex * _Scale3));
            fixed3 nor4 = UnpackNormal(UNITY_SAMPLE_TEX2D_SAMPLER(_BumpMap4, _LowerBumpMap, IN.uv_MainTex * _Scale4));

            fixed3 nor = nor0;
            nor = lerp(nor, nor1, IN.color.r);
            nor = lerp(nor, nor2, IN.color.g);
            nor = lerp(nor, nor3, IN.color.b);
            nor = lerp(nor, nor4, IN.color.a);

            // Metallic colors
            fixed4 data0 =  UNITY_SAMPLE_TEX2D (_LowerMRA, IN.uv_MainTex * _Scale0);
            fixed4 data1 =  UNITY_SAMPLE_TEX2D_SAMPLER (_MRA1, _LowerMRA, IN.uv_MainTex * _Scale1);
            fixed4 data2 =  UNITY_SAMPLE_TEX2D_SAMPLER (_MRA2, _LowerMRA,IN.uv_MainTex * _Scale2);
            fixed4 data3 =  UNITY_SAMPLE_TEX2D_SAMPLER (_MRA3, _LowerMRA,IN.uv_MainTex * _Scale3);
            fixed4 data4 =  UNITY_SAMPLE_TEX2D_SAMPLER (_MRA4, _LowerMRA,IN.uv_MainTex * _Scale4);

            fixed4 d = data0;
            d = lerp(d, data1, IN.color.r);
            d = lerp(d, data2, IN.color.g);
            d = lerp(d, data3, IN.color.b);
            d = lerp(d, data4, IN.color.a);


            // Assign data
            o.Albedo = c.rgb;
            o.Normal = normalize(nor);
            o.Metallic = d.r;
            o.Smoothness = d.g;
            o.Occlusion = d.b;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Standard"
}

Defining a texture without a sampler like that works only with DX11 (and maybe iOS Metal?). With all variants of OpenGL textures and samplers are intrinsically linked once you’ve gotten to the shader. I think there’s some extensions for OpenGL 3.3 and beyond that allow for connecting a single sampler to multiple textures on the API side prior to the shader running, but this only works on PC since Macs didn’t support past OpenGL 3.1 until quite recently.

That is how I did it as well, but it still runs into a limit on D3D9. But that might be because I have 3 more samplers, one for each material layer so textures can be tiled differently per asset.
You’re missing one of the biggest advantages of doing a material system like this by not at least having one extra sampler for a secondary UV set. Generally the first UV set is for a base normal map, AO, and the material layer want a UV set that utilizes the tiling capabilities.

I don’t need secoundary UV set, because this is for dynamically generated mesh where UVs are same as XZ positions. I use “Scale” floats for tilling.

What about tex2darray? Is it also limited like saparated samplers and textures?

Texture arrays are supported by more platforms.