Search Unity

Question Hiding texture tiling of triplanar terrain shader? (for mobile)

Discussion in 'Shaders' started by look001, Dec 13, 2023.

  1. look001

    look001

    Joined:
    Mar 23, 2017
    Posts:
    111
    Hey there,

    im working on a terrain project. The terrain shader uses multiple textures and renders them depending on world height using triplanar mapping. This is the mapping function...

    Code (CSharp):
    1. float3 triplanar(float3 pos, float scale, float3 blend, int texIndex) {
    2.     float3 scaled = pos / scale;
    3.     float3 x_projected = blend.x * UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaled.y, scaled.z, texIndex));
    4.     float3 y_projected = blend.y * UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaled.x, scaled.z, texIndex));
    5.     float3 z_projected = blend.z * UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaled.x, scaled.y, texIndex));
    6.     return x_projected + y_projected + z_projected;
    7. }

    To avoid tiling of implemented this techique from here...

    Code (CSharp):
    1. float4 TextureNoTile(sampler2D tex, float2 uv)
    2. {
    3.     float2 x = uv;
    4.     float v = 1.0; // Set your desired 'v' value here
    5.     float k = tex2D(_Variation, 0.05 * x).r; // cheap (cache-friendly) lookup
    6.  
    7.     float2 duvdx = ddx(uv);
    8.     float2 duvdy = ddy(uv);
    9.  
    10.     float l = k * 8.0;
    11.     float f = frac(l);
    12.  
    13.     float ia = floor(l); // my method
    14.     float ib = ia + 1.0;
    15.  
    16.     float2 offa = sin(float2(3.0, 7.0) * ia); // can replace with any other hash
    17.     float2 offb = sin(float2(3.0, 7.0) * ib); // can replace with any other hash
    18.  
    19.     float4 cola = tex2Dgrad(tex, uv + v * offa, duvdx, duvdy);
    20.     float4 colb = tex2Dgrad(tex, uv + v * offb, duvdx, duvdy);
    21.  
    22.     return lerp(cola, colb, smoothstep(0.2, 0.8, f - 0.1 * dot(cola - colb, 1.0)));
    23. }
    Tiling is no longer visible. Now I need to combine the shaders, but I see a few problems here:

    1. I'm using about 5 texture layers for the terrain and I plan on targeting mobile. I feel like sampling twice for each layer to avoid tiling is pretty expensive especially for the plattform. Do you know of any techniques to avoid having so many gpu instructions to achieve this?

    2. As far as I know UNITY_SAMPLE_TEX2DARRAY can not be called providing a gradient ddy/ddy. Is there another way without reimplement it. I didn't find any resources explaining what TEX2DARRAY actually does behind the scenes. OR...

    3. I would like to not use texture arrays at all as it is not supported on OpenGL ES 2.0. Is it a good idea to pack all textures into one instead, and sample with tex2Dgrad (offsetting the uvs for each layer). The number of textures are about 512x512 large.

    Thank you for your help!
     
    Last edited: Dec 13, 2023
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,336
    1. Yes. It'll be expensive. The "alternative" is to have a much more complex setup where you calculate what texture and UV you want per a grid or voronoi cell and blend between those so you're only ever doing 4 samples at a time.

    2. You're correct, UNITY_SAMPLE_TEX2DARRAY itself cannot be called with gradients. But that's because that's just a macro that's calling the non-gradient version of
    Sample()
    , and Unity didn't bother to write versions that call
    SampleGrad()
    .
    Code (csharp):
    1. #define UNITY_SAMPLE_TEX2DARRAY(tex,coord) tex.Sample (sampler##tex,coord)
    Basically
    UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaled.y, scaled.z, texIndex))
    is being converted into
    baseTextures.Sample(samplerbaseTextures, float3(scaled.y, scaled.z, texIndex))
    at compile time. If you want to use gradients like you would with
    tex2Dgrad()
    , use this instead:
    Code (csharp):
    1. baseTextures.SampleGrad(samplerbaseTextures, float3(scaled.y, scaled.z, texIndex), duvdx, duvdy)
    3. Then you're going to have to accept this shader being very expensive on those platforms, or have a fallback version of the shader that doesn't use the anti-tiling, and maybe not even triplanar.
     
    look001 likes this.
  3. look001

    look001

    Joined:
    Mar 23, 2017
    Posts:
    111
    Thank you, this was very helpful! I didn't find any details about the grid / voronoi cell technique. Do you know of any articles about it?

    I simlified the shader now, by using a 2x2 texture atlas. The shader then decides what texture offset to choose based on uvs (float4) representing each layer weight. To simlify, the shader only ever blends between two adjacent layers. Both layer indices are calculated directly from the weights. Therefore, instead of always sampling all 4 layers it samples only 2 at a time. Still this lefts me with 6 samples for triplanar mapping and in case of anti tiling it doubles to 12. I will probably only apply anti tiling on the y projection, as tiling is most visible on flat areas (resulting in 8 samples).

    Furthermore, I still have issues with jumping mipmaps when looking from an angle (see attached image). I'm not sure if this is right way to calculate the derivatives for triplanar mapping but it is the best I could acheive.

    If you have any ideas for improvement, I would be very happy about it. Thank you very much!

    The shader looks like this now:

    Code (CSharp):
    1.  
    2. CGPROGRAM
    3. #pragma surface surf Lambert noforwardadd vertex:vert
    4.  
    5. sampler2D _MainTex;
    6. float4 _MainTex_ST;
    7. sampler2D _BumpMap;
    8. sampler2D _Variation;
    9.  
    10. float _AtlasSize;
    11. float _Padding;
    12.  
    13. struct Input {
    14.    float3 worldPos;
    15.    float3 worldNormal;
    16.  
    17.    float2 weights0;
    18.    float2 weights1;
    19. };
    20.  
    21. void vert(inout appdata_full v, out Input o) {
    22.    o.worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1)).xyz;
    23.    o.worldNormal = mul(unity_ObjectToWorld, float4(v.normal, 0)).xyz;
    24.     o.weights0 = v.texcoord.xy;
    25.     o.weights1 = v.texcoord.zw;
    26. }
    27.  
    28. float4 texTri(float3 worldPos, float scale, float index, float3 blendAxes)
    29. {
    30.    float2 uvx = worldPos.yz * scale;
    31.    float2 uvy = worldPos.zx * scale;
    32.    float2 uvz = worldPos.xy * scale;
    33.  
    34.    // Get offset to tile in atlas
    35.    float tileSetDim = 2;
    36.    float xpos = fmod(index, tileSetDim);
    37.    float ypos = floor(index / tileSetDim);
    38.    float2 uv = float2(xpos, ypos) / tileSetDim;
    39.  
    40.    // Offset to fragment position inside tile
    41.    float2 offsetX = frac(uvx) / _Padding;
    42.    float2 offsetY = frac(uvy) / _Padding;
    43.    float2 offsetZ = frac(uvz) / _Padding;
    44.  
    45.    // Sample based on gradient and set output
    46.    float2 uvr = (uvx + uvy + uvz) * 0.3333 / _Padding;
    47.  
    48.    float4 cx = tex2Dgrad(_MainTex, uv + offsetX, ddx(uvr), ddy(uvr)) * blendAxes.x;
    49.    float4 cy = tex2Dgrad(_MainTex, uv + offsetY, ddx(uvr), ddy(uvr)) * blendAxes.y;
    50.    float4 cz = tex2Dgrad(_MainTex, uv + offsetZ, ddx(uvr), ddy(uvr)) * blendAxes.z;
    51.  
    52.    return cx + cy + cz;
    53. }
    54.  
    55.  
    56. void surf (Input IN, inout SurfaceOutput o) {
    57.    float w0 = IN.weights0.x;
    58.    float w1 = IN.weights0.y;
    59.    float w2 = IN.weights1.x;
    60.    float w3 = IN.weights1.y;
    61.  
    62.    float c1 = ceil(w1);
    63.    float c2 = ceil(w2);
    64.    float c3 = ceil(w3);
    65.  
    66.    // Finding indices
    67.    float indexA = c2 * 2;
    68.    float indexB = c1 + c3 * 3;
    69.    // test check
    70.    // c0      => iA = 0 ; iB = x
    71.    // c0 & c1 => iA = 0 ; iB = 1
    72.    // c1      => iA = x ; iB = 1
    73.    // c1 & c2 => iA = 2 ; iB = 1
    74.    // c2      => iA = 2 ; iB = x
    75.    // c2 & c3 => iA = 2 ; iB = 3
    76.    // c3      => iA = x ; iB = 3
    77.  
    78.    // Finding Blending Weight
    79.    float wA = w0 * c0 + w2 * c2;
    80.    float wB = w1 * c1 + w3 * c3;
    81.    // test check
    82.    // c0      => wA = w0 ; wB = 0
    83.    // c0 & c1 => wA = w0 ; wB = w1
    84.    // c1      => wA = 0  ; wB = w1
    85.    // c1 & c2 => wA = w2 ; wB = w1
    86.    // c2      => wA = w2 ; wB = 0
    87.    // c2 & c3 => wA = w2 ; wB = w3
    88.    // c3      => wA = 0  ; wB = w3
    89.  
    90.    // normalize weights
    91.    wA = wA / (wA + wB);
    92.    wB = wB / (wA + wB);
    93.  
    94.    float scale = 0.1;
    95.  
    96.    float3 blendAxes = abs(IN.worldNormal);
    97.    blendAxes /= blendAxes.x + blendAxes.y + blendAxes.z;
    98.  
    99.    o.Albedo = texTri(IN.worldPos, scale, indexA, blendAxes) * wA +
    100.             texTri(IN.worldPos, scale, indexB, blendAxes) * wB;
    101. }
    102. ENDCG
    103.  
     

    Attached Files:

    Last edited: Dec 14, 2023