Calculate Vertex Normals in shader from heightmap.

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 :

		void vert (inout appdata_full v) {
		
		// VERTEX NORMALS START
			float2 v1UVs = v.texcoord;
				v1UVs.y -= 0.001;
				
			float2 v3UVs = v.texcoord;
				v3UVs.y += 0.001;
				
			float2 v4UVs = v.texcoord;
				v4UVs.x -= 0.001;
				
			float2 v2UVs = v.texcoord;
				v2UVs.x += 0.001;
				
			float4 heightMapv1 = tex2Dlod (_HeightMap, float4(v1UVs,0,0));
			float4 heightMapv2 = tex2Dlod (_HeightMap, float4(v2UVs,0,0));
			float4 heightMapv3 = tex2Dlod (_HeightMap, float4(v3UVs,0,0));
			float4 heightMapv4 = tex2Dlod (_HeightMap, float4(v4UVs,0,0));
			
			float heightv1 = ((heightMapv1.r - 0.5) *2) * 10;
			float heightv2 = ((heightMapv2.r - 0.5) *2) * 10;
			float heightv3 = ((heightMapv3.r - 0.5) *2) * 10;
			float heightv4 = ((heightMapv4.r - 0.5) *2) * 10;
			
			float3 vectorv1 = (0,heightv1,1);
			float3 vectorv2 = (1,heightv2,0);
			float3 vectorv3 = (0,heightv3,-1);
			float3 vectorv4 = (-1,heightv4,0);
			
			
			float3 crossv12 = normalize(cross(vectorv1, vectorv2));
			float3 crossv23 = normalize(cross(vectorv1, vectorv4));
			float3 crossv34 = normalize(cross(vectorv3, vectorv4));
			float3 crossv41 = normalize(cross(vectorv3, vectorv2));
			
			float3 average12 = normalize((crossv12 + crossv23) * 0.5);
			float3 average34 = normalize((crossv34 + crossv41) * 0.5);
			
			float3 average = normalize((average12 + average34) * 0.5);
			
			v.normal = average;
			
		//VERTEX NORMALS END
		
         	float4 heightMap = tex2Dlod (_HeightMap, float4(v.texcoord.xy,0,0));
          	v.vertex.y += ((heightMap.r - 0.5) *2) * 10;
      	}

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 :slight_smile:

Thanks!

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. :frowning:

		void vert (inout appdata_full v) {
		
		// VERTEX NORMALS START
			float2 v1UVs = v.texcoord;
				v1UVs.y -= 0.01;
				
			float2 v3UVs = v.texcoord;
				v3UVs.y += 0.01;
				
			float2 v4UVs = v.texcoord;
				v4UVs.x -= 0.01;
				
			float2 v2UVs = v.texcoord;
				v2UVs.x += 0.01;
				
			float4 heightMap = tex2Dlod (_HeightMap, float4(v.texcoord.xy,0,0));
			float4 heightMapv1 = tex2Dlod (_HeightMap, float4(v1UVs,0,0));
			float4 heightMapv2 = tex2Dlod (_HeightMap, float4(v2UVs,0,0));
			float4 heightMapv3 = tex2Dlod (_HeightMap, float4(v3UVs,0,0));
			float4 heightMapv4 = tex2Dlod (_HeightMap, float4(v4UVs,0,0));
			
			float height = heightMap.r;
			float heightv1 = heightMapv1.r;
			float heightv2 = heightMapv2.r;
			float heightv3 = heightMapv3.r;
			float heightv4 = heightMapv4.r;
			
			heightv1 -= height;
			heightv2 -= height;
			heightv3 -= height;
			heightv4 -= height;
			
			float3 average;
			average.x = heightv4 - heightv3;
			average.z = heightv1 - heightv2;
			average.y = 0;
			
			v.normal = normalize(average);
			
		//VERTEX NORMALS END
		
         	
          	v.vertex.y += ((heightMap.r - 0.5) *2) * 10;
      	}

Managed to fix all of the shader warnings I was getting by using

float3 vectorv1 = float3(0,heightv1,1);

rather than

float3 vectorv1 = (0,heightv1,1);

I’m still not getting the results I was expecting though! :face_with_spiral_eyes:

Try something like this

                        float3 FindNormal(sampler2D tex, float2 uv, float u)
			{
			        //u is one uint size, ie 1.0/texture size
				float2 offsets[4];
				offsets[0] = uv + float2(-u, 0);
				offsets[1] = uv + float2(u, 0);
				offsets[2] = uv + float2(0, -u);
				offsets[3] = uv + float2(0, u);
				
				float hts[4];
				for(int i = 0; i < 4; i++)
				{
					hts[i] = tex2D(tex, offsets[i]).x;
				}
				
				float2 _step = float2(1.0, 0.0);
				
				float3 va = normalize( float3(_step.xy, hts[1]-hts[0]) );
			    float3 vb = normalize( float3(_step.yx, hts[3]-hts[2]) );
			    
			   return cross(va,vb).rbg; //you may not need to swizzle the normal
			    
			}

