Parallax Terrain Shader Problem

Latest package: Dropbox - File Deleted - Simplify your life

I’m trying to add parallax effect to default terrain shader “TerrBumpFirstPass” which comes with Unity 4. Here’s the current code:

Shader "Custom/Simple Terrain Parallax" {
    Properties {
        _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
        _Shininess ("Shininess", Range (0.03, 1)) = 0.078125
        _Parallax ("Height", Range (0.005, 0.108)) = 0.02
   
        // set by terrain engine
        [HideInInspector] _Control ("Control (RGBA)", 2D) = "red" {}
        [HideInInspector] _Splat3 ("Layer 3 (A)", 2D) = "white" {}
        [HideInInspector] _Splat2 ("Layer 2 (B)", 2D) = "white" {}
        [HideInInspector] _Splat1 ("Layer 1 (G)", 2D) = "white" {}
        [HideInInspector] _Splat0 ("Layer 0 (R)", 2D) = "white" {}
        [HideInInspector] _Normal3 ("Normal 3 (A)", 2D) = "bump" {}
        [HideInInspector] _Normal2 ("Normal 2 (B)", 2D) = "bump" {}
        [HideInInspector] _Normal1 ("Normal 1 (G)", 2D) = "bump" {}
        [HideInInspector] _Normal0 ("Normal 0 (R)", 2D) = "bump" {}
        // used in fallback on old cards  base map
        [HideInInspector] _MainTex ("BaseMap (RGB)", 2D) = "white" {}
        [HideInInspector] _Color ("Main Color", Color) = (1,1,1,1)
    }
   
    SubShader {
        Tags {
            "SplatCount" = "4"
            "Queue" = "Geometry-100"
            "RenderType" = "Opaque"
        }
       
        CGPROGRAM
        #pragma surface surf BlinnPhong vertex:vert
        #pragma target 3.0
       
        sampler2D _Control;
        sampler2D _Splat0,_Splat1,_Splat2,_Splat3;
        sampler2D _Normal0,_Normal1,_Normal2,_Normal3;
        half _Shininess;
        float _Parallax;
       
        struct Input {
            float2 uv_Control : TEXCOORD0;
            float2 uv_Splat0 : TEXCOORD1;
            float2 uv_Splat1 : TEXCOORD2;
            float2 uv_Splat2 : TEXCOORD3;
            float2 uv_Splat3 : TEXCOORD4;
            float3 viewDir;
        };
       
        void vert (inout appdata_full v, out Input o)
        {
            o.uv_Control = v.texcoord;
            o.uv_Splat0 = v.texcoord1;
            o.uv_Splat1 = v.texcoord1;
            o.uv_Splat2 = v.texcoord1;
            o.uv_Splat3 = v.texcoord1;
       
            v.tangent.xyz = cross(v.normal, float3(0,0,1));
            v.tangent.w = -1;
           
            o.viewDir = WorldSpaceViewDir(v.vertex);
        }
       
        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 splat_control = tex2D (_Control, IN.uv_Control);
            fixed4 col;
           
            // Parallax
            half h;
            float2 offset;
           
            h = tex2D (_Splat0, IN.uv_Splat0).w;
            offset = ParallaxOffset (h, _Parallax, IN.viewDir);
            IN.uv_Splat0 += offset;
           
            h = tex2D (_Splat1, IN.uv_Splat1).w;
            offset = ParallaxOffset (h, _Parallax, IN.viewDir);
            IN.uv_Splat1 += offset;
           
            h = tex2D (_Splat2, IN.uv_Splat2).w;
            offset = ParallaxOffset (h, _Parallax, IN.viewDir);
            IN.uv_Splat2 += offset;
           
            h = tex2D (_Splat3, IN.uv_Splat3).w;
            offset = ParallaxOffset (h, _Parallax, IN.viewDir);
            IN.uv_Splat3 += offset;
           
            col  = splat_control.r * tex2D (_Splat0, IN.uv_Splat0);
            col += splat_control.g * tex2D (_Splat1, IN.uv_Splat1);
            col += splat_control.b * tex2D (_Splat2, IN.uv_Splat2);
            col += splat_control.a * tex2D (_Splat3, IN.uv_Splat3);
            o.Albedo = col.rgb;
       
            fixed4 nrm;
            nrm  = splat_control.r * tex2D (_Normal0, IN.uv_Splat0);
            nrm += splat_control.g * tex2D (_Normal1, IN.uv_Splat1);
            nrm += splat_control.b * tex2D (_Normal2, IN.uv_Splat2);
            nrm += splat_control.a * tex2D (_Normal3, IN.uv_Splat3);
           
            fixed splatSum = dot(splat_control, fixed4(1,1,1,1));
            fixed4 flatNormal = fixed4(0.5,0.5,1,0.5);
            nrm = lerp(flatNormal, nrm, splatSum);
            o.Normal = UnpackNormal(nrm);
           
            o.Gloss = col.a * splatSum;
            o.Specular = _Shininess;
       
            o.Alpha = 0.0;
        }
        ENDCG
    }
   
    Dependency "AddPassShader" = "Hidden/Nature/Terrain/Bumped Specular AddPass"
    Dependency "BaseMapShader" = "Specular"
   
    Fallback "Nature/Terrain/Diffuse"
}

