Car Tracks On Snow Or Sand – Using viewport textures and particles


A recreation of a snow shader that I saw on YouTube Adapted to Godot using a SubViewport and CPU/GPU particles.



See this repository in GitHub here or download the demo project here, to see for yourself. Or follow the next instructions:

  1. Add a MeshInstance3D node, add PlaneMesh > ShaderMaterial > New shader, and place this shader code inside. Set MeshInstance3D with render_layer=layer1
  2. Add a Camera3D with cull_mask=layer1
  3. Create a SubViewport, inside, add Camera3D pointing down aligned with the terrain with cull_mask = layer2.
  4. Create a CPU/GPUParticleSystem that leaves a trail and set its render_layer=layer2. Copy-paste it into every tire of your car.
  5. Ideally, you’d want to set up the shader inspector values like in screenshot 2. **In the viewport_texture uniform you need to select “New ViewportTexture” and select the SubViewport Node in the scene tree
  6. Over your car node add the following GDScript:
# The car will move where the mouse pointer goes
# Make sure the particles are following your car by either appending them as children or using a RemoteTransform3D
extends Node3D
class_name Car

@onready var cam:Camera3D = $"../Camera3D"
@onready var wheels:Array[Node3D] = [$WheelFR, $WheelFL, $WheelBR, $WheelBL]
@onready var space_state:PhysicsDirectSpaceState3D = get_world_3d().direct_space_state

const INTERACT_RADIUS:int = 15
var query :=
var mouse_position:Vector3

func _ready():

func _physics_process(delta:float):
	# Save the world position from where the mouse is pointing
	var result:Dictionary = _detect_from_cam_to_mouse()
	if result:
		mouse_position = result.position
	# Move and rotate car towards point continuously
	global_position = lerp(global_position, mouse_position, delta)
	var speed:float = (global_position - mouse_position).length()
	if speed > 0.01:
		# Animate wheels according to car's speed
		for wheel in wheels:
			wheel.rotate(Vector3.LEFT, delta*speed*5.0)

func _detect_from_cam_to_mouse() -> Dictionary:
	query.from = cam.global_position = query.from + _get_world_mouse_ray()
	return space_state.intersect_ray(query)

func _get_world_mouse_ray() -> Vector3:
	var mouse_pos:Vector2 = get_viewport().get_mouse_position()
	return cam.project_ray_normal(mouse_pos) * INTERACT_RADIUS

..honestly just download it, it’s too much setup in this one (but hey, the shader is like 5 lines of code)

*Updated: Tracks are a lot smoother now 😀

Shader code
shader_type spatial;

group_uniforms SetupTheseValues;
uniform sampler2D viewport_texture; // Set it as: "New ViewportTexture" and select your SubViewport Node in the scene tree
uniform sampler2D floor_texture:hint_default_white;
uniform sampler2D floor_heightmap:hint_default_white;
uniform vec3 trail_color:source_color = vec3(0.0);
uniform vec3 ground_modulate:source_color = vec3(1.0); // Same as it usually works with modulate property in Sprites and such
uniform float ditch_height:hint_range(0.0, 1.0) = 0.2;

void fragment() {
	float trail = texture(viewport_texture, UV).r;
	vec3 ground = texture(floor_texture, UV).rgb;

	// This changes all blacks and grays into a darkened trail_color-scale but leaves whites as whites
	vec3 trail_recolored = 1.0 - (1.0-trail) * (1.0-trail_color);

	// Multiplying the base colors with the darker colors of the trail will darken the resulting ditch smoothly
	ALBEDO = ground*ground_modulate * trail_recolored;

void vertex() {
	// If heightmap is gray in average, multiplying it by a black trail will create ditches in the ground
	// So it might not look right with mostly-black heightmaps
	float trail = texture(viewport_texture, UV).r;
	VERTEX.y = texture(floor_heightmap, UV).r * ditch_height * trail;

	// Honestly I don't know about normals nearly enough to know if I'm doing it right...
3d, particles, sand, Snow, texture, track, trail, viewport
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from dip

Bloody Pool! – Smooth blood trail

Stylized Cartoon Grass

Related shaders

Rain and Snow with Parallax Effect

Waving Particles

Wind Waker Water – NO Textures needed!

Notify of

Newest Most Voted
Inline Feedbacks
View all comments
7 months ago

VERY AWESOME! Just for anyone using godot 4.0
replace :
@onready var wheels:Array[Node3D] = [$WheelFR, $WheelFL, $WheelBR, $WheelBL]
with this:
@onready var wheels : Array = [ $WheelFR, $WheelFL, $WheelBR, $WheelBL]
I really love this shader 🙂

2 months ago

What does ‘Set MeshInstance3D with render_layer=layer1’ mean?