Just change the tex2D to tex2DLod if needed.

You read the dice paper on this? It contains the relevant snippet. See top of pg. 43: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.161.8979&rep=rep1&type=pdf

2 Likes

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…

//Returns a normal from a grid of heights
float3 computeNormals( float h_A, float h_B, float h_C, float h_D, float h_N, float heightScale )
{
	//To make it easier we offset the points such that n is "0" height
	float3 va = { 0, 1, (h_A - h_N)*heightScale };
	float3 vb = { 1, 0, (h_B - h_N)*heightScale };
	float3 vc = { 0, -1, (h_C - h_N)*heightScale };
	float3 vd = { -1, 0, (h_D - h_N)*heightScale };
	//cross products of each vector yields the normal of each tri - return the average normal of all 4 tris
	float3 average_n = ( cross(va, vb) + cross(vb, vc) + cross(vc, vd) + cross(vd, va) ) / -4;
	return normalize( average_n );
}

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.

I wrote one for a friend, here - 3rd post on this page;
http://www.polycount.com/forum/showthread.php?t=117185

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. :roll_eyes:
I’ll leave my original idiotic post here for anyone that might stumble upon it and learn something from it.:smile:

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.

Shader "Terrain/HeightmapToNormalmap"
{
   Properties
   {
       _Heightmap ("Heightmap (A)", 2D) = "black" {}
       _Height ("Height", Range(-10,10)) = 0
       _MipLevel ("MipLevel", Range(0,7)) = 0
   }
 
   SubShader
   {
       Tags
       {
           "RenderType" = "Opaque"
           "Queue" = "Geometry"
       }
       Pass
       {
           CGPROGRAM
        
            #include "UnityCG.cginc"

            #pragma target 3.0
            #pragma vertex vertex_shader
            #pragma fragment fragment_shader
        
            sampler2D _Heightmap;
            float4 _Heightmap_TexelSize;
            float _Height;
            int _MipLevel;

           // STRUCTS =====================================
        
           struct VS_Input //vertex shader input
           {
               float4 pos : POSITION;
               float2 uv : TEXCOORD0;
           };
        
           struct VS_Output //vertex shader output
           {
               float4 pos: SV_POSITION;
               float3 normal : NORMAL;
               float2 uv : TEXCOORD0;
           };
        
        
           // FUNCTIONS ===================================
        
            float3 filterNormalLod(float4 uv, float texelSize)
            {
                float4 h;
                h[0] = tex2Dlod(_Heightmap, uv + float4(texelSize * float2( 0,-1),0,0)).a * _Height;
                h[1] = tex2Dlod(_Heightmap, uv + float4(texelSize * float2(-1, 0),0,0)).a * _Height;
                h[2] = tex2Dlod(_Heightmap, uv + float4(texelSize * float2( 1, 0),0,0)).a * _Height;
                h[3] = tex2Dlod(_Heightmap, uv + float4(texelSize * float2( 0, 1),0,0)).a * _Height;
                float3 n;
                n.z = h[0] - h[3];
                n.x = h[1] - h[2];
                n.y = 2;
                return normalize(n);
            }

           VS_Output vertex_shader(VS_Input input)
           {
               VS_Output output;
               output.pos = UnityObjectToClipPos(float4(input.uv,0,0));
                output.uv = input.uv;

                //this is not working - WHY?
                float4 uvlod = float4(input.uv,0,_MipLevel);
                output.normal = filterNormalLod(uvlod, _Heightmap_TexelSize.xy);

               return output;
           }
        
           float4 fragment_shader(VS_Output input) : COLOR
           {
                float4 uvlod = float4(input.uv,0,_MipLevel);
                float3 normal = filterNormalLod(uvlod, _Heightmap_TexelSize.xy);

                //uncomment to use vertex shader normals
                //normal = input.normal;

                return float4(normal * 0.5 + 0.5, 1);
           }

           ENDCG
       }
   }
}

3429118--270918--heightmapToNormalFragment.PNG 3429118--270919--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?):face_with_spiral_eyes:

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?

You saved my life!

The paper does not explain why the y component is equal to 2. That magic number is bit odd.

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:

float3 filterNormal(float2 uv, float texelSize, int terrainSize)
        {
            float4 h;
            h[0] = tex2D(_HeightTex, uv + texelSize*float2(0,-1)).r * _Displacement;
            h[1] = tex2D(_HeightTex, uv + texelSize*float2(-1,0)).r * _Displacement;
            h[2] = tex2D(_HeightTex, uv + texelSize*float2(1,0)).r * _Displacement;
            h[3] = tex2D(_HeightTex, uv + texelSize*float2(0,1)).r * _Displacement;

            float3 n;
            n.z = -(h[0] - h[3]);
            n.x = (h[1] - h[2]);
            n.y = 2 * texelSize * terrainSize; // pixel space -> uv space -> world space

            return normalize(n);
        }

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.