As I need to get terrain’s vetex data such as camera distance in surf part, so I need to calculate needed data in vert including out parameters. But you may see that there is a problem with the View Direction “viewDir” or maybe tangents in different terrain angles when you increase “Height” value of the parallax in shader. I’ve also noticed that if you change the name of “viewDir” to something else in shader, the results will vary as it’s a predefined variable in Unity.

I’m really stuck at this part and I’m no expert at shaders. I’m sure solving this issue will help many others and this shader will be useful for many.

I’ve attached sample Diffuse Normal textures. Displacement data is in the alpha channel of the diffuse texture. Add textures to your terrain in Unity and check the shader for yourself.

1414063–74138–$Pavement.zip (1.82 MB)

Better results in some cases, but still have problems!

Shader "Custom/Simple Terrain Parallax" {
	Properties {
		_SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
		_Shininess ("Shininess", Range (0.03, 1)) = 0.078125
		_Parallax ("Height", Range (0.005, 0.108)) = 0.02
	
		// set by terrain engine
		[HideInInspector] _Control ("Control (RGBA)", 2D) = "red" {}
		[HideInInspector] _Splat3 ("Layer 3 (A)", 2D) = "white" {}
		[HideInInspector] _Splat2 ("Layer 2 (B)", 2D) = "white" {}
		[HideInInspector] _Splat1 ("Layer 1 (G)", 2D) = "white" {}
		[HideInInspector] _Splat0 ("Layer 0 (R)", 2D) = "white" {}
		[HideInInspector] _Normal3 ("Normal 3 (A)", 2D) = "bump" {}
		[HideInInspector] _Normal2 ("Normal 2 (B)", 2D) = "bump" {}
		[HideInInspector] _Normal1 ("Normal 1 (G)", 2D) = "bump" {}
		[HideInInspector] _Normal0 ("Normal 0 (R)", 2D) = "bump" {}
		// used in fallback on old cards  base map
		[HideInInspector] _MainTex ("BaseMap (RGB)", 2D) = "white" {}
		[HideInInspector] _Color ("Main Color", Color) = (1,1,1,1)
	}
	
	SubShader {
		Tags {
			"SplatCount" = "4"
			"Queue" = "Geometry-100"
			"RenderType" = "Opaque"
		}
		
        CGPROGRAM
		#pragma surface surf BlinnPhong vertex:vert
		#pragma target 3.0
        
        sampler2D _Control;
		sampler2D _Splat0,_Splat1,_Splat2,_Splat3;
		sampler2D _Normal0,_Normal1,_Normal2,_Normal3;
		half _Shininess;
		float _Parallax;
		
        struct Input {
			float2 uv_Control : TEXCOORD0;
			float2 uv_Splat0 : TEXCOORD1;
			float2 uv_Splat1 : TEXCOORD2;
			float2 uv_Splat2 : TEXCOORD3;
			float2 uv_Splat3 : TEXCOORD4;
			float3 eye;
		};
        
        void vert (inout appdata_full v, out Input o)
        {
        	o.uv_Control = v.texcoord;
        	o.uv_Splat0 = v.texcoord1;
			o.uv_Splat1 = v.texcoord1;
			o.uv_Splat2 = v.texcoord1;
			o.uv_Splat3 = v.texcoord1;
			
			float4 pos = float4(v.vertex.xyz, 1.0);
			float3x3 world2Tangent;
			
			float3x3 normalMatrix;
			normalMatrix[0] = UNITY_MATRIX_MV[0];
			normalMatrix[0] = UNITY_MATRIX_MV[0];
			normalMatrix[1] = UNITY_MATRIX_MV[1];
			normalMatrix[2] = UNITY_MATRIX_MV[2];
			
			float3 N = v.normal;
			float3 T = float3(1,0,0) - N * dot(N, float3(1,0,0));
			
			v.tangent.xyz = T;
		
			float tangentDir = dot(cross(N, float3(-1,0,0)), float3(0,0,1));
			if(tangentDir <= 0)
				v.tangent.w = -1.0;
			else
				v.tangent.w = 1.0;
		
			float3 B = cross(T, N) * v.tangent.w;
			
			N = normalize(mul(normalMatrix, N));
			T = normalize(mul(normalMatrix, T));
			B = normalize(mul(normalMatrix, B));
		
			world2Tangent = float3x3(T, B, N);
			
			float3 vpos = mul(UNITY_MATRIX_MV, pos).xyz;
			o.eye = mul(world2Tangent, vpos.xyz);
        }
        
		void surf (Input IN, inout SurfaceOutput o) {
			fixed4 splat_control = tex2D (_Control, IN.uv_Control);
			fixed4 col;
			
			// Parallax
			half h;
			float2 offset;
			
			h = tex2D (_Splat0, IN.uv_Splat0).w;
			offset = ParallaxOffset (h, _Parallax, IN.eye);
			IN.uv_Splat0 += offset;
			
			h = tex2D (_Splat1, IN.uv_Splat1).w;
			offset = ParallaxOffset (h, _Parallax, IN.eye);
			IN.uv_Splat1 += offset;
			
			h = tex2D (_Splat2, IN.uv_Splat2).w;
			offset = ParallaxOffset (h, _Parallax, IN.eye);
			IN.uv_Splat2 += offset;
			
			h = tex2D (_Splat3, IN.uv_Splat3).w;
			offset = ParallaxOffset (h, _Parallax, IN.eye);
			IN.uv_Splat3 += offset;
			
			col  = splat_control.r * tex2D (_Splat0, IN.uv_Splat0);
			col += splat_control.g * tex2D (_Splat1, IN.uv_Splat1);
			col += splat_control.b * tex2D (_Splat2, IN.uv_Splat2);
			col += splat_control.a * tex2D (_Splat3, IN.uv_Splat3);
			o.Albedo = col.rgb;
		
			fixed4 nrm;
			nrm  = splat_control.r * tex2D (_Normal0, IN.uv_Splat0);
			nrm += splat_control.g * tex2D (_Normal1, IN.uv_Splat1);
			nrm += splat_control.b * tex2D (_Normal2, IN.uv_Splat2);
			nrm += splat_control.a * tex2D (_Normal3, IN.uv_Splat3);
			
			fixed splatSum = dot(splat_control, fixed4(1,1,1,1));
			fixed4 flatNormal = fixed4(0.5,0.5,1,0.5);
			nrm = lerp(flatNormal, nrm, splatSum);
			o.Normal = UnpackNormal(nrm);
			
			o.Gloss = col.a * splatSum;
			o.Specular = _Shininess;
		
			o.Alpha = 0.0;
		}
		ENDCG
	}
	
	Dependency "AddPassShader" = "Hidden/Nature/Terrain/Bumped Specular AddPass"
	Dependency "BaseMapShader" = "Specular"
	
	Fallback "Nature/Terrain/Diffuse"
}

