Cracked Ice Shader (using Parallax Effect)

Final effect of the cracked ice shader.

Over the holidays I’ve decided to learn a little about parallax effects, especially how to use the effect in shaders and effects; I’ve begun my research by looking at what other people have done and trying to figure out myself how this can be done. I’ve come across a nice tutorial by Binary Impact in which a similar effect is shown using Unity’s Shader Graph (link).

Binary Impact shows how to create an effect inspired by Ali Youssef’s UE4 ice shader, while for my version I wanted to create something that looks more similar to what can be seen in Dragon Age: Inquisition where you can see different layers of cracks underneath the ice surface.

Left: Ali Youssef’s cracked ice shader | Right: Dragon Age: Inqusition, cracked ice in Emprise du Lion

On the left you can see the original shader by Ali Youssef, while the shader on the right is Dragon Age: Inquisition’s ice, the main difference is in the cracks themselves; on the left you can only see one layer of cracks which repeats itself to create depth as if the crack is depp in the ice.

Compared to the right, where you can see multiple layers of cracks; you can see the top layer which is on the surface and then additional layers underneath the surface. This is the effect I wanted to recreate.

So, I’ve begun experimenting, I started with Shader Graph this way I could experiment with the effect more easily and at the time I didn’t know how to write it myself. As the base for my shader I used Binary Impact’s shader graph tutorial.

The first difference between my variant and Binary Impact’s shader is the amount of layers, in the original the amount needs to be high in order to recreate the depth effect, while in my version I don’t need as many so I have decided to settle on just three.

For now I will show shader graph screenshots, I will share the code later on in this post.


Shader Graph Changes

image showing three layers of cracks combined together
In this variant, I will only be using three layers to create the depth effect. Without the ADD node at the top left corner, the first layer would show on the surface.

The first layer would be displayed on the surface of the object, for this reason I add offset to the first layer this way it actually appears underneath the surface and the object’s surface is affected by the diffuse texture and the normal texture.

The next difference comes in the parallax sub-graph itself, I use the normal texture from the shader to distort the lines; this in the end makes them look more realistic as if they are distorted by the rough surface of the ice. It’s a simple effect, but adds a lot to the final version.

image showing normal texture altering UVs of the cracks texture
In this version, I use the normal texture to offset the UVs of the cracks texture.

The next change is quite small, but I wanted to be able to change the colours of the ice; for this to work I had to make the diffuse texture black and white (black areas are shallow and white are deeper); this was quickly done in Photoshop and resulted in this texture:

Diffuse texture used in shader, other textures come from Binary Impact’s tutorial.

That was it for the changes of Binary Impact’s shader, I’ve adjusted the offset value and player around with the textures and I was able to come up with the final version of the shader; the cracks texture I’m using is a packed texture (packing different grayscale textures into RGBA channels of one texture) – more on packed textures on Polycount.

Using the RGB channels to pack different crack textures.

Unity Shader Code

So, the next part was actually figuring out how to do this all but in shader code. The first issue was the parallax effect itself. I’ve never done anything like this in shader code and to make matters even worse this shader uses tangent space view direction.

Luckily I was able to find a tutorial by Harry Alisavakis (link) in which he describes how he went around creating the parallax effect in shader code; Harry also uses Binary Impact’s tutorial as the reference for the parallax effect.

I won’t copy what Harry said in the blog post, go and read it, but in order to achieve the view direction in tangent space we need to use this code:

float4 objCam = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1.0));
float3 viewDir = v.vertex.xyz - objCam.xyz;
float tangentSign = v.tangent.w * unity_WorldTransformParams.w;
float3 bitangent = cross(v.normal.xyz, v.tangent.xyz) * tangentSign;
o.viewDirTangent = float3(
    dot(viewDir, v.tangent.xyz),
    dot(viewDir, bitangent.xyz),
    dot(viewDir, v.normal.xyz));

In short, we’re calculating the view direction and the bitangent and we’re multiplying our view direction by the tangent, bitangent and normal values; this gives us the desired effect. Once again, Harry’s version recreates a similar effect to the one seen on Binary Impact’s video, so we will need to make some adjustments; here is the final code, and I will walk you through it:

Shader "Custom/CrackedIce"
{
    Properties
    {
        _IceTint ("Ice Texture Tint", Color) = (1,1,1,1)
        _MainTex ("Ice Albedo (RGB)", 2D) = "white" {}
        _CrackLayers("Packed Cracks Texture", 2D) = "white" {}
        _OffsetScale("Crack Offset Scale", float) = 0.5
        _CracksStrength("Cracks Fade Strength", vector) = (0.0, .75, .45, .25)
        _NormalTex ("Ice Normal Texture", 2D) = "bump" {}
        _Roughness ("Ice Roughness Texture", 2D) = "black" {}
        _RoughnessStrength ("Roughness Strength", Range(0.0, 1.0)) = 0.4
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
 
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows vertex:vert
        #pragma target 3.5
 
        sampler2D _MainTex;
        sampler2D _NormalTex;
        sampler2D _CrackLayers;
        sampler2D _Roughness;
 
        half _OffsetScale;
        half _RoughnessStrength;
        half4 _CracksStrength;
 
        half _Metallic;
        fixed4 _IceTint;
 
        struct Input
        {
            float2 uv_MainTex;
            float2 uv_NormalTex;
            float2 uv_CrackLayers;
            float2 uv_Roughness;
 
            float3 viewDirTangent;
        };
 
        // From Unity's shader graph manual.
        float4 blendMultiply (float4 baseTex, float4 blendTex, float opacity)
        {
            float4 baseBlend = baseTex * blendTex;
            float4 ret = lerp(baseTex, baseBlend, opacity);
            return ret;
        }
 
        void vert (inout appdata_full v, out Input o)
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);
 
            // [VIEW DIRECTION IN TANGENT SPACE] : In order for parallax to work correctly, we need to find
            //  view direction of the camera in tangent space. Calculation below takes care of that.
            // Credit: Harry Alisavakis
            float4 objCam = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1.0));
            float3 viewDir = v.vertex.xyz - objCam.xyz;
            float tangentSign = v.tangent.w * unity_WorldTransformParams.w;
            float3 bitangent = cross(v.normal.xyz, v.tangent.xyz) * tangentSign;
            o.viewDirTangent = float3(
                dot(viewDir, v.tangent.xyz),
                dot(viewDir, bitangent.xyz),
                dot(viewDir, v.normal.xyz)
            );
        }
 
        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 mainTex = tex2D(_MainTex, IN.uv_MainTex) * _IceTint;
            fixed3 normalTex = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
            fixed rougnessTex = tex2D(_Roughness, IN.uv_Roughness).r * _RoughnessStrength;
 
            fixed parallax = 0;
            for (int j = 0; j < 4; j ++) {
                float ratio = (float) j / 4;
 
                if (j == 0)
                {
                    // I don't want to show the first layer, because this would be flat on the object (no depth),
                    //   I want to start with the second iteration of the parallax effect, which will have depth.
                }
                else if (j == 1)
                {
                    // First layer of cracks.
                    parallax += tex2D(_CrackLayers, IN.uv_CrackLayers + lerp(0, _OffsetScale, ratio) * normalize(IN.viewDirTangent) + normalTex).g * _CracksStrength.y;
                }
                else if (j == 2)
                {
                    // Second layer of cracks.
                    parallax += tex2D(_CrackLayers, IN.uv_CrackLayers + lerp(0, _OffsetScale, ratio) * normalize(IN.viewDirTangent) + normalTex).b * _CracksStrength.z;
                }
                else if (j == 3)
                {
                    // Third layer of cracks.
                    parallax += tex2D(_CrackLayers, IN.uv_CrackLayers + lerp(0, _OffsetScale, ratio) * normalize(IN.viewDirTangent) + normalTex).r * _CracksStrength.w;
                }
            }
            parallax *= 1.5;
 
            fixed4 blended = blendMultiply(mainTex, parallax, 0.55);
 
            o.Albedo = blended;
            o.Normal = normalTex;
            o.Metallic = _Metallic;
            o.Smoothness = 1 - rougnessTex;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

At the top, just like in every shader, I declare my properties; remember that the crack layers is a packed texture so I have access to three textures from one file. On line 46 I have a custom blend texture, this is taken from Unity’s shader graph code but I will talk more about this later. On line 57 you can see the code for tangent view direction which I’ve already described.

In the surface function I sample the main diffuse texture first, this is the black and white texture which I then multiply by the tint color; next I sample the normal texture, remember to unpack it, and a roughness texture which only needs one channel.

Next is a loop, I know it looks ugly but that is the only way I could figure out how to recreate this effect; if you remember earlier I described how we needed less layers and I’ve shown a shader graph screenshot with only three layers? Well, this is the equivalent of that.

On line 81 I discard the first layer (do nothing), as this would be displayed on the surface, but then on line 87 I create the first layer of cracks. Just like in the shader graph version, I sample the texture and I use the textures UVs but I need to use the lerp() function with normalized tangent view direction in order to properly calculate the offset for the parallax effect.

Finally at the end of the line I add the normal texture to the UVs, this creates the distortion effect, and finally I sample just the green channel to get one texture out of the packed texture; additionally I multiply that by a value that I show in inspector as this allows me to change the strength of the layer and make it more/less visible.

I then do it for the second and third iterations, using the blue and red channels; each iteration is added to the parallax value which is also like a grayscale image and that’s why it is a fixed instead of fixed4.

Next on line 102 I make the cracks a little stronger, I’ve noticed that if I leave them as they are they tend to be hard to see even with the strength values set to 1

Next, Binary Impact uses a blend node to blend between the diffuse texture and the cracks; I wasn’t sure how to do it, so I looked up the shader code used in Unity to make sure I get the same result. Finally I apply the albedo, normal, metallic and smoothness values in the surface output. It’s worth pointing out that I invert the roughness texture because Unity uses white color as flat while the texture is the reverse.

Final effect using the shader above.

Epilogue

That’s it! Well done, and thank you for reading through this blog post, I hope that you found this shader useful; let me know if you have any questions and I’ll be able to help. It was quite fun recreating this effect, I’ve used the same textures as Binary Impact because it seems that its quite hard to find good cracked ice textures (lol).

As always, you can find the Unity project over at my shader GitHub repo.

Stick around for some more shader magic coming in future!


logo-black-transparent

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Back to top