Search Unity

Calculate Vertex Normals in shader from heightmap.

Discussion in 'Shaders' started by MylesLambert, Feb 11, 2013.

  1. MylesLambert

    MylesLambert

    Joined:
    Dec 31, 2012
    Posts:
    61
    Hey! I've been attempting to calculate the vertex normals in a shader due to the mesh being dynamic. I figured I could do this by grabbing 4 heightmap colour offsets and cross product/ averaging them, but this is giving me just a single normal direction over the entire mesh.

    Here is my shader code :

    Code (csharp):
    1.         void vert (inout appdata_full v) {
    2.        
    3.         // VERTEX NORMALS START
    4.             float2 v1UVs = v.texcoord;
    5.                 v1UVs.y -= 0.001;
    6.                
    7.             float2 v3UVs = v.texcoord;
    8.                 v3UVs.y += 0.001;
    9.                
    10.             float2 v4UVs = v.texcoord;
    11.                 v4UVs.x -= 0.001;
    12.                
    13.             float2 v2UVs = v.texcoord;
    14.                 v2UVs.x += 0.001;
    15.                
    16.             float4 heightMapv1 = tex2Dlod (_HeightMap, float4(v1UVs,0,0));
    17.             float4 heightMapv2 = tex2Dlod (_HeightMap, float4(v2UVs,0,0));
    18.             float4 heightMapv3 = tex2Dlod (_HeightMap, float4(v3UVs,0,0));
    19.             float4 heightMapv4 = tex2Dlod (_HeightMap, float4(v4UVs,0,0));
    20.            
    21.             float heightv1 = ((heightMapv1.r - 0.5) *2) * 10;
    22.             float heightv2 = ((heightMapv2.r - 0.5) *2) * 10;
    23.             float heightv3 = ((heightMapv3.r - 0.5) *2) * 10;
    24.             float heightv4 = ((heightMapv4.r - 0.5) *2) * 10;
    25.            
    26.             float3 vectorv1 = (0,heightv1,1);
    27.             float3 vectorv2 = (1,heightv2,0);
    28.             float3 vectorv3 = (0,heightv3,-1);
    29.             float3 vectorv4 = (-1,heightv4,0);
    30.            
    31.            
    32.             float3 crossv12 = normalize(cross(vectorv1, vectorv2));
    33.             float3 crossv23 = normalize(cross(vectorv1, vectorv4));
    34.             float3 crossv34 = normalize(cross(vectorv3, vectorv4));
    35.             float3 crossv41 = normalize(cross(vectorv3, vectorv2));
    36.            
    37.             float3 average12 = normalize((crossv12 + crossv23) * 0.5);
    38.             float3 average34 = normalize((crossv34 + crossv41) * 0.5);
    39.            
    40.             float3 average = normalize((average12 + average34) * 0.5);
    41.            
    42.             v.normal = average;
    43.            
    44.         //VERTEX NORMALS END
    45.        
    46.             float4 heightMap = tex2Dlod (_HeightMap, float4(v.texcoord.xy,0,0));
    47.             v.vertex.y += ((heightMap.r - 0.5) *2) * 10;
    48.         }
    I do get some strange warnings on the shader too :

    implicit truncation of vector type, comma expression used where a vector constructor may have been intended, potentially unintended use of a comma expression in a variable initializer, floating point division by zero.

    Sorry to duplicate this from answers, but I have had no help before on more complex issues :)

    Thanks!
     
  2. MylesLambert

    MylesLambert

    Joined:
    Dec 31, 2012
    Posts:
    61
    I managed to make a little more progress with this by trying the heightmap based off the gradient, of nearby pixels and comparing them to the current, this does produce some results this time! But far from what I'm looking for. :(

    Code (csharp):
    1.         void vert (inout appdata_full v) {
    2.        
    3.         // VERTEX NORMALS START
    4.             float2 v1UVs = v.texcoord;
    5.                 v1UVs.y -= 0.01;
    6.                
    7.             float2 v3UVs = v.texcoord;
    8.                 v3UVs.y += 0.01;
    9.                
    10.             float2 v4UVs = v.texcoord;
    11.                 v4UVs.x -= 0.01;
    12.                
    13.             float2 v2UVs = v.texcoord;
    14.                 v2UVs.x += 0.01;
    15.                
    16.             float4 heightMap = tex2Dlod (_HeightMap, float4(v.texcoord.xy,0,0));
    17.             float4 heightMapv1 = tex2Dlod (_HeightMap, float4(v1UVs,0,0));
    18.             float4 heightMapv2 = tex2Dlod (_HeightMap, float4(v2UVs,0,0));
    19.             float4 heightMapv3 = tex2Dlod (_HeightMap, float4(v3UVs,0,0));
    20.             float4 heightMapv4 = tex2Dlod (_HeightMap, float4(v4UVs,0,0));
    21.            
    22.             float height = heightMap.r;
    23.             float heightv1 = heightMapv1.r;
    24.             float heightv2 = heightMapv2.r;
    25.             float heightv3 = heightMapv3.r;
    26.             float heightv4 = heightMapv4.r;
    27.            
    28.             heightv1 -= height;
    29.             heightv2 -= height;
    30.             heightv3 -= height;
    31.             heightv4 -= height;
    32.            
    33.             float3 average;
    34.             average.x = heightv4 - heightv3;
    35.             average.z = heightv1 - heightv2;
    36.             average.y = 0;
    37.            
    38.             v.normal = normalize(average);
    39.            
    40.         //VERTEX NORMALS END
    41.        
    42.            
    43.             v.vertex.y += ((heightMap.r - 0.5) *2) * 10;
    44.         }
     
  3. MylesLambert

    MylesLambert

    Joined:
    Dec 31, 2012
    Posts:
    61
    Managed to fix all of the shader warnings I was getting by using

    Code (csharp):
    1. float3 vectorv1 = float3(0,heightv1,1);
    rather than

    Code (csharp):
    1. float3 vectorv1 = (0,heightv1,1);
    I'm still not getting the results I was expecting though! :confused:
     
  4. scrawk

    scrawk

    Joined:
    Nov 22, 2012
    Posts:
    804
    Try something like this

    Code (csharp):
    1.  
    2.  
    3.                         float3 FindNormal(sampler2D tex, float2 uv, float u)
    4.             {
    5.                     //u is one uint size, ie 1.0/texture size
    6.                 float2 offsets[4];
    7.                 offsets[0] = uv + float2(-u, 0);
    8.                 offsets[1] = uv + float2(u, 0);
    9.                 offsets[2] = uv + float2(0, -u);
    10.                 offsets[3] = uv + float2(0, u);
    11.                
    12.                 float hts[4];
    13.                 for(int i = 0; i < 4; i++)
    14.                 {
    15.                     hts[i] = tex2D(tex, offsets[i]).x;
    16.                 }
    17.                
    18.                 float2 _step = float2(1.0, 0.0);
    19.                
    20.                 float3 va = normalize( float3(_step.xy, hts[1]-hts[0]) );
    21.                 float3 vb = normalize( float3(_step.yx, hts[3]-hts[2]) );
    22.                
    23.                return cross(va,vb).rbg; //you may not need to swizzle the normal
    24.                
    25.             }
    26.  
    Just change the tex2D to tex2DLod if needed.
     
    Last edited: Jul 1, 2013
  5. duke

    duke

    Joined:
    Jan 10, 2007
    Posts:
    763
    hungrybelome and PrimalCoder like this.
  6. 747823

    747823

    Joined:
    Apr 10, 2013
    Posts:
    43
    Just wrote this today, I don't know if this thread will ever be looked at again but in case anyone wants a simple solution with pre-sampled heights: I have not really tested that much so there might be some problems but it seems like a pretty simple task to me, once you actually think about it. This would probably error at the edges of the texture but mine is tiled so...

    Code (csharp):
    1.  
    2. //Returns a normal from a grid of heights
    3. float3 computeNormals( float h_A, float h_B, float h_C, float h_D, float h_N, float heightScale )
    4. {
    5.     //To make it easier we offset the points such that n is "0" height
    6.     float3 va = { 0, 1, (h_A - h_N)*heightScale };
    7.     float3 vb = { 1, 0, (h_B - h_N)*heightScale };
    8.     float3 vc = { 0, -1, (h_C - h_N)*heightScale };
    9.     float3 vd = { -1, 0, (h_D - h_N)*heightScale };
    10.     //cross products of each vector yields the normal of each tri - return the average normal of all 4 tris
    11.     float3 average_n = ( cross(va, vb) + cross(vb, vc) + cross(vc, vd) + cross(vd, va) ) / -4;
    12.     return normalize( average_n );
    13. }
    14.  
    The "grid" would be like this:
    + A +
    D N B
    + C +

    If you aren't getting expected results perhaps you are sampling the "near" points too far or near to the center, i.e. in mine I multiplied the offset by 0.01, values that were much greater or less really screwed it up.
     
    Last edited: Apr 21, 2013
  7. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
  8. Kekec

    Kekec

    Joined:
    Jul 12, 2015
    Posts:
    24
    EDIT (Solved): nevermind ... It looks like I'm having one of those weird days ... of course the reason it does not work in the vertex shader is because the Graphics.Blit operation renders using a quad. And a quad naturally only has 4 corner vertices at which the sampling occurs. :rolleyes:
    I'll leave my original idiotic post here for anyone that might stumble upon it and learn something from it.:D



    Hi

    I've been trying to do what the title says, and so I found this thread and got it working (using slightly modified version of function from dice paper).

    The problem I have is, I'm not sure why does it work if I use it to calculate the normal in the fragment shader, but it is not working when I try to use it in the vertex shader.

    This is the shader that I use in Graphics.Blit to render terrain normalmap into rendertexture that I can use later for the lighting pass.

    Code (csharp):
    1.  
    2. Shader "Terrain/HeightmapToNormalmap"
    3. {
    4.    Properties
    5.    {
    6.        _Heightmap ("Heightmap (A)", 2D) = "black" {}
    7.        _Height ("Height", Range(-10,10)) = 0
    8.        _MipLevel ("MipLevel", Range(0,7)) = 0
    9.    }
    10.  
    11.    SubShader
    12.    {
    13.        Tags
    14.        {
    15.            "RenderType" = "Opaque"
    16.            "Queue" = "Geometry"
    17.        }
    18.        Pass
    19.        {
    20.            CGPROGRAM
    21.        
    22.             #include "UnityCG.cginc"
    23.  
    24.             #pragma target 3.0
    25.             #pragma vertex vertex_shader
    26.             #pragma fragment fragment_shader
    27.        
    28.             sampler2D _Heightmap;
    29.             float4 _Heightmap_TexelSize;
    30.             float _Height;
    31.             int _MipLevel;
    32.  
    33.            // STRUCTS =====================================
    34.        
    35.            struct VS_Input //vertex shader input
    36.            {
    37.                float4 pos : POSITION;
    38.                float2 uv : TEXCOORD0;
    39.            };
    40.        
    41.            struct VS_Output //vertex shader output
    42.            {
    43.                float4 pos: SV_POSITION;
    44.                float3 normal : NORMAL;
    45.                float2 uv : TEXCOORD0;
    46.            };
    47.        
    48.        
    49.            // FUNCTIONS ===================================
    50.        
    51.             float3 filterNormalLod(float4 uv, float texelSize)
    52.             {
    53.                 float4 h;
    54.                 h[0] = tex2Dlod(_Heightmap, uv + float4(texelSize * float2( 0,-1),0,0)).a * _Height;
    55.                 h[1] = tex2Dlod(_Heightmap, uv + float4(texelSize * float2(-1, 0),0,0)).a * _Height;
    56.                 h[2] = tex2Dlod(_Heightmap, uv + float4(texelSize * float2( 1, 0),0,0)).a * _Height;
    57.                 h[3] = tex2Dlod(_Heightmap, uv + float4(texelSize * float2( 0, 1),0,0)).a * _Height;
    58.                 float3 n;
    59.                 n.z = h[0] - h[3];
    60.                 n.x = h[1] - h[2];
    61.                 n.y = 2;
    62.                 return normalize(n);
    63.             }
    64.  
    65.            VS_Output vertex_shader(VS_Input input)
    66.            {
    67.                VS_Output output;
    68.                output.pos = UnityObjectToClipPos(float4(input.uv,0,0));
    69.                 output.uv = input.uv;
    70.  
    71.                 //this is not working - WHY?
    72.                 float4 uvlod = float4(input.uv,0,_MipLevel);
    73.                 output.normal = filterNormalLod(uvlod, _Heightmap_TexelSize.xy);
    74.  
    75.                return output;
    76.            }
    77.        
    78.            float4 fragment_shader(VS_Output input) : COLOR
    79.            {
    80.                 float4 uvlod = float4(input.uv,0,_MipLevel);
    81.                 float3 normal = filterNormalLod(uvlod, _Heightmap_TexelSize.xy);
    82.  
    83.                 //uncomment to use vertex shader normals
    84.                 //normal = input.normal;
    85.  
    86.                 return float4(normal * 0.5 + 0.5, 1);
    87.            }
    88.  
    89.            ENDCG
    90.        }
    91.    }
    92. }
    93.  
    heightmapToNormalFragment.PNG heightmapToNormalVertex.PNG

    Here you can see what it looks like when same function is used in fragment shader and when used in vertex shader (the all green one). It appears as if tex2dlod in vertex shader is not be working, yet my other shader clearly shows that tex2dlod works in the vertex shader as it is sampling the heightmap to displace the mesh.

    Does anyone have any idea as to what might be causing this? (Maybe Graphics.Blit is not supporting vertex texture fetch?):confused:
     
    Last edited: Mar 18, 2018
  9. callebo_FK

    callebo_FK

    Joined:
    Nov 23, 2017
    Posts:
    30
    Since this has been bumped from the grave already I might as well...


    I understand the how it works, but how do you implement this? I'm using a surface shader with an an animated displacement map for vertex manipulation, would this way work for rebuilding the normals?
     
  10. hungrybelome

    hungrybelome

    Joined:
    Dec 31, 2014
    Posts:
    336
  11. TheCelt

    TheCelt

    Joined:
    Feb 27, 2013
    Posts:
    742
  12. otoomey

    otoomey

    Joined:
    Jan 11, 2019
    Posts:
    1
    The reason for this is because they are sampling across a 2 'cell' distance on the heightmap in both the x and z directions, so naturally you want to make the y-component the same length. Otherwise the vector would be biased toward the x and z axes.

    Nevertheless, the code is weird. The `texelAspect` parameter especially. It seem to be a conversion factor for bringing displacement amount into the height map's pixel space. This seems like quite a backwards approach to me. However, the x, z and y values have to be of the same units, otherwise your normals will be wrong. I have adapted the code somewhat to achieve this in my setup, may prove useful to someone:
    Code (CSharp):
    1. float3 filterNormal(float2 uv, float texelSize, int terrainSize)
    2.         {
    3.             float4 h;
    4.             h[0] = tex2D(_HeightTex, uv + texelSize*float2(0,-1)).r * _Displacement;
    5.             h[1] = tex2D(_HeightTex, uv + texelSize*float2(-1,0)).r * _Displacement;
    6.             h[2] = tex2D(_HeightTex, uv + texelSize*float2(1,0)).r * _Displacement;
    7.             h[3] = tex2D(_HeightTex, uv + texelSize*float2(0,1)).r * _Displacement;
    8.  
    9.             float3 n;
    10.             n.z = -(h[0] - h[3]);
    11.             n.x = (h[1] - h[2]);
    12.             n.y = 2 * texelSize * terrainSize; // pixel space -> uv space -> world space
    13.  
    14.             return normalize(n);
    15.         }
    Here `texelSize` is just the length of a single pixel in uv space. `terrainSize`, on the other hand, is the physical width or size of the terrain in world space. `_Displacement` is simply the conversion factor between the heightmap's color values and world space.
     
    Last edited: May 13, 2020