Any ideas?

That’s funny. You have to answer your own questions. It appeared to me that the parallax diffuse shader wasn’t working, which I posted about a couple of days ago: http://forum.unity3d.com/threads/211211-Parallax-Diffuse-Texturing

I’m glad I found your post. I’ll take a look at it and report back any findings I may have at a later time. I’m sorry I cannot promise anything specific because this is getting queued up with my other problems. Hopefully though I’ll get back to this in a few days.

I totally agree, Unity’s community is always active and answers your questions most of the time but whenever there comes a problem with shaders it’s so quiet and unfortunately devs and experts don’t give any supports usually.

Actually the default parallax diffuse shader works as expected but there are 2 main factors about it:

  • It’s hard to create a good displacement map out of a diffuse/normal without a lot of proper tweakings in the settings whether by CrazyBump or other programs.
  • Unity’s built-in Parallax shader uses a cheap and simple technique to achieve the effect and thus the results are not fine and even not usable most of the times. You have to implement a better technique such as Steep Parallax mapping which is quite heavier too. Also some people believe that as gpus support DX11 nowadays, Parallax effect has been shadowed by DX11’s Tessellation which is a newer technique and removes some existing artifacts as it generates real geometries, However, I’ve never seen such a shader in Unity until now which takes displacement data for tessellation.

