Interactive snow shader

Made by Jeany de Vries

Foreword

Horizon Zero Dawn] [Screenshot] The snow physics are such an awesome little  touch. - Imgur
Figure 1: Snow in horizon zero dawn (The snow physics are such an awesome little touch., 17–11-21).

Have you ever been in a snow area in a game where the footsteps are printed in the snow. Did it feel satisfying? For me it did. That is why I wanted to make the same.

I chose this for the assignment because I always had some interest in creating custom shaders using HLSL. This was the perfect opportunity for this!

I made an interactive snow shader where there are trails left behind for every mesh. Also after some while the snow goes back to the normal state when it is snowing. Plus you can walk around and the snow will stay correctly the same. After you go out of the range it will create a new render texture because of memory reasons.

At first I took a different approach. I worked more in C# then in shader code, but the trouble began with performance. Everything was processed in the CPU, which cannot handle that much. So after some thinking I did an approach with working more in the GPU, which can handle a lot more. See more about this later on.

Table of contents

  1. Tesselation
    1. Fixed tesselation
    2. Distance based tesselation
    3. Edge based tesselation
    4. Phong based tesselation
  2. Render texture
    1. Set UV
    2. Set depth
  3. Making it interactive
    1. First attempt
    2. Orthographic camera
    3. Material
    4. Opacity and blending
  4. Keep it moving
  5. Snowing
  6. Finished product
  7. Conclusion
  8. Future references
  9. Sources

1. Tesselation

Figure 2: Tesselation (Flick, z.d.)

Tesselation is an important part in making a snow shader. This is because a snow shader manipulates the vertices to creates depth in a plane. Normally a plane has 4 vertices. This will not do when creating depth, it does not have enough vertices. That is why tesselation is needed. Tesselation increases the amount of vertices of a mesh (in this case a plane). Tesselation basically subdivides patches. This allows us to replace a single triangle with a collection of smaller triangles. With this we can increase it to manipulate more vertices so we can create a nicer feel of depth.

There are multiple sorts of tesselation (Unity(21–03-22c)). I will discuss all of them and describe why I chose the ones I have and do not have.

1.1 Fixed tesselation

Figure 3: Fixed tesselation (Unity, 2021)

Fixed tesselation is quite simple. To add it to the shader you have to add the method to the #pragma. After that call the method plus the variable for the tesselation amount. Thankfully the method does all the work.

“The tessFixed tessellation function returns four tessellation factors as a single float4 value: three factors for each edge of the triangle, and one factor for the inside of the triangle.” (Unity(21–03-22c))

I used this at the beginning when I simply had a plane, but when I added a mountain model it hit the performance hard. It made more vertices for the whole mesh which takes a lot of performance. So for simple meshes this can be used, but this is not a smart choice performance wise.

This is what it will look like in HLSL:

tessellate:tessFixed
float _Tess;
        float4 tessFixed()
        {
            return _Tess;
        }

1.2 Distance based tesselation

Figure 4: Distance based tesselation (Unity, 2021)

This function looks at the distance between the camera and the vertices. If it is further away than the tesselation will be lower ~ like how level of detail works.

I used this eventually in my project because this is great for the performance when using big meshes. Then the size does not matter, because the tesselation will be set super low when it is far away.

Here is also works the same. Add the method in the pragma and add the minimal distance, maximal distance and tesselation variables into the method. After that the method will do the work.

This function computes the distance of each vertex to the camera and derives the final tessellation factors.” (Unity(21–03-22c))

tessellate:tessDistance
float _Phong;
float _MinDist;
float _MaxDist;
float _Tess;
 
float4 tessDistance (appdata v0, appdata v1, appdata v2) {
    return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, _MinDist, _MaxDist, _Tess);
}

1.3 Edge based tesselation

Figure 5: Edge based tesselation (Unity, 2021)

Edge based is a handy method when all the triangles are at the same size. For me this was not the case. I had different depths and eventually a mesh with mountains etc.

