Search Unity

Free Terrain Shader that adds AO + Heights / parallax

Discussion in 'Shaders' started by a436t4ataf, Dec 20, 2019.

  1. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    For some testing, I wanted Unity standard terrains with a shader that supported the same things Unity Standard Shader has supported since 5.x (but which Unity strangely still hasn't fixed in their default shader??).

    I found this old post from 5 years ago which included some attempts to fix the broken default terrain shader: https://forum.unity.com/threads/so-i-created-this-pbr-terrain-shader.281658/#post-1878021

    ...and updated it to include AO.

    This is a quick-and-dirty update, it doesn't match the modern (2020) Unity Standard Shader (I had a look at the standard shader sources, and couldn't see a quick easy way to add-in the missing features, so went with this partially correct approach). If anyone cares enough, I'd delve into the current StandardTerrainShader and try fixing it, but I expect you're using the (free) MicroSplat if you care that much ;).

    Key features:
    1. Uses the pre-existing forums implementation: Albedo + Normal + Heights/parallax
    2. Adds Normal-Scale from the 2017-onwards Unity TerrainShader (missing in original forums one)
    3. Adds AO
    4. Adds a proprietary "intensity" for AO
    Requires:
    • In Unity Terrain Inspector, your layer needs to have:
    • ...in Albedo slot:
      • RGB = Albedo
      • A = Smoothness
    • ...in Normal slot:
      • RG = Normal (note: I build the Normal's 3rd coord from first two using normalize)
      • B = AO
      • A = Heights
    I also wrote a quick-and-dirty Editor script to combine textures into channels, because doing it in Affinity is too slow and painful (and the official approach requires watching an 8 minute long video (!) just to do this thing that takes 3 seconds in Photoshop :)).

    Code (CSharp):
    1. Shader "Nature/Terrain/Terrain PBR_Parallax"
    2. {
    3.     Properties
    4.     {
    5.         [HideInInspector] _Control ("Control (RGBA)", 2D) = "red" {}
    6.         [HideInInspector] _Splat3 ("Layer 3 (A)", 2D) = "white" {}
    7.         [HideInInspector] _Splat2 ("Layer 2 (B)", 2D) = "white" {}
    8.         [HideInInspector] _Splat1 ("Layer 1 (G)", 2D) = "white" {}
    9.         [HideInInspector] _Splat0 ("Layer 0 (R)", 2D) = "white" {}
    10.         [HideInInspector] _Normal3 ("Normal 3 (A)", 2D) = "bump" {}
    11.         [HideInInspector] _Normal2 ("Normal 2 (B)", 2D) = "bump" {}
    12.         [HideInInspector] _Normal1 ("Normal 1 (G)", 2D) = "bump" {}
    13.         [HideInInspector] _Normal0 ("Normal 0 (R)", 2D) = "bump" {}
    14.         [HideInInspector] _NormalScale3 ("NormalScale 3 (R)", Float) = 1.0
    15.         [HideInInspector] _NormalScale2 ("NormalScale 2 (R)", Float) = 1.0
    16.         [HideInInspector] _NormalScale1 ("NormalScale 1 (R)", Float) = 1.0
    17.         [HideInInspector] _NormalScale0 ("NormalScale 0 (R)", Float) = 1.0
    18.         [HideInInspector] _MainTex ("BaseMap (RGB)", 2D) = "white" {}
    19.         [HideInInspector] _Color ("Main Color", Color) = (1,1,1,1)
    20.         _AOStrength( "AO Strength", Float) = 1.0
    21.         _BaseColormap ("Colormap (RGB) Smoothness (A)", 2D) = "white" {}
    22.         _BaseNormalMap ("Normalmap (RGB)", 2D) = "white" {}
    23.         _BasemapDistance ("BaseMap Distance", Float) = 1000
    24.         _FadeLength ("Fade Lenght", Float) = 100
    25.         _Parallax ("Height", Range (0.005, 0.08)) = 0.02
    26.     }
    27.    
    28.     SubShader
    29.     {
    30.         Tags
    31.         {
    32.             "SplatCount" = "4"
    33.             "Queue" = "Geometry-100"
    34.             "RenderType" = "Opaque"
    35.         }
    36.        
    37.         CGINCLUDE
    38.         #define _GLOSSYENV 1
    39.         #define UNITY_SETUP_BRDF_INPUT SpecularSetup
    40.         ENDCG
    41.        
    42.         CGPROGRAM
    43.         #pragma target 3.0
    44.         #include "UnityPBSLighting.cginc"
    45.         #pragma surface surf StandardSpecular vertex:vert
    46.         #pragma exclude_renderers gles
    47.  
    48.         struct Input
    49.         {
    50.             float2 uv_Control;
    51.             float2 uv_Splat0;
    52.             float2 uv_Splat1;
    53.             float2 uv_Splat2;
    54.             float2 uv_Splat3;
    55.             float3 viewDir;
    56.             float3 worldPos;
    57.            
    58.         };
    59.  
    60.         void vert (inout appdata_full v)
    61.         {
    62.             v.tangent.xyz = cross(v.normal, float3(0,0,1));
    63.             v.tangent.w = -1;
    64.         }
    65.  
    66.         sampler2D _BaseColormap, _BaseNormalMap;
    67.         sampler2D _Control;
    68.         sampler2D _Splat0,_Splat1,_Splat2,_Splat3;
    69.         sampler2D _Normal0,_Normal1,_Normal2,_Normal3;
    70.         // t-machine support Unity scaleable normals
    71.         float _NormalScale0, _NormalScale1, _NormalScale2, _NormalScale3;
    72.         // t-machine custom feature: AO-strength
    73.         float _AOStrength;
    74.        
    75.         float _BasemapDistance, _FadeLength;
    76.         float _Parallax;
    77.  
    78.         void surf (Input IN, inout SurfaceOutputStandardSpecular o)
    79.         {
    80.                     // Base maps
    81.             fixed4 splat_control = tex2D(_Control, IN.uv_Control);
    82.        
    83.                     // Normal
    84.             fixed4 splatNormal;
    85.             fixed4 inSplatNormal;
    86.             inSplatNormal  = splat_control.r * tex2D(_Normal0, IN.uv_Splat0);
    87.             inSplatNormal += splat_control.g * tex2D(_Normal1, IN.uv_Splat1);
    88.             inSplatNormal += splat_control.b * tex2D(_Normal2, IN.uv_Splat2);
    89.             inSplatNormal += splat_control.a * tex2D(_Normal3, IN.uv_Splat3);
    90.            
    91.             float normalScale;
    92.             normalScale = splat_control.r * _NormalScale0;
    93.             normalScale += splat_control.g * _NormalScale1;
    94.             normalScale += splat_control.b * _NormalScale2;
    95.             normalScale += splat_control.a * _NormalScale3;
    96.            
    97.             // t-machine: normalize from RG, use B for AO
    98.             fixed3 preNormalized = 1;
    99.             preNormalized.xy = inSplatNormal.xy;
    100.             splatNormal.xyz = normalize( preNormalized ) * normalScale;
    101.             float splatAO = inSplatNormal.z;
    102.            
    103.             // original: use A for Heights
    104.             half h = inSplatNormal.a;
    105.             float2 offset = ParallaxOffset (h, _Parallax, IN.viewDir);
    106.             IN.uv_Splat0 += offset;
    107.             IN.uv_Splat1 += offset;
    108.             IN.uv_Splat2 += offset;
    109.             IN.uv_Splat3 += offset;
    110.  
    111.        
    112.             float d = distance(IN.worldPos.xyz, _WorldSpaceCameraPos.xyz);
    113.             float fadeout = saturate((_BasemapDistance - d ) / _FadeLength);
    114.            
    115.  
    116.  
    117.             fixed4 baseColor = tex2D(_BaseColormap, IN.uv_Control);
    118.             // t-machine: normalize from RG, and use B for AO
    119.             fixed3 baseNormal = 0;
    120.             fixed3 incomingNormal = 1;
    121.             incomingNormal.xy = tex2D(_BaseNormalMap, IN.uv_Control).xy;
    122.             baseNormal.xyz = normalize( incomingNormal );
    123.            
    124.             // t-machine: add AO
    125.             float baseAO = tex2D(_BaseNormalMap, IN.uv_Control).z;
    126.             baseColor *= pow( baseAO, _AOStrength );
    127.            
    128.             //fixed baseMetalness = baseColor.a;
    129.             fixed baseMetalness = float3(0,0,0);
    130.             fixed baseSmoothness = baseColor.a;
    131.             fixed3 baseSpecular = lerp(float3(0.04,0.04,0.04), baseColor.rgb, baseMetalness);
    132.                        
    133.             // Albedo
    134.             fixed4 splatColor;
    135.             splatColor  = splat_control.r * tex2D (_Splat0, IN.uv_Splat0);
    136.             splatColor += splat_control.g * tex2D (_Splat1, IN.uv_Splat1);
    137.             splatColor += splat_control.b * tex2D (_Splat2, IN.uv_Splat2);
    138.             splatColor += splat_control.a * tex2D (_Splat3, IN.uv_Splat3);
    139.            
    140.             // t-machine: add AO
    141.             splatColor *= pow( splatAO, _AOStrength );
    142.            
    143.             // Specular
    144.             //float splatMetalness = min(splatColor.a, 1.0);
    145.             //float splatSmoothness = min(splatNormal.a, 1.0);
    146.             float splatSmoothness = min(splatColor.a, 1.0);
    147.             //fixed3 splatSpecular = lerp(float3(0.04, 0.04, 0.04), splatColor.rgb, splatMetalness);
    148.             fixed3 splatSpecular = float3(0.04, 0.04, 0.04);
    149.             //splatColor.rgb = lerp(float3(0.0, 0.0, 0.0), splatColor.rgb, 1.0-splatMetalness);
    150.             splatColor.rgb = lerp(float3(0.0, 0.0, 0.0), splatColor.rgb, 1.0);
    151.             //baseColor.rgb = lerp(float3(0.0, 0.0, 0.0), baseColor.rgb, 1.0-baseMetalness);
    152.  
    153.             // Lerp to flat normal
    154.             fixed splatSum = dot(splat_control, fixed4(1,1,1,1));
    155.             fixed4 flatNormal = fixed4(0.5,0.5,1,0.5);
    156.            
    157.             // Lerp albedo to black for metal surfaces
    158.             splatNormal.rgb = lerp(flatNormal, splatNormal.rgb, splatSum);
    159.             baseNormal.rgb = lerp(flatNormal, baseNormal.rgb, splatSum);
    160.            
    161.             o.Albedo = lerp(baseColor.rgb*splatSum, splatColor.rgb, fadeout);
    162.            
    163.             o.Normal = lerp(baseNormal.rgb * 2 - 1, splatNormal.rgb * 2 - 1, fadeout);
    164.             o.Specular = lerp(baseSpecular.rgb*splatSum, splatSpecular.rgb, fadeout);
    165.             o.Smoothness = lerp(baseSmoothness*splatSum, splatSmoothness, fadeout);
    166.         }
    167.         ENDCG
    168.     }
    169.    
    170.     Dependency "AddPassShader" = "Hidden/Nature/Terrain/Terrain PBR-AddPass_Parallax"
    171.     Dependency "BaseMapShader" = "Hidden/Nature/Terrain/Terrain PBR-Far_Parallax"
    172.     Dependency "Details0"      = "Hidden/TerrainEngine/Details/Vertexlit"
    173.     Dependency "Details1"      = "Hidden/TerrainEngine/Details/WavingDoublePass"
    174.     Dependency "Details2"      = "Hidden/TerrainEngine/Details/BillboardWavingDoublePass"
    175.     Dependency "Tree0"         = "Hidden/TerrainEngine/BillboardTree"
    176.  
    177.     FallBack "Nature/Terrain/Diffuse"
    178. }
    179.  
    And the custom texture-channel merger if you want to do it all in Unity and avoid using PS:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using System.IO;
    4. using UnityEditor;
    5. using UnityEngine;
    6.  
    7. public enum TextureSelector
    8. {
    9.     TEXTURE_A,
    10.     TEXTURE_B,
    11.     TEXTURE_C
    12. }
    13. public enum ChannelSelector
    14. {
    15.     R,G,B,A
    16. }
    17.  
    18. public enum Operation
    19. {
    20.     NONE,
    21.     ONE_MINUS
    22. }
    23.  
    24. [System.Serializable]
    25. public struct TextureChannels
    26. {
    27.     public TextureSelector texture;
    28.     public ChannelSelector channel;
    29.     public Operation transform;
    30.  
    31.     public TextureChannels( TextureSelector ts, ChannelSelector cs, Operation op )
    32.     {
    33.         texture = ts;
    34.         channel = cs;
    35.         transform = op;
    36.     }
    37. }
    38.  
    39. public class IntelligentTextureMerger : MonoBehaviour
    40. {
    41.     public Texture2D inputTexture1;
    42.     public Texture2D inputTexture2;
    43.     public Texture2D inputTextureC;
    44.  
    45.     public TextureChannels outputR, outputG, outputB, outputA;
    46.  
    47.     public string outputBase = "";
    48.     public string outputSuffix = "_NEW";
    49.     public string outputFilename;
    50.     public string outputFullPath;
    51.  
    52.     [ContextMenu( "UpdateOutputPath" )]
    53.     public void UpdateOutputPath()
    54.     {
    55.         outputFilename = (outputBase.Length < 1 ? inputTexture1.name : outputBase) + outputSuffix + ".png";
    56.        
    57.         outputFullPath =  Path.GetDirectoryName(AssetDatabase.GetAssetPath( inputTexture1 ) )+ "/" + outputFilename;
    58.     }
    59.  
    60.     public Color[] WorkaroundUnityAPIBugs_GetPixelsForTexture( Texture2D t )
    61.     {
    62.         var aImporter = AssetImporter.GetAtPath( AssetDatabase.GetAssetPath(t) ) as TextureImporter;
    63.         {
    64.             aImporter.textureType = TextureImporterType.Advanced;
    65.  
    66.             aImporter.isReadable = true;
    67.  
    68.             AssetDatabase.ImportAsset( AssetDatabase.GetAssetPath(t) );
    69.             AssetDatabase.Refresh();
    70.         }
    71.         return t.GetPixels(0,0, t.width, t.height);
    72.     }
    73.    
    74.     [ContextMenu( "Merge" )]
    75.     public void Merge()
    76.     {
    77.         UpdateOutputPath();
    78.  
    79.         Color[] aSource = WorkaroundUnityAPIBugs_GetPixelsForTexture( inputTexture1 );
    80.         Color[] bSource = WorkaroundUnityAPIBugs_GetPixelsForTexture(inputTexture2);
    81.         Color[] cSource = inputTextureC == null ? new Color[0] : WorkaroundUnityAPIBugs_GetPixelsForTexture(inputTextureC);
    82.  
    83.         Color[] output = new Color[Mathf.Max( aSource.Length, bSource.Length )];
    84.         for( int i = 0; i < output.Length; i++ )
    85.         {
    86.             Color a = aSource.Length > i ? aSource[i] : Color.black;
    87.             Color b = bSource.Length > i ? bSource[i] : Color.black;
    88.             Color c = cSource.Length > i ? cSource[i] : Color.black;
    89.            
    90.            
    91.             output[i] = new Color( MergeChannelFrom(a,b,c,outputR),MergeChannelFrom(a,b,c,outputG),MergeChannelFrom(a,b,c,outputB), MergeChannelFrom(a,b,c,outputA) );
    92.         }
    93.        
    94.         Texture2D outputTexture = new Texture2D( inputTexture1.width, inputTexture1.height );
    95.         outputTexture.SetPixels( output );
    96.         byte[] bytes = outputTexture.EncodeToPNG();
    97.         File.WriteAllBytes(outputFullPath, bytes);
    98.         AssetDatabase.Refresh();
    99.     }
    100.  
    101.     public float MergeChannelFrom( Color A, Color B, Color C, TextureChannels chooseFrom )
    102.     {
    103.         Color src;
    104.         switch( chooseFrom.texture )
    105.         {
    106.             case TextureSelector.TEXTURE_A:
    107.                 src = A;
    108.                 break;
    109.             case TextureSelector.TEXTURE_B:
    110.                 src = B;
    111.                 break;
    112.             case TextureSelector.TEXTURE_C:
    113.                 src = C;
    114.                 break;
    115.             default:
    116.                 return -1f;
    117.         }
    118.  
    119.         float c;
    120.         switch( chooseFrom.channel )
    121.         {
    122.             case ChannelSelector.A:
    123.                 c = src.a;
    124.                 break;
    125.             case ChannelSelector.B:
    126.                 c = src.b;
    127.                 break;
    128.             case ChannelSelector.G:
    129.                 c = src.g;
    130.                 break;
    131.             case ChannelSelector.R:
    132.                 c = src.r;
    133.                 break;
    134.             default:
    135.                 c = -1f;
    136.                 break;
    137.         }
    138.  
    139.         switch( chooseFrom.transform )
    140.         {
    141.             case Operation.NONE:
    142.                 return c;
    143.             case Operation.ONE_MINUS:
    144.                 return 1f - c;
    145.             default:
    146.                 return -1f;
    147.         }
    148.     }
    149. }
    150.  
     
    Fibonaccov likes this.
  2. jbooth

    jbooth

    Joined:
    Jan 6, 2014
    Posts:
    5,461
    You'll get higher quality if you pack normals into G/A. Texture compression compresses the A channel separately from the RGB channels, and since the RGB components are compressed based on how the eye sees, they tend to favor the green channel since our eyes see green more than other colors- additionally some block selectors use 565 bits for the color component, so you have extra bits in Green. And since normals tend to have the most effect on quality, using G/A for normals, and packing smoothness/AO in the remaining channels, is the best option.
     
    a436t4ataf likes this.
  3. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    And with the quick texture combiner above, it only takes a few seconds to re-combine your textures no matter which channels they were originally saved / authored in. So there's no excuse not to :).

    But I still think most people probably want a more feature-complete solution for terrain shading - this is just a stopgap.

    By the way - is there a public API reference (or similar) for *-splat? One of my reasons for using the solution above is that I'm testing custom terrain manipulation tools, and I wanted to start with Unity's default interpretation of textures, and then test that my code was correctly re interpreting (*) different incoming texture definitions (and having the source above made that easy). i.e. from a scripting perspective, is there a reference for how *splat differs from old Unity Terrain (I know the headline stuff as a user, e.g. better texturing etc, but I'm not sure what out-of-band / extra info *splat needs/expects - if any! - if I'm manipulating the terrain data underneath it)

    (*) - to be clear: for instance, programmatically modifying the texture data in the splats (wetness, for instance). Or, more tricky, knowing which data is heights (because I do stuff on CPU in C# land with that data, interacting with scripted behaviours).