About your issue, I’ve heard that all 4 channels of normal maps are reserved in Unity and you can’t add other data in their alpha channel. But you can put displacement data in the alpha channel of the diffuse texture. Attached files in the first post uses this method and I simply did this using Photoshop. Below is the tweaked Parallax Diffuse shader which doesn’t need a Heightmap texture slot and takes the displacement data from diffuse’s alpha channel. Check them with this shader:

Shader "Custom/Parallax Diffuse" {
	Properties {
		_Color ("Main Color", Color) = (1,1,1,1)
		_Parallax ("Height", Range (0.005, 0.08)) = 0.02
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BumpMap ("Normalmap", 2D) = "bump" {}
	}
	
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 500
	
		CGPROGRAM
		#pragma surface surf Lambert
		
		sampler2D _MainTex;
		sampler2D _BumpMap;
		fixed4 _Color;
		float _Parallax;
		
		struct Input {
			float2 uv_MainTex;
			float2 uv_BumpMap;
			float3 viewDir;
		};
		
		void surf (Input IN, inout SurfaceOutput o) {
			half h = tex2D (_MainTex, IN.uv_BumpMap).w;
			float2 offset = ParallaxOffset (h, _Parallax, IN.viewDir);
			IN.uv_MainTex += offset;
			IN.uv_BumpMap += offset;
			
			fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			o.Alpha = c.a;
			o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
		}
		ENDCG
	}
	FallBack "Bumped Diffuse"
}

This works as expected using a simple technique to distort uvs of textures to achieve the illusion but I still got problems implementing viewDir in a terrain shader correctly. Break the silence guys!

Just add a viewDir property in your Input struct and Unity will fill it.

struct Input {
    float3 viewDir:TEXCOORD0
}

…etc, as per the builtin variables listed here if you’re curious —

So just add that to your input struct and use it in your parallax function and happy days.

EDIT: Exactly like the diffuse shader above :stuck_out_tongue:
EDIT: Actually, not sure if you can use the alpha channel of the terrain textures… may need to use separate displacement textures.

Thanks for the answer spraycanmansam, Did you ever check the shader for yourself on a terrain? It’s a little more complicated than this and doesn’t work with default settings.

Yes, I’m aware of the built-in variables and if you look at the first shader I posted, you’ll see that I’ve used viewDir in struct input and temporary filled it in the vert part to compeletly initialize SurfaceOutput o. In the link you mentioned it is stated that:

so there might be a simple solution for the issue I’m facing.

That shader doesn’t have a vert function and as I said you have to temporary fill in all variables from struct input in the vert part. I think the correct way to fill viewDir is to write this line: o.viewDir = WorldSpaceViewDir(v.vertex); as advised in forums.

I’ve already tested it on terrain and it works so i modified the shader and posted here, use the Pavement.zip attachment in the first post for sample textures.

Look at this picture to have better understanding of the issue:

Notice the circular line at the bottom and incorrect distortions around it which finally ruins the parallax effect.

Don’t have to, I’ve already converted Unity’s terrain shader to parallax.

No you dont have to fill it manually. Remove this —

 o.viewDir = WorldSpaceViewDir(v.vertex);

…and it will be populated automatically when it’s compiled. I can’t remember off the top of my head right now, but Unity assigns different values depending on what it needs. The issue above looks like a ‘space’ issue to me… like the shader is expecting viewDir in tangent or object space.

[EDIT]For reference, this is all I have in my vertex program of my parallax terrain shader —

