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. Dismiss Notice

Question Confusion while passing arbitrary data to surface shader via vertex UVs

Discussion in 'Shaders' started by mannyhams, Aug 23, 2021.

  1. mannyhams

    mannyhams

    Joined:
    Feb 6, 2015
    Posts:
    34
    According to docs, UV data is available in surface shaders through the TEXCOORD0...TEXCOORD3 fields:

    "TEXCOORD1, TEXCOORD2 and TEXCOORD3 are the 2nd, 3rd and 4th UV coordinates, respectively."

    Questions:
    1. When the docs say "2nd, 3rd and 4th UV coordinates", they are referring to Mesh#uv...Mesh#uv4, correct?
    2. Assuming "yes" to question 1, what about Mesh#uv5...Mesh#uv8?
    3. The docs say TEXCOORDN can be "float2, float3 or float4"... but the data defined on the Mesh can only be Vector2. Why would TEXCOORDN be anything except float2?
    4. I am passing non-UV-related data into UV data fields because there appears to be no other way. Is there really no easier way to pass arbitrary data into a shader?
    Thanks and apologies for these questions, which I'm sure are trivial for the experienced folk. I've already burned a good amount of time searching online and putzing around in my shader code without success :(
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,238
    1. Yes. * (See 3)

    2. Also supported, the Surface Shader documentation just predates support for those additional UV sets and was not updated.

    3. The
    mesh.uv
    methods for getting and setting the UV data is indeed limited to
    Vector2
    /
    float2
    , but it is really only there for legacy support.

    You want to use the
    mesh.SetUVs()
    and
    mesh.GetUVs()
    functions instead. Those support
    Vector2/3/4
    inputs and outputs.
    https://docs.unity3d.com/2021.1/Documentation/ScriptReference/Mesh.SetUVs.html

    4. If you need it to be per vertex, it is the easiest way that works seamlessly with other Unity rendering systems. Alternatively you could use the vertex ID and supply arbitrary arrays, data textures, or structured buffers to hold data. But these don't play nice with Unity's batching systems.
     
    vectorized-runner and mannyhams like this.
  3. mannyhams

    mannyhams

    Joined:
    Feb 6, 2015
    Posts:
    34
    @bgolus Thanks very much, this is really helpful!

    I'm still confused about how to access all the UV data in the shader, though. Is it always accessible through
    TEXCOORD0...TEXCOORD3
    ?

    I could potentially see that being the case when
    Mesh#uv...Mesh#uv8
    is set to
    Vector2
    data (
    TEXCOORD0
    is populated with
    Mesh#uv/uv2
    ,
    TEXCOORD1
    with
    Mesh#uv3/uv4
    , etc). But I guess the same behavior couldn't hold for passing
    Vector3/4
    data using
    Mesh#setUVs
    because there isn't enough space in
    TEXCOORD0...TEXCOORD3
    .

    Edit: Added inline code tags because it seems they actually do work!
     
    Last edited: Aug 24, 2021
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,238
    Yeah, inline code tags look dreadful in the preview, and quotes. But they do work. For years I used the Courier New font option instead, but stopped because I felt like the inline code tags popped code blocks out a little better.

    If we’re talking explicitly about Surface Shaders, then the “trick” is you need to define your own
    appdata
    struct, and then access the data in the vertex function.
    Code (CSharp):
    1. Shader "Example/Extra Texcoords" {
    2.     Properties {
    3.       _MainTex ("Texture", 2D) = "white" {}
    4.       _Amount ("Extrusion Amount", Range(-1,1)) = 0.5
    5.     }
    6.     SubShader {
    7.       Tags { "RenderType" = "Opaque" }
    8.       CGPROGRAM
    9.       #pragma surface surf Lambert vertex:vert
    10.  
    11. struct appdata {
    12.     float4 vertex : POSITION;
    13.     float4 tangent : TANGENT;
    14.     float3 normal : NORMAL;
    15.     float4 texcoord : TEXCOORD0;
    16.     float4 texcoord1 : TEXCOORD1;
    17.     float4 texcoord2 : TEXCOORD2;
    18.     float4 texcoord3 : TEXCOORD3;
    19.     float4 texcoord4 : TEXCOORD4;
    20.     float4 texcoord5 : TEXCOORD5;
    21.     float4 texcoord6 : TEXCOORD6;
    22.     float4 texcoord7 : TEXCOORD7;
    23.     fixed4 color : COLOR;
    24.     UNITY_VERTEX_INPUT_INSTANCE_ID
    25. };
    26.      
    27.       struct Input {
    28.           float2 uv_MainTex;
    29.           float4 customData;
    30.       };
    31.  
    32.       void vert (inout appdata_full v, out Input o) {
    33.           o.customData = v.texcoord7;
    34.       }
    35.       sampler2D _MainTex;
    36.       void surf (Input IN, inout SurfaceOutput o) {
    37.           o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
    38.           o.Emission = IN.customData; // whatever
    39.       }
    40.       ENDCG
    41.     }
    42.     Fallback "Diffuse"
    43.   }
    Though this doesn’t work 100% of the time since some features of Surface Shaders are hard coded to expect the built in
    appdata_full
    struct.
     
    mannyhams likes this.
  5. mannyhams

    mannyhams

    Joined:
    Feb 6, 2015
    Posts:
    34
    Ah, awesome!

    This helped me smooth out terrain type edges in my prototype square-tile-based world so that it looks a bit less blocky, thanks very much!

    Before:
    upload_2021-8-24_11-46-2.png

    After:
    (TODO fix shader logic to prevent dimming)
    upload_2021-8-24_11-47-16.png

    In case it's useful for anyone I'll paste my WIP shader here - the next thing I'm going to do is attempt to replace all the different textures with a texture atlas, but I wanted to get a naiive texture blend working before attempting that.

    Code (CSharp):
    1. Shader "Custom/VertexColors" {
    2.     Properties {
    3.         _GrasslandTex ("Grassland Texture", 2D) = "white" {}
    4.         _ForestTex ("Forest Texture", 2D) = "white" {}
    5.         _OceanTex ("Ocean Texture", 2D) = "white" {}
    6.         _LakeTex ("Lake Texture", 2D) = "white" {}
    7.         _RiverTex ("River Texture", 2D) = "white" {}
    8.         _CliffTex ("Cliff Texture", 2D) = "white" {}
    9.         _Glossiness ("Smoothness", Range(0,1)) = 0.5
    10.         _Metallic ("Metallic", Range(0,1)) = 0.0
    11.     }
    12.     SubShader {
    13.         Tags { "RenderType"="Opaque" }
    14.         LOD 200
    15.  
    16.         CGPROGRAM
    17.         #pragma surface surf Standard fullforwardshadows vertex:vert
    18.         #pragma target 4.0
    19.  
    20.         struct Input {
    21.             float2 uv_GrasslandTex;
    22.             float2 uv_ForestTex;
    23.             float2 uv_OceanTex;
    24.             float2 uv_LakeTex;
    25.             float2 uv_RiverTex;
    26.             float2 uv_CliffTex;
    27.             float4 color : COLOR;
    28.             // Texture index for the tile to which this vertex belongs
    29.             float self_texture_index;
    30.             // Provided by Unity
    31.             float3 worldPos;
    32.             // Texture indices starting from SquareDirection#North and moving clockwise, ending with SquareDirection#Southeast
    33.             float4 neighbor_texture_indices_from_north;
    34.             // Texture indices starting from SquareDirection#South and moving clockwise, ending with SquareDirection#Northwest
    35.             float4 neighbor_texture_indicies_from_south;
    36.         };
    37.  
    38.         half _Glossiness;
    39.         half _Metallic;
    40.         fixed4 _Color;
    41.         sampler2D _GrasslandTex;
    42.         sampler2D _ForestTex;
    43.         sampler2D _OceanTex;
    44.         sampler2D _LakeTex;
    45.         sampler2D _RiverTex;
    46.         sampler2D _CliffTex;
    47.  
    48.         void vert (inout appdata_full v, out Input o) {
    49.             UNITY_INITIALIZE_OUTPUT(Input, o);
    50.             // This is the hack(?) recommended by Unity devs.
    51.             //    The custom data values we want on a per-pixel basis are packed into a number of
    52.             //    otherwise unused fields (tangent, texcoord1, texcoord2).
    53.             //    https://forum.unity.com/threads/passing-data-to-shaders.12758/
    54.             o.self_texture_index = v.tangent[0];
    55.             o.neighbor_texture_indices_from_north = v.texcoord1;
    56.             o.neighbor_texture_indicies_from_south = v.texcoord2;
    57.         }
    58.  
    59.         float4 eval_texture(float texture_index, Input IN)
    60.         {
    61.             // Comparing with plenty of forgiveness to compensate for the inexact int->float conversion for this data.
    62.             if (texture_index <= 0.5)
    63.             {
    64.                 return tex2D (_GrasslandTex, IN.uv_GrasslandTex);
    65.             }
    66.             if (texture_index <= 1.5)
    67.             {
    68.                 return tex2D (_ForestTex, IN.uv_GrasslandTex);
    69.             }
    70.             if (texture_index <= 2.5)
    71.             {
    72.                 return tex2D (_OceanTex, IN.uv_GrasslandTex);
    73.             }
    74.             if (texture_index <= 3.5)
    75.             {
    76.                 return tex2D (_LakeTex, IN.uv_GrasslandTex);
    77.             }
    78.             if (texture_index <= 4.5)
    79.             {
    80.                 return tex2D (_RiverTex, IN.uv_GrasslandTex);
    81.             }
    82.             if (texture_index <= 5.5)
    83.             {
    84.                 return tex2D (_CliffTex, IN.uv_GrasslandTex);
    85.             }
    86.  
    87.             return float4(1,1,1,1);
    88.         }
    89.  
    90.         // Calculations in this method depend on a tile size of 1
    91.         fixed4 calc_blended_texture(Input IN)
    92.         {
    93.             const float tile_origin_offset = 0.5;
    94.             const float x_weight = (IN.worldPos.x + tile_origin_offset) % 1;
    95.             const float z_weight = (IN.worldPos.z + tile_origin_offset) % 1;
    96.             const float4 summed_texture = eval_texture(IN.self_texture_index, IN) +
    97.                 eval_texture(IN.neighbor_texture_indices_from_north.x, IN) * z_weight +
    98.                  eval_texture(IN.neighbor_texture_indices_from_north.y, IN) * sqrt(pow(x_weight, 2) + pow(z_weight, 2)) +
    99.                  eval_texture(IN.neighbor_texture_indices_from_north.z, IN) * x_weight +
    100.                  eval_texture(IN.neighbor_texture_indices_from_north.w, IN) * sqrt(pow(x_weight, 2) + pow(1 - z_weight, 2)) +
    101.                  eval_texture(IN.neighbor_texture_indicies_from_south.x, IN) * (1 - z_weight) +
    102.                  eval_texture(IN.neighbor_texture_indicies_from_south.y, IN) * sqrt(pow(1 - x_weight, 2) + pow(1 - z_weight, 2)) +
    103.                  eval_texture(IN.neighbor_texture_indicies_from_south.z, IN) * (1 - x_weight) +
    104.                  eval_texture(IN.neighbor_texture_indicies_from_south.w, IN) * sqrt(pow(1 - x_weight, 2) + pow(z_weight, 2));
    105.  
    106.             const float diagonal_distance = 1.4142135623730951;
    107.             return fixed4(summed_texture / (5 + 4 * diagonal_distance));
    108.         }
    109.  
    110.         void surf (Input IN, inout SurfaceOutputStandard o) {
    111.             const fixed4 c = calc_blended_texture(IN);
    112.             o.Albedo = c.rgb * IN.color;
    113.             o.Metallic = _Metallic;
    114.             o.Smoothness = _Glossiness;
    115.             o.Alpha = c.a;
    116.         }
    117.         ENDCG
    118.     }
    119.  
    120.     FallBack "Diffuse"
    121. }
    122.  
     
  6. mannyhams

    mannyhams

    Joined:
    Feb 6, 2015
    Posts:
    34
    Apologies to dig this thread up but I have a quick followup for @bgolus or anyone else knowledgeable:

    If you were to convert this shader over to URP, what would be your favored approach?

    I guess this built-in render pipeline will (eventually) be deprecated in favor of URP where a surface shader equivalent does not (yet) exist. It appears that either shader graph or vert/frag shaders would be the way forward for now, but is one of these two options better than the other in your opinion? Or, does this shader do something which is unachievable by URP right now?
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,238
    For the URP the best option would be to use Shader Graph. There's not anything that you're doing that Shader Graph couldn't, but there is one gotcha for your specific setup.

    I don't believe there is a way to get the tangent data, not unmodified. Last I looked the value you get from a Shader Graph node always has the xyz normalized. Even if you access the object space tangent in a graph that only gets used to modify the vertex (meaning it runs as part of the vertex shader). So you'll need to pack that value in something else, likely the z of the main UV.
     
    mannyhams likes this.
  8. mannyhams

    mannyhams

    Joined:
    Feb 6, 2015
    Posts:
    34
    Gotcha, thanks for your time!! <3