Search Unity

Issue calculating tangent space normals for procedural planet shader

Discussion in 'Shaders' started by dgreenheck, Jul 25, 2020.

  1. dgreenheck

    dgreenheck

    Joined:
    May 19, 2018
    Posts:
    21
    I'm working on a procedural planet generator based on fractal 3D noise. I was previously using Godot and had a working shader in GLSL but am moving over to Unity so I have been porting my shader code over to HLSL. I basically got it working but am having issues calculating the tangent space normals from my 3D noise function. I must be making an incorrect assumption somewhere since the shadows in my model are not consistent with the light direction.

    The attached image below shows the odd behavior. Some parts of the sphere have correct light but I am noticing a weird "pole" effect at other parts of the sphere. In the scene I have a single directional light shining directly down on the sphere.

    I know my approach to calculating the surface normals is correct since it worked fine in Godot, so I'm thinking there's some issue with the transform to tangent space. I did the spherical to Cartesian coordinate transform derivations by hand to make sure my angles made sense. I am calculating the tangent and bi-tangent vectors in object space by taking the partials of the Cartesian coordinates with respect to theta and phi. After I calculate my surface normals (in object space) from my noise map, I project the normal onto each axis of the tangent vectors in object space to get the normal in tangent space.

    Also, I'm open to any feedback/suggestions. I haven't use surface shaders before so if there is a more efficient way of doing things I would be happy to learn!

    Code (CSharp):
    1.  
    2. // AUTHOR:         Dan Greenheck
    3. // DESCRIPTION:    Procedural planet generator
    4. // DATE CREATED:   2020-07-25
    5. //
    6. // --------------------------------------------------------------------------------
    7.  
    8. Shader "Custom/Planet" {
    9.     Properties {
    10.         _OceanColor ("Ocean Color", Color) = (0.08, 0.25, 0.41, 1.0)
    11.         _BeachColor ("Beach Color", Color) = (0.89, 0.80, 0.61, 1.0)
    12.         _PlainsColor ("Plains Color", Color) = (0.17, 0.29, 0.16, 1.0)
    13.         _TreeColor ("Tree Color", Color) = (0.10, 0.20, 0.10, 1.0)
    14.         _MountainColor ("Mountain Color", Color) = (0.75, 0.75, 0.75, 1.0)
    15.         _BumpStrength ("Bump Strength", Range(0.01, 1.0)) = 0.5
    16.         _Scale ("Scale", Range(0.0, 2.0)) = 1.06
    17.         _Period ("Period", Range(0.01, 0.5)) = 0.339
    18.         _Octaves ("Octaves", Range(1, 10)) = 5
    19.         _Lacunarity ("Lacunarity", Range(0.1, 10.0)) = 2.447
    20.         _Persistence ("Persistence", Range(0.0, 1.0)) = 0.493
    21.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    22.     }
    23.  
    24.     SubShader {
    25.         Tags { "RenderType"="Opaque" }
    26.         LOD 200
    27.  
    28.         CGPROGRAM
    29.         // Physically based Standard lighting model, and enable shadows on all light types
    30.         #pragma surface surf Standard vertex:vert
    31.  
    32.         // Use shader model 3.0 target, to get nicer looking lighting
    33.         #pragma target 3.0
    34.  
    35.         #include "UnityCG.cginc"
    36.  
    37.         // Magnitude of offset used for calculating tangent vectors
    38.         static const float delta = 0.0001;
    39.  
    40.         // Thresholds controlling transitions between each terrain color
    41.         static const float th_beach_min = 0.53;
    42.         static const float th_beach_max = 0.55;
    43.         static const float th_plains_min = 0.50;
    44.         static const float th_plains_max = 0.60;
    45.         static const float th_trees_min = 0.50;
    46.         static const float th_trees_max = 0.62;
    47.         static const float th_mountain_min = 0.69;
    48.         static const float th_mountain_max = 0.75;
    49.  
    50.         // struct SurfaceOutputStandard {
    51.         //     fixed3 Albedo;      // base (diffuse or specular) color
    52.         //     fixed3 Normal;      // tangent space normal, if written
    53.         //     half3 Emission;
    54.         //     half Metallic;      // 0=non-metal, 1=metal
    55.         //     half Smoothness;    // 0=rough, 1=smooth
    56.         //     half Occlusion;     // occlusion (default 1)
    57.         //     fixed Alpha;        // alpha for transparencies
    58.         // };
    59.  
    60.         struct Input {
    61.             float2 uv_MainTex;
    62.             float3 pos;
    63.         };
    64.  
    65.         fixed4 _OceanColor;
    66.         fixed4 _BeachColor;
    67.         fixed4 _PlainsColor;
    68.         fixed4 _TreeColor;
    69.         fixed4 _MountainColor;
    70.         float _BumpStrength;
    71.         float _Scale;
    72.         float _Period;
    73.         int _Octaves;
    74.         float _Lacunarity;
    75.         float _Persistence;
    76.  
    77.         // Author: Inigo Quilez
    78.         // https://www.shadertoy.com/view/Xsl3Dl
    79.         float3 hash(float3 p) {
    80.             p = float3(dot(p, float3(127.1, 311.7, 74.7)),
    81.                     dot(p, float3(269.5, 183.3, 246.1)),
    82.                     dot(p, float3(113.5, 271.9, 124.6)));
    83.  
    84.             return -1.0 + 2.0 * frac(sin(p) * 43758.5453123);
    85.         }
    86.  
    87.         // Author: Inigo Quilez
    88.         // https://www.shadertoy.com/view/Xsl3Dl
    89.         float noise(float3 p) {
    90.             float3 i = floor(p);
    91.             float3 f = frac(p);
    92.             float3 u = f * f * (3.0 - 2.0 * f);
    93.            
    94.             float n = lerp(lerp(lerp(dot(hash(i + float3(0.0, 0.0, 0.0)), f - float3(0.0, 0.0, 0.0)),
    95.                                 dot(hash(i + float3(1.0, 0.0, 0.0)), f - float3(1.0, 0.0, 0.0)), u.x),
    96.                             lerp(dot(hash(i + float3(0.0, 1.0, 0.0)), f - float3(0.0, 1.0, 0.0)),
    97.                                 dot(hash(i + float3(1.0, 1.0, 0.0)), f - float3(1.0, 1.0, 0.0)), u.x), u.y),
    98.                         lerp(lerp(dot(hash(i + float3(0.0, 0.0, 1.0)), f - float3(0.0, 0.0, 1.0)),
    99.                                 dot(hash(i + float3(1.0, 0.0, 1.0)), f - float3(1.0, 0.0, 1.0)), u.x),
    100.                             lerp(dot(hash(i + float3(0.0, 1.0, 1.0)), f - float3(0.0, 1.0, 1.0)),
    101.                                 dot(hash(i + float3(1.0, 1.0, 1.0)), f - float3(1.0, 1.0, 1.0)), u.x), u.y), u.z );
    102.                            
    103.             // Normalize to [0.0, 1.0]
    104.             return 0.5*(n + 1.0);
    105.         }
    106.  
    107.         // Converts spherical coordinates to Cartesian coordinates
    108.         float3 sph2cart(float phi, float theta) {
    109.             float3 unit = float3(0.0, 0.0, 0.0);
    110.             unit.x = cos(phi) * cos(theta);
    111.             unit.y = sin(phi) * cos(theta);
    112.             unit.z = sin(theta);
    113.             return normalize(unit);
    114.         }
    115.  
    116.         // Calculates tangent vector for unit sphere
    117.         float3 sph2tan(float phi) {
    118.             float3 tangent = float3(0.0, 0.0, 0.0);
    119.             tangent.x = -sin(phi);
    120.             tangent.y = cos(phi);
    121.             return normalize(tangent);
    122.         }
    123.  
    124.         void vert (inout appdata_full v, out Input o) {
    125.             UNITY_INITIALIZE_OUTPUT(Input,o);
    126.             o.pos = normalize(v.normal);
    127.         }
    128.  
    129.         void surf (Input IN, inout SurfaceOutputStandard o)  {    
    130.            
    131.             // Convert Cartesian position to spherical coordinates
    132.             float phi = atan2(IN.pos.y, IN.pos.x);
    133.             float theta = atan2(IN.pos.z, sqrt(IN.pos.x*IN.pos.x + IN.pos.y*IN.pos.y));
    134.            
    135.             // Normal vector
    136.             float3 N = normalize(IN.pos);
    137.  
    138.             // Perturb the normal vector in the theta/phi directions. The 3D noise wil
    139.             // be sampled at these vectors to later numerically calculate the surface normal
    140.             float3 N_dx = sph2cart(phi + delta, theta);
    141.             float3 N_dy = sph2cart(phi, theta + delta);
    142.            
    143.             // Get the tangent and bi-tangent vectors
    144.             float3 T = sph2tan(phi);
    145.             float3 B = cross(N, T);
    146.  
    147.             // Terrain height, sampled at N, N_dx, N_dy respectively
    148.             float h = 0.0;
    149.             float h_dx = 0.0;
    150.             float h_dy = 0.0;
    151.            
    152.             // ----------- 3D fractal noise calculations ----------
    153.  
    154.             float a = 1.0; // Amplitude for current octave
    155.             float max_amp = a; // Accumulate max amplitude so we can normalize after
    156.             float p = _Period;  // Period for current octave
    157.             for(int i = 0; i < _Octaves; i++) {
    158.                 // Sample the 3D noise
    159.                 h += a*noise(N/p);
    160.                 h_dx += a*noise(N_dx/p);
    161.                 h_dy += a*noise(N_dy/p);
    162.                
    163.                 // Amplitude decay for higher octaves
    164.                 a *= _Persistence;
    165.                 max_amp += a;
    166.                 // Divide period by lacunarity
    167.                 p = p / _Lacunarity;
    168.             }
    169.            
    170.             // --------------------------------------------------
    171.            
    172.             // Scale heights between 0.0 and 1.0 and then scale to
    173.             // adjust land/water distribution
    174.             h /= float(max_amp) / _Scale;
    175.             h_dx /= float(max_amp) / _Scale;
    176.             h_dy /= float(max_amp) / _Scale;
    177.            
    178.             // Exaggerate bump strength for mountains
    179.             float bump_strength_scaled = _BumpStrength;
    180.             if (h > th_mountain_min) {
    181.                 bump_strength_scaled *= 3.0;
    182.             }
    183.  
    184.             // Threshold values for transitioning from each terrain type. Use
    185.             // smoothstep to control transitions between terrain types. Large differences
    186.             // between min/max values will result in smoother transitions.
    187.             float th_ocean2beach = smoothstep(th_beach_min, th_beach_max, h);
    188.             float th_beach2plains = smoothstep(th_plains_min, th_plains_max, h);
    189.             float th_plains2trees = smoothstep(th_trees_min, th_trees_max, h);
    190.             float th_trees2mountains = smoothstep(th_mountain_min, th_mountain_max, h);
    191.  
    192.            
    193.             // Scale unit vectors on sphere by the noise multiplied by bump strength
    194.             float3 R = N*(1.0 + bump_strength_scaled*h);
    195.             float3 R_dx = N_dx*(1.0 + bump_strength_scaled*h_dx);
    196.             float3 R_dy = N_dy*(1.0 + bump_strength_scaled*h_dy);
    197.  
    198.             // Calculate the object-space normal by taking differences between the
    199.             // normal vector and the perturbed normal vectors
    200.             float3 normal_obj = normalize(cross(R_dx - R, R_dy - R));
    201.             // Interpolate between the flat surface normal and the terrain normal
    202.             // This will cause the ocean areas to be shaded as a flat sphere.
    203.             normal_obj = lerp(N, normal_obj, th_ocean2beach);
    204.            
    205.             // Project the normal defined in object space to tangent space
    206.             o.Normal = normalize(float3(dot(normal_obj,T),
    207.                                         dot(normal_obj,B),
    208.                                         dot(normal_obj,N)));
    209.  
    210.             // ---------------- ALBEDO ----------------------------
    211.  
    212.             // lerp between the ocean color and the land color
    213.             float3 color = lerp(_OceanColor.rgb, _BeachColor.rgb, th_ocean2beach);
    214.             color = lerp(color.rgb, _PlainsColor.rgb, th_beach2plains);
    215.             color = lerp(color.rgb, _TreeColor.rgb, th_plains2trees);
    216.             color = lerp(color.rgb, _MountainColor.rgb, th_trees2mountains);
    217.            
    218.             o.Albedo = color.rgb;
    219.             o.Smoothness = 1.0 -lerp(0.35, 1.0, th_ocean2beach);
    220.         }
    221.         ENDCG
    222.     }
    223.     FallBack "Diffuse"
    224. }
     

    Attached Files:

  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Unity's Surface Shaders assume any value set on the
    o.Normal
    is in the same space as the mesh's vertex tangents. My guess is your sphere mesh's tangents don't match those you're calculating.

    Also, if your mesh does already have per vertex tangent data, there's little need to calculate it manually in the shader afterwards. In fact you really shouldn't if you can avoid it as it's more important for the tangent space to match than it is for it to be correct.
     
  3. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I posted an example of how to convert a world space vector to a tangent space vector (approximately) here:
    https://github.com/bgolus/Normal-Ma...der/blob/master/TriplanarSurfaceShader.shader

    See the
    WorldToTangentNormalVector
    function on line 63. You'll need the
    float3 worldNormal;
    and
    INTERNAL_DATA
    in the
    Input
    struct for this to work. You'll also need to convert your object space normals into world space before using that function, but that's a simple enough problem to solve.
    Code (csharp):
    1. // inverse transpose object to world
    2. float normal_world = mul(normal_obj, (float3x3)unity_WorldToObject);
    3. o.Normal = WorldToTangentNormalVector(normal_world);
    The normal, tangent, and bitangent the Surface Shader has access to is by default in world space, hence the need to convert from object to world space. You could pass the vertex tangent from the vert function to avoid that, but it's probably not a lot faster.

    It should be noted that using an transposed matrix as an inverse, as the
    WorldToTangentNormalVector
    function and what your code is doing is only an approximation, but it's close enough that most people won't notice anything wrong. I also posted an example of an accurate inverse world to tangent matrix here:
    https://forum.unity.com/threads/flat-lighting-without-separate-smoothing-groups.280183/#post-5057189
     
    PrimalCoder likes this.