Introduction

A real-time interactive snow deformation system implemented with custom shaders, enabling dynamic footprints, object interaction, and environmental response.

The source code is available on GitHub.

Snow Heightmap Update

A traditional approach to simulate snow deformation is to use a heightmap texture to represent the snow surface. The heightmap is updated in real-time using a custom shader that modifies the height values based on interactions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
float4 frag(v2f_customrendertexture IN) : COLOR
{
float4 previous_color = tex2D(_SelfTexture2D, IN.localTexcoord.xy);
if (_DrawPosition.x < 0)
return previous_color;

// Rotation
float2 offset = IN.localTexcoord.xy - _DrawPosition;
float2x2 rot = float2x2(cos(_DrawAngle), -sin(_DrawAngle),
sin(_DrawAngle), cos(_DrawAngle));
offset = mul(rot, offset);

// Distance
float dist = length(offset);
float height = smoothstep(_DrawMaxDistance, _DrawMinDistance, dist);

return max(previous_color - height * _DrawHeight, 0.0f);
}

Interactive Snow

To call the shader and update the heightmap in Unity, we can use the following C# script to manage the snow deformation process.

  • Use terrain due to its easy to divide into more surfaces for better visual effect.
  • StepPrint is a class that contains the position, rotation, and size of the footprint to be drawn.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class InteractiveSnow : MonoBehaviour
{
public Material snowMaterial;
public Material heightMapMaterial;
public StepPrint[] stepPrints;
private CustomRenderTexture _snowHeightMap;
private readonly int _heightMap = Shader.PropertyToID("_HeightMap");

// CRT only can process one step print per frame, so we need to keep track of the index
private int _index = 0;

private void Awake()
{
// Initialize material
var material = new Material(snowMaterial);
heightMapMaterial.SetVector("_DrawPosition", new Vector4(-1, -1, 0, 0)); // Don't draw at the beginning

// Initialize terrain
var terrain = gameObject.GetComponent<Terrain>();
_snowHeightMap = CreateHeightMap(512, 512, heightMapMaterial);
terrain.materialTemplate = material;
terrain.materialTemplate.SetTexture(_heightMap, _snowHeightMap);
_snowHeightMap.Initialize();
}

private void Update()
{
if (stepPrints.Length == 0) return;

stepPrints[_index].DrawTrails(heightMapMaterial);
_snowHeightMap.Update();
_index = (_index + 1) % stepPrints.Length;
}

private static CustomRenderTexture CreateHeightMap(int weight, int height, Material material)
{
var texture = new CustomRenderTexture(weight, height)
{
dimension = TextureDimension.Tex2D,
format = RenderTextureFormat.R8,
material = material,
updateMode = CustomRenderTextureUpdateMode.Realtime,
doubleBuffered = true
};

// Initialize to white
var activeRT = RenderTexture.active;
RenderTexture.active = texture;
GL.Clear(true, true, Color.white);
RenderTexture.active = activeRT;
return texture;
}
}

Step Print

Every agent that can leave a footprint on the snow surface should have a StepPrint component with all its foots transforms stored in TrailsTransforms.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class StepPrint : MonoBehaviour
{
public float printHeight = 0.3f;
public float printInterval = 0.1f;
public float drawHeight = 0.8f;
public float drawMinDistance = 0.1f;
public float drawMaxDistance = 0.2f;
public Transform[] trailsTransforms;

private int _index = 0;
private float[] _lastPrintTimes;
private readonly int _drawPosition = Shader.PropertyToID("_DrawPosition");
private readonly int _drawHeight = Shader.PropertyToID("_DrawHeight");
private readonly int _drawMinDistance = Shader.PropertyToID("_DrawMinDistance");
private readonly int _drawMaxDistance = Shader.PropertyToID("_DrawMaxDistance");
private readonly int _drawAngle = Shader.PropertyToID("_DrawAngle");

private void Awake()
{
_lastPrintTimes = new float[trailsTransforms.Length];
}

public void DrawTrails(Material material)
{
if (Time.time - _lastPrintTimes[_index] > printInterval)
{
var trail = trailsTransforms[_index];
var ray = new Ray(trail.position, Vector3.down);
if (Physics.Raycast(ray, out var hit, printHeight, LayerMask.GetMask("Snow")))
{
material.SetVector(_drawPosition, hit.textureCoord);
material.SetFloat(_drawHeight, drawHeight);
material.SetFloat(_drawMinDistance, drawMinDistance);
material.SetFloat(_drawMaxDistance, drawMaxDistance);
material.SetFloat(_drawAngle, trail.rotation.eulerAngles.y * Mathf.Deg2Rad);

// Update last print time
_lastPrintTimes[_index] = Time.time;
}
}

_index = (_index + 1) % trailsTransforms.Length;
}
}

Interactive Snow on the Stone

It is a little poor to only render interactive snow on the terrain. We can also apply the snow heightmap to other objects like stones.

Also, we can use many independent terrains to cover all the objects in the scene(such as trees, buildings, etc.) for better visual effects. But be careful about the performance issue when you have too many terrains in the scene.

Another Method by Using Camera

Maybe this method is better for cover all objects with only one heightmap texture, like (this)[https://www.patreon.com/posts/51008483].

Create snow from bottom to top and if the object intersects with the snow volume, we can update the heightmap texture by rendering the intersected area from the camera’s view.