Home Unity

creating normals from alpha/heightmap inside a shader?

AlecMoody
ngon master
Offline / Send Message
AlecMoody ngon master
I am making some custom terrain shaders with strumpy's editor and I want to be able to create normals based on my blend mask. Does anyone know how I can turn grayscale data into basic normal mapping info? I have seen someone do this in unreal but I can't remember where.

Replies

  • Farfarer
    You probably want to get the difference of the values pixel-by-pixel using ddx, I think.

    Off the top of my head... for CG/HLSL;

    float heightmap = your height map value;
    float3 normal;
    normal.x = ddx(heightmap);
    normal.y = ddy(heightmap);
    normal.z = sqrt(1 - normal.x*normal.x - normal.y * normal.y); // Reconstruct z component to get a unit normal.

    For OpenGL, I think the functions are dFdx and dFdy.

    DirectX11 has ddx_fine and ddy_fine which I think give more accurate results.


    That'll give you the normal in, ehr... screen space I think?

    Otherwise you can sample the heightmap multiple times, offsetting it by a pixel and working out the difference from those values. That should get you it in whatever space the base normal is in.

    Nicked frm here; http://www.gamedev.net/topic/594781-hlsl-height-map-to-normal-algorithm/
    float me = tex2D(heightMapSampler,IN.tex).x;
    float n = tex2D(heightMapSampler,float2(IN.tex.x,IN.tex.y+1.0/heightMapSizeY)).x;
    float s = tex2D(heightMapSampler,float2(IN.tex.x,IN.tex.y-1.0/heightMapSizeY)).x;
    float e = tex2D(heightMapSampler,float2(IN.tex.x+1.0/heightMapSizeX,IN.tex.y)).x;
    float w = tex2D(heightMapSampler,float2(IN.tex.x-1.0/heightMapSizeX,IN.tex.y)).x;                
    
    //find perpendicular vector to norm:        
    float3 temp = norm; //a temporary vector that is not parallel to norm
    if(norm.x==1)
    	temp.y+=0.5;
    else
    	temp.x+=0.5;
    
    //form a basis with norm being one of the axes:
    float3 perp1 = normalize(cross(norm,temp));
    float3 perp2 = normalize(cross(norm,perp1));
    
    //use the basis to move the normal in its own space by the offset        
    float3 normalOffset = -bumpHeightScale*(((n-me)-(s-me))*perp1 + ((e-me)-(w-me))*perp2);
    norm += normalOffset;
    norm = normalize(norm);
    
  • Farfarer
    Gave it a try out of curiosity.

    Here's the multi-sample method in a stripped-down surface shader.

    I had to change the handedness from the code above to match Unity (changing the sign of the operation when sampling the e and w values from + to - and vice versa).
    Shader "Debug/Normal Map From Height" {
    	Properties {
    		_Color ("Main Color", Color) = (1,1,1,1)
    		_MainTex ("Diffuse (RGB) Alpha (A)", 2D) = "white" {}
    		_BumpMap ("Normal (Normal)", 2D) = "bump" {}
    		_HeightMap ("Heightmap (R)", 2D) = "grey" {}
    		_HeightmapStrength ("Heightmap Strength", Float) = 1.0
    		_HeightmapDimX ("Heightmap Width", Float) = 2048
    		_HeightmapDimY ("Heightmap Height", Float) = 2048
    	}
    
    	SubShader{
    		Tags { "RenderType" = "Opaque" }
    
    		CGPROGRAM
    
    			#pragma surface surf NormalsHeight
    			#pragma target 3.0
    
    			struct Input
    			{
    				float2 uv_MainTex;
    			};
    
    			sampler2D _MainTex, _BumpMap, _HeightMap;
    			float _HeightmapStrength, _HeightmapDimX, _HeightmapDimY;
    
    			void surf (Input IN, inout SurfaceOutput o)
    			{
    				o.Albedo = fixed3(0.5);
    
    				float3 normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
    
    				float me = tex2D(_HeightMap,IN.uv_MainTex).x;
    				float n = tex2D(_HeightMap,float2(IN.uv_MainTex.x,IN.uv_MainTex.y+1.0/_HeightmapDimY)).x;
    				float s = tex2D(_HeightMap,float2(IN.uv_MainTex.x,IN.uv_MainTex.y-1.0/_HeightmapDimY)).x;
    				float e = tex2D(_HeightMap,float2(IN.uv_MainTex.x-1.0/_HeightmapDimX,IN.uv_MainTex.y)).x;
    				float w = tex2D(_HeightMap,float2(IN.uv_MainTex.x+1.0/_HeightmapDimX,IN.uv_MainTex.y)).x;
    
    				float3 norm = normal;
    				float3 temp = norm; //a temporary vector that is not parallel to norm
    				if(norm.x==1)
    					temp.y+=0.5;
    				else
    					temp.x+=0.5;
    
    				//form a basis with norm being one of the axes:
    				float3 perp1 = normalize(cross(norm,temp));
    				float3 perp2 = normalize(cross(norm,perp1));
    
    				//use the basis to move the normal in its own space by the offset
    				float3 normalOffset = -_HeightmapStrength * ( ( (n-me) - (s-me) ) * perp1 + ( ( e - me ) - ( w - me ) ) * perp2 );
    				norm += normalOffset;
    				norm = normalize(norm);
    
    				o.Normal = norm;
    			}
    
    			inline fixed4 LightingNormalsHeight (SurfaceOutput s, fixed3 lightDir, fixed3 viewDir, fixed atten)
    			{
    				viewDir = normalize(viewDir);
    				lightDir = normalize(lightDir);
    				s.Normal = normalize(s.Normal);
    				float NdotL = dot(s.Normal, lightDir);
    				_LightColor0.rgb = _LightColor0.rgb;
    
    				fixed4 c;
    				c.rgb = float3(0.5) * saturate ( NdotL ) * _LightColor0.rgb * atten;
    				c.a = 1.0;
    				return c;
    			}
    
    		ENDCG
    	}
    	FallBack "VertexLit"
    }
    

    Using the base normal map and the height map I had used to generate a detail normal from.

    heightmapShader.png
  • Farfarer
    Derivative method works, but it's fucking ugly 'cause it's in screen space - really noisy.

    ddx_fine might give better results in DX11, but it looks like crap in DX9.

    heightmapShaderDeriv.png
Sign In or Register to comment.