Developed using Unity and HLSL

Snow Ground Shader

I have developed a custom URP shader with HLSL tessellation and a RenderTexture-based interaction mask to simulate stylized snow.


The snow ground reacts to player collisions and disappears over time, and is stylized to glisten under higher levels of scene light.  The shader uses GPU tessellation to dynamically add geometry to the snow surface, with distance-based tessellation so higher geometry detail is only generated close to the camera.


Snow Ground Shader animated preview

Snow Controller.cs

using UnityEngine;

public class SnowController : MonoBehaviour
{
    [Header("snow setup")]
    public ComputeShader snowComputeShader;

    [HideInInspector]
    public RenderTexture snowRT;

    public int resolution = 512;
    public float colorValueToAdd;


    // shader property names
    private string snowImageProperty = "snowImage";
    private string colorValueProperty = "colorValueToAdd";
    private string resolutionProperty = "resolution";
    private string positionXProperty = "positionX";
    private string positionYProperty = "positionY";
    private string spotSizeProperty = "spotSize";

    // kernels
    private string csMainKernel = "CSMain";
    private string fillWhiteKernel = "FillWhite";



    private MeshRenderer meshRenderer;


    private void Awake()
    {
        CreateRenderTexture();
        SetRTColorToWhite();
        SetMaterialTexture();

        // slowly fills the texture back over time
        InvokeRepeating(nameof(AddSnowLayer), 0.1f, 0.1f);

        ExtendBoundsOfMesh();
    }



    //creates the texture the shader writes into
    private void CreateRenderTexture()
    {
        snowRT = new RenderTexture(resolution, resolution, 24);

        snowRT.enableRandomWrite = true;
        snowRT.Create();
    }


    // starts the texture fully white
    private void SetRTColorToWhite()
    {
        int kernelHandle = snowComputeShader.FindKernel(fillWhiteKernel);

        snowComputeShader.SetTexture(kernelHandle, snowImageProperty, snowRT);

        snowComputeShader.SetFloat(colorValueProperty, colorValueToAdd);
        snowComputeShader.SetFloat(resolutionProperty, resolution);

        snowComputeShader.SetFloat(positionXProperty, 0);
        snowComputeShader.SetFloat(positionYProperty, 0);
        snowComputeShader.SetFloat(spotSizeProperty, 0);

        snowComputeShader.Dispatch(kernelHandle, snowRT.width / 8, snowRT.height / 8, 1);
    }


    //sends the render texture to the material
    private void SetMaterialTexture()
    {
        meshRenderer = GetComponent<MeshRenderer>();

        meshRenderer.material.SetTexture("_PathTexture", snowRT);
    }


    // slowly restores snow over footprints
    private void AddSnowLayer()
    {
        int kernelHandle = snowComputeShader.FindKernel(csMainKernel);

        snowComputeShader.SetTexture(kernelHandle, snowImageProperty, snowRT);

        snowComputeShader.SetFloat(colorValueProperty, colorValueToAdd);
        snowComputeShader.SetFloat(resolutionProperty, resolution);

        snowComputeShader.SetFloat(positionXProperty, 0);
        snowComputeShader.SetFloat(positionYProperty, 0);
        snowComputeShader.SetFloat(spotSizeProperty, 0);

        snowComputeShader.Dispatch(kernelHandle, snowRT.width / 8, snowRT.height / 8, 1);
    }


    // expands the mesh bounds so the shader effect doesnt get culled
    private void ExtendBoundsOfMesh()
    {
        Bounds bounds = GetComponent<MeshFilter>().mesh.bounds;

        bounds.extents = new Vector3(2, 0, 2);

        GetComponent<MeshFilter>().mesh.bounds = bounds;
    }
}

Path Drawer.cs

using UnityEngine;

public class SnowPathDrawer : MonoBehaviour
{
    [Header("compute shader setup")]
    public ComputeShader snowComputeShader;
    public RenderTexture snowRT;

    [Header("footstep settings")]
    public float spotSize = 5f;


    private PlayerMovement playerMovement;

    private GameObject[] snowControllerObjs;

    private SnowController snowController;

    private Vector2Int position = new Vector2Int(256, 256);


    //property names
    private string snowImageProperty = "snowImage";
    private string colorValueProperty = "colorValueToAdd";
    private string resolutionProperty = "resolution";
    private string positionXProperty = "positionX";
    private string positionYProperty = "positionY";
    private string spotSizeProperty = "spotSize";

    // compute shader kernel name
    private string drawSpotKernel = "DrawSpot";


    private void Awake()
    {
        // grab all snow surfaces in the scene
        snowControllerObjs = GameObject.FindGameObjectsWithTag("Ground");

        // used for grounded checks before drawing footprints
        playerMovement = GetComponent<PlayerMovement>();
    }


    private void FixedUpdate()
    {
        // dont draw trails while the player is airborne
        if (playerMovement == null || !playerMovement.Grounded)
        {
            return;
        }
        

        // check nearby snow surfaces and draw onto them
        for (int i = 0; i < snowControllerObjs.Length; i++)
        {
            // skip surfaces that are too far away
            if (Vector3.Distance(snowControllerObjs[i].transform.position, transform.position) > spotSize * 5f)
            {
                continue;
            }

            // get the snow controller for this surface
            snowController = snowControllerObjs[i].GetComponent<SnowController>();

            // use this surface's render texture
            snowRT = snowController.snowRT;


            // convert world position into texture space
            GetPosition();
            // draw the footprint into the render texture
            DrawSpot();
        }
    }


    // converts the player world position into render texture coordinates
    private void GetPosition()
    {

        float scaleX = snowController.transform.localScale.x;
        float scaleY = snowController.transform.localScale.z;

        float snowPosX = snowController.transform.position.x;
        float snowPosY = snowController.transform.position.z;

        int posX = snowRT.width / 2 - (int)(((transform.position.x - snowPosX) * snowRT.width / 2) / scaleX);
        int posY = snowRT.height / 2 - (int)(((transform.position.z - snowPosY) * snowRT.height / 2) / scaleY);


        position = new Vector2Int(posX, posY);
    }


    // sends the footprint data into the compute shader
    private void DrawSpot()
    {
        // nothing to draw onto
        if (snowRT == null)
        {
            return;
        }

        // compute shader missing
        if (snowComputeShader == null)
        {
            return;
        }

        int kernelHandle = snowComputeShader.FindKernel(drawSpotKernel);

        // assign the texture the shader will modify
        snowComputeShader.SetTexture(kernelHandle, snowImageProperty, snowRT);

        // shader values used for drawing the footprint
        snowComputeShader.SetFloat(colorValueProperty, 0);

        snowComputeShader.SetFloat(resolutionProperty, snowRT.width);
        snowComputeShader.SetFloat(positionXProperty, position.x);
        snowComputeShader.SetFloat(positionYProperty, position.y);
        snowComputeShader.SetFloat(spotSizeProperty, spotSize);


        // run the shader across the texture
        snowComputeShader.Dispatch(kernelHandle, snowRT.width / 8, snowRT.height / 8, 1);
    }
}

Snow Computer Shader.compute

#pragma kernel CSMain
#pragma kernel FillWhite
#pragma kernel DrawSpot


RWTexture2D<float4> snowImage;

float colorValueToAdd;
float resolution;

float positionX;
float positionY;

float spotSize;


[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	// slowly rebuilds the snow layer over time
	snowImage[id.xy] += float4(colorValueToAdd, colorValueToAdd, colorValueToAdd, colorValueToAdd);
}



[numthreads(8, 8, 1)]
void FillWhite(uint3 id : SV_DispatchThreadID)
{
	// reset the whole texture back to white
	snowImage[id.xy] = float4(1.0, 1.0, 1.0, 1.0);
}


[numthreads(8, 8, 1)]
void DrawSpot(uint3 id : SV_DispatchThreadID)
{
	float x = id.x / resolution;
	float y = id.y / resolution;


	float value;

	// distance from current pixel to the draw position
	value = sqrt(
		((x - positionX / resolution) * (x - positionX / resolution)) +
		((y - positionY / resolution) * (y - positionY / resolution))
	) * 100;


	// only affect pixels inside the spot radius
	if (value < spotSize)
	{

		
		// dont overwrite deeper footprints
		if (value / spotSize > snowImage[id.xy].x)
		{
			return;
		}

		snowImage[id.xy] = float4(value, value, value, value) / spotSize;
	}
}