Car Tracks On Snow Or Sand – Using viewport textures and particles
WHAT IS THIS?
A recreation of a snow shader that I saw on YouTube https://www.youtube.com/watch?v=ThlqTMBzyjI&ab_channel=MertKirimgeri. Adapted to Godot using a SubViewport and CPU/GPU particles.
HOW TO SET THIS UP?!
See this repository in GitHub here or download the demo project here, to see for yourself. Or follow the next instructions:
- Add a MeshInstance3D node, add PlaneMesh > ShaderMaterial > New shader, and place this shader code inside. Set MeshInstance3D with render_layer=layer1
- Add a Camera3D with cull_mask=layer1
- Create a SubViewport, inside, add Camera3D pointing down aligned with the terrain with cull_mask = layer2.
- Create a CPU/GPUParticleSystem that leaves a trail and set its render_layer=layer2. Copy-paste it into every tire of your car.
- 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
- Over your car node add the following GDScript:
# EXAMPLE SCRIPT
# 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 := PhysicsRayQueryParameters3D.new()
var mouse_position:Vector3
func _ready():
query.set_collide_with_areas(true)
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:
look_at(mouse_position)
# 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.to = 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;
group_uniforms;
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...
NORMAL.y = VERTEX.y;
}
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 🙂
Yay! Glad you liked it <3
What does ‘Set MeshInstance3D with render_layer=layer1’ mean?