Add the method in the pragma and only add the wanted edge length in the method.

For performance reasons, you can call the UnityEdgeLengthBasedTessCull function instead, which performs patch frustum culling. This makes the shader a bit more expensive, but saves a lot of GPU work for parts of meshes that are outside of the Camera’s view. (Unity(21–03-22c))

tessellate:tessEdge
float _EdgeLength;
 
        float4 tessEdge(appdata v0, appdata v1, appdata v2)
        {
            return UnityEdgeLengthBasedTess(v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
        }

1.4 Phong based tesselation

Figure 6: Phong based tesselation (Unity, 2021)

The phong tesselation is a bit different than the rest. It still adds triangles except the logic is different. The phong tesselation is perfect for rounding the edges in meshes, see figure 6.

The question is: Why did I add this to my project? Well I wanted the snow to look realistic so I thought it would be nice to smooth the edges where the plane goes deeper. It does cost more performance so I still wonder if this was the right choice but I still don’t have fps issues so for now it is fine.

The nice thing is that there is no need for a method. You only need to add the _Phong to the pragma and have a custom float with the same now from the inspector so you can change it. The surface shader will do the rest.

tessphong:_Phong
float _Phong;

2. Render texture

Render textures are a texture that can be updated during runtime (Unity(21–03-22b)). Then the texture can be put on the camera as the target texture.

I used render textures to create a splat map that outputs a red color to create depth in the scene. The shader will then read the texture and checks the color. See more information below.

2.1 Set UV

Figure 7: UV correction (MinionsArt, 19–01-25)

Before we can start with the depth, we need to set the uv correctly. The uv has a different sort of position and scale. It goes from 0 to 1, from left to right and bottom to top. To begin I first set the position correctly.. I set the position based on the distance on the xz plane and the camera. Then to set the scale correctly I divided it with the camera size. Because I used an orthographic camera I had to do times 2, because the width and height system is a bit different. After that fix the offset by just adding 0.5. To see the whole process, see figure 7.

This is what it looks like in HLSL:

// calculates UV based on distance on XZ plane to orthographic camera. 
            float2 uv = IN.worldPos.xz - _CameraPosition.xz;
            //Set the correct scale 
            uv = uv / (_OrthographicCamSize * 2);
            //Add an offset so the position is correct
            uv += 0.5;

2.2 Set depth

Figure 8: Depth with red value render texture (self made)

As I said above the depth is calculated with the red value in the render texture. I first made a simple texture with a red smiley via photoshop and added this to the snow shader, with the code below. The results are shown in figure 8. The places that are red in the UV will get read in the shader and with that the depth is calculated. I also lerp between the ground and snow texture for a smooth transition.

//Vertex shader
 
//Clamp the texture from 0 to 1 so it will not stack 
//Look up the splat texture and grab the red value of the uv
//Add the displacement value as an offset
float depth = saturate(tex2Dlod(_SnowSplatTex, float4(uv, 0, 0)).r) * _Displacement;
//Set the vertex and distract the normal with the displacement for the depth in the snow
v.vertex.xyz -= v.normal * depth;
//Surface shader
 
//Clamp the texture from 0 to 1 so it will not stack
//Look up the splat texture and grab the red value of the uv
//Add the displacement value as an offset
half depth = saturate(tex2Dlod(_SnowSplatTex, float4(uv, 0, 0)).r);
 
//mix the snow and ground texture according to the depth of the snow
half4 color = lerp(tex2D(_SnowTex, IN.uv_SnowTex) * _SnowColor, tex2D(_GroundTex, IN.uv_GroundTex) * _GroundColor, depth);

3. Making it interactive

Now that the shader takes the red value of a render texture, it is now the task to let custom objects output the color red and draw it to the render texture.

3.1 First attempt

Figure 9: Splat map walking (self made)

My first attempt was by following a YouTube course from Peer Play about a snow shader (Peer Play(18–01-23a)).

In this course they set the red color in the texture via code. I checked with a raycast where the objects hit and changed a vector in the shader. This vector was the hit point. The shader then knows that in that point there has to be made some depth. After that we make a new splatmap/render texture temporarily (for memory reasons). I then blit the texture of the shader with the new texture. Blit combines the 2 so the new one has the input from the old one. After that I connect it with the material so it uses the new texture. After that the target texture from the camera is the new one.

This is what it looks like in C#:

foreach (Transform interactionObject in interactionObjects)
{
    if (Physics.Raycast(interactionObject.transform.position, -Vector3.up, out hit))
    {
        SnowInteractionObject snowInteractionObject = interactionObject.GetComponent<SnowInteractionObject>();
        if (!snowInteractionObject.onSnow)
            return;
 
        snowInteractionObject._drawMaterial.SetVector
            ("_Coordinate"new Vector4(hit.textureCoord.x, hit.textureCoord.y, 00));
        RenderTexture temp = RenderTexture.GetTemporary(_splatmap.width, _splatmap.height, 0, RenderTextureFormat.ARGBFloat);
        Graphics.Blit(_splatmap, temp);
        Graphics.Blit(temp, _splatmap, snowInteractionObject._drawMaterial);
 
        //Remove it from memory
        RenderTexture.ReleaseTemporary(temp);
        _camera.targetTexture = temp;
    }
}

This attempt worked nicely, but later on during this project I had some performance issues. The first issue was the foreach loop in the update for each object that needs to interact with the snow. This can be a heavy process for the CPU. The second one is that this mostly runs on the CPU with all the other processes. The CPU cannot handle that much. So I decided for a new technique using more of the GPU, using a orthographic camera.

3.2 Orthographic camera

Figure 10: Render texture output (self made)

I saw this technique on the site of minionsart (MinionsArt, M (19–01-23)). It uses an orthographic camera to output certain objects from a layer to the render texture of the camera. This is perfect for this project. This way I do not have to do this in code, but I can just work via a layer and read the render texture in the shader. I still have to do some things in code but not as much as before.

What I did to make this work was to first create a layer for all the objects that need to interact with the snow. Then the main camera will not display the layer but the orthographic camera will (and only that layer). This way I will have the output of the objects on the render texture. The orthographic camera does need to have the no clear flag on so the output of the objects will not dissapear but stack.

Connecting the shader with the render texture was particularly easy to do. I only had to use the same render texture in the shader as the one in the orthographic camera. Plus I had to set the scale correctly of the uv using the orthographic camera size, see code below.

//Set the value correctly in the shader
Shader.SetGlobalFloat("_OrthographicCamSize", orthoCam.orthographicSize);

3.3 Material

Figure 11: Color material and mesh as a child (self made)

Now that we connected the orthographic camera with the shader, it still will not output the red color. We need to have a specific material which outputs the color red. Plus that material needs to be on the layer of the interactive snow objects so it will be set on the render texture. Then I made the object with the material the parent and the object with the mesh and a normal material as a child without the layer put on, see figure 11.

With this technique it also works with whatever mesh the red material is on. Which was not possible with the old attempt as well. So now it can work for sphere, cubes, custom person meshes etc.

3.4 Opacity and blending

Figure 12: Opacity in the snow (self made)

So now the render texture works when walking around, but I did not want the snow to be instant gone. I wanted to have a custom opacity per object. I added a float for the opacity in the inspector in the same red color material. I then multiplied the opacity with the color, but sadly it still did the same. After some research I had to add blend mode in the shader (Unity. (21–03-13)). This tells the shader that the colors will add up, which we exactly need for this shader.

So after that the result is that the longer the object stay, the brigher the red color becomes. The brighter it is, the depth increases in the snow.

4. Keep it moving

Figure 13: Render texture out of range (self made)

Now that the snow worked with the meshes I wanted there was still an issue. The orthographic camera only has a specific size. When the objects got of out the range this happens, see figure 13. The result is showing in the scene view on the left.

Figure 14: Render texture moving according to player movement (self made)

To fix this I needed the camera position on the player, plus make a new render texture with the right positions for the interactive snow objects. I made a bit of a sketch on the right, see figure 14. So I thought when the player moves to the right, then the other objects need to move to the left to correspond for the new render texture. So I first set the camera to the player position, but no suprise the snow did not move with it. So the next step for me was to also move the render texture opposite from the player movement. So when the player moves right, the render texture needs to move to the left.

I first made a script that changes the render texture with the new one, again with Blit and a temporary render texture as before (for memory reasons). I also had to give the camera position to the snow shader so it knows at which point to set the UV. I sadly forgot about this at first so I was confused for a long time why it did not work. I also made a new shader “MovementCorrection” to set the position right on the new render texture. In the shader. In this shader I add the offset to the uv. Sadly this was not enough. When a red sphere was just on the outside rim of the render texture it would still drag the red color in the new render texture, which still causes the effect in figure 13.

To fix this I made in the same shader a outside rim to prevent it. The outside rim will be black so it would not drag the red color. I made an x and a y value that I set between 0 and 1 (because of the uv value). I also use the abs value so it does not matter if it is a negative value. I do 1 minus the texelsize (resolution) of the main texture. After that I set the rgb value of the color to black if it is in the outside rim, otherwise it will return the normal color that was in the uv texture/splat map. I also had to do this in the opacity shader because that ran also per fps and that overwrites it a bit. I mean with that is when the movementCorrection shader put it to black, the opacity shader made it red again. That is why I also did the same process in there.

After this whole process I set the new texture to the shaders that needed it. This is the code that made it all work:

C# code:

Vector2 currPos = new Vector2(player.transform.position.x, player.transform.position.z);
Vector2 delta = currPos - oldPlayerPos;
delta /= orthoCam.orthographicSize * 2.0f;
 
oldPlayerPos = currPos;
orthoCam.transform.position = player.transform.position + new Vector3(0.0f50.0f0.0f);
Shader.SetGlobalVector("_CameraPosition", orthoCam.transform.position);
 
MovementCorrectionMaterial.SetVector("_UVOffset"delta);
Shader.SetGlobalVector("_UVOffsetSnow"delta);
 
RenderTexture temp = RenderTexture.GetTemporary(orthoCam.targetTexture.width, orthoCam.targetTexture.height,
      0, orthoCam.targetTexture.format);
Graphics.Blit(orthoCam.targetTexture, temp, MovementCorrectionMaterial);
Graphics.Blit(temp, orthoCam.targetTexture);
 
Shader.SetGlobalTexture("_SplatTex", orthoCam.targetTexture);
Shader.SetGlobalTexture("_SnowSplatTex", orthoCam.targetTexture);
 
temp.Release();

Shader code:

fixed4 col = tex2D(_MainTex, i.uv +  _UVOffset);
 
//Set a black edge around the uv
float x = step(1.0 - _MainTex_TexelSize.* 2.0f, saturate(abs((i.uv.x  * 2.0) - 1.0)));
float y = step(1.0 - _MainTex_TexelSize.* 2.0f, saturate(abs((i.uv.y  * 2.0) - 1.0)));
col.rgb = lerp(col.rgb, (0.0).xxx, min(+ y, 1.0));  

5. Snowing

Figure 15: Snow filling after time (self made)

A fun thing I wanted to have in this project was the effect of snowing. So when it snowes the depth of the snow would fill up again after some time. See figure 15 for the result.

To make this work I made a new shader for the snow falling. I made a random value so it creates a bit of a natural feel to it. thankfully there already existed a method for a random value so I grabbed that. For me it required a float3 so I grabbed the x and y values of the uv and multiplied it with the time of the GPU. Then I used the ceil function to make it o or 1 rounded up. But most of the values will probably be a bit above 0 which rounded up is 1. To have some more control over this I added a flake amount to control it a bit more in the inspector. After that I also added the flake opacity to make it stronger. I only added this to the red value, because red is the only value read by the snow shader.

This is the shader code:

float rValue = ceil(random(float3(i.uv.x, i.uv.y, 0) * _Time.x) - (1 - _FlakeAmount));
col.= col.- (rValue * _FlakeOpacity);

But only a shader is not enough. It needed to connect with the snow track shader. To do so we again grabbed the render texture from the mesh renderer of the plane. We then made a new temporary render texture again and blit it with the one from the snow plane. We then connected the material to it and set the new texture to the snow track shader. And bam it worked!

6. Finished product

Git link: https://github.com/JeanyDeVries/InteractiveSnowShader

7. Conclusion

I got the result I wanted, making a shader that leaves prints behind. I’m happy that it works easily for each mesh. It also is a great shader performance wise, because (mostly) everything goes via the GPU.

The shader works via a orthographic camera which makes a render texture of the output of specific objects, interactive snow objects. These leave a red trail because of a shader put on the objects that output a red color. Then the render texture gets read by the snow shader that gives depth to the places where the render texture is red.

The player can also move around the scene without worrying that the render texture gets out of bounds. A new render texture is made with an offset that gives the snow depth the correct placement according to the player position. A black outline is also added so the red color will not be dragged into the new render texture.

A fun addition to the shader is the snow particles. When the snow particles object is on, the snow will accumulate with the depth of the snow. So it will basically grow back the snow.

8. Future references

I’m happy with how the product turns out, but there are still a ton of things that can be improved. I want to improve this shader later on and make it more user friendly so I can sell it on the asset store. I want to improve the accumulation of the snow particles, because for me it is still a bit sketchy Plus I want snow to accumalate on the sides and as I said make it more user friendly for the asset store.

I also want to broaden my HLSL skills a bit. I just want to try out stuff and get used to all the methods and tricks that can be used for shaders. I still have an amazing course I have to follow (de Byl(20–11-01)).

Sources

de Byl, P. B. (20–11-01). Shader Development from Scratch for Unity with Cg. Udemy. https://www.udemy.com/course/unity-shaders/

Flick, J. F. (z.d.). Tessellation [Image]. https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/

MinionsArt, M. (19–01-23). Making Interactive Water using RenderTexture. Patreon. https://www.patreon.com/posts/24192529

MinionsArt, M. (19–01-25). Steps projecting a render texture. Imgur. https://imgur.com/dCf9cFa

NVIDIA. (z.d.). Cg Standard Library. Geraadpleegd op 21–03-14, van http://developer.download.nvidia.com/cg/index_stdlib.html

Peer Play. (18–01-23a). Snowtracks Shader – Unity CG/C# Tutorial [Part 1 – Tesselation Theory]. YouTube. https://www.youtube.com/watch?v=Sr2KoaKN3mU&list=WL&index=14&t=482s&ab_channel=PeerPlay

Peer Play. (18–01-23b). Snowtracks Shader – Unity CG/C# Tutorial [Part 5 – Regenerating Snow]. YouTube. https://www.youtube.com/watch?v=LMSDFhGP73g&t=665s&ab_channel=PeerPlay

The snow physics are such an awesome little touch. (17–11-21). [Image]. imgur. https://imgur.com/r/ps4/9Jl2rDa

Unity. (2021, 22 maart). Surface Shaders with DX11 / OpenGL Core Tessellation. https://docs.unity3d.com/Manual/SL-SurfaceShaderTessellation.html

Unity. (21–03-22a). Graphics.Blit. https://docs.unity3d.com/ScriptReference/Graphics.Blit.html

Unity. (21–03-22b). Render Texture. https://docs.unity3d.com/Manual/class-RenderTexture.html

Unity. (21–03-13). ShaderLab: Blending. https://docs.unity3d.com/Manual/SL-Blend.html

Unity. (21–03-22c). Surface Shader examples. https://docs.unity3d.com/Manual/SL-SurfaceShaderExamples.html

Related Posts