struct Input {
    float2 uv_Splat0 : TEXCOORD1;
    float2 uv_Splat1 : TEXCOORD2;
    float2 uv_Splat2 : TEXCOORD3;
    float2 uv_Splat3 : TEXCOORD4;
    float3 viewDir   : TEXCOORD5;
};

void vert (inout appdata_full v)
{
    v.tangent.xyz = cross(v.normal, float3(0,0,1));
    v.tangent.w = -1;
}

If you really want to do it manually, I would imagine you will need to do something like this in your vertex program —

...

TANGENT_SPACE_ROTATION;
o.viewDir = mul (rotation, ObjSpaceViewDir(v.vertex));

// TANGENT_SPACE_ROTATION is a Unity helper function that calculates below..
// float3 binormal = cross( v.normal, v.tangent.xyz ) * v.tangent.w;
// float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal );

That will get your viewDir into tangent space, which I suspect it needs. But like I said above, none of that should be necessary.

Thanks a lot for the detailed response. To my limited knowledge about shaders, I always thought that shader warnings will ruin the rendering and causes to fail compiling. With your help I’ve prepared a package containing of both terrain and mesh parallax shaders and a sample scene for anyone who’s also looking for it. As already said it uses Unity’s default parallax method and shaders are tweaked to use displacement data from the alpha channel of the diffuse texture. You will get some warnings but it’s ok.

Simple Parallax Terrain: https://www.dropbox.com/s/k2rk0okhrwbboe3/ParallaxTerrain.unitypackage

Actually, we could write a custom terrain shader which uses steep parallax mapping technique plus some extra features but unfortunately even using spraycanmansam’s solution, there are still viewing issues. Anyway, here are some screenshots of what we have done so far:

Terrain Shader Features:

  • 8192 resolution colormap (2x2 grid of 4 Satellite Images in 4096 resolution)
  • Unlimited detail textures painting on terrain all with normalmaps and heightmaps
  • Blending between colormap layer and detail textures based on specified distance and blending amount
  • Blending between far viewing and close up normalmaps
  • Steep Parallax Mapping with amount and quality modifiers
  • POM self shadowing with amount and quality modifiers
  • Lighten/Darken Scattering for more natural visuals on detail textures
  • Global tileable Normalmap for boosting far viewing distance details
  • Terrain Specular Shininess
  • Brightness modifier for the whole terrain rendering
  • More realistic rendering of images textures colors
  • Procedural Snow layer with slope detection and starting height option
  • Snow Fader for smooth gradient transition density
  • Satellite Image Correction to add snow to detected snow parts in satellite images
  • Normals Strength modifier

This shader will be included in our TerraLand package as soon as the version 2 comes out.

1 Like

Nice looking screenshots. More terrain assets/shaders are always welcome!

Thanks jc :slight_smile:

Happy to help.
Results look good :slight_smile:

Thank you again spray, will let you know about the updates.

Is it me or it’s not just what it’s meant to be the Parallax Effect man!

I try to use your shader but I had this error: Shader warning in ‘Custom/Simple Terrain Parallax’: Program ‘vert_surf’, ‘vert’: output parameter ‘o’ not completely initialized (compiling for d3d11) at line 47

Could you help?

Don’t worry about the warnings you get, the shaders will compile fine. Check out the prepared package from this link:

Simple Parallax Terrain: https://www.dropbox.com/s/k2rk0okhrwbboe3/ParallaxTerrain.unitypackage

Well am sorry to tell you this but it doesn’t works for me :frowning:

Is “Use Direct3D 11” enabled in Unity’s Player Settings? if that is so the terrain shader won’t get the Parallax effect. As said before these shaders are so simple using Unity’s internal Parallax technique and there is no reason for them not to work.

Simply play with “Height” slider in the 2 materials and see if there are any effects!

Hey there TerraUnity,

I downloaded your shader package, put it in a brand new project and got this:

I’m using Unity 4.5.1 (pro), is the shader not compatible with this version?

It will run ok in any Unity version,

Just make sure that Unity is not in Direct3D 11 mode by going to Edit => Project Settings => Player and uncheck “Use Direct3D 11” Simply add this line at the beginning of the vert function:

UNITY_INITIALIZE_OUTPUT(Input, o);

and then find the shader “ParallaxTerrain” in Shaders folder in project, right click on it and select “Reimport”. Now the terrain is back with this cheap parallax effect :slight_smile: