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 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; } }
