This is the thing I've rebuilt from scratch in every project and finally decided to do properly. A surface type system: tag your floor/terrain nodes with a resource, and anything that touches those surfaces (character feet, projectiles, whatever) can ask "what's here?" and get back the right footstep sound, impact particle, and decal.
The resource itself is just:
class_name SurfaceType
extends Resource
@export var surface_name: String = "default"
@export var footstep_sounds: Array[AudioStream] = []
@export var impact_particles: PackedScene = null
@export var decal_scene: PackedScene = null
@export var footstep_volume_db: float = 0.0
@export var footstep_pitch_variance: float = 0.1Then a small autoload does a short downward raycast and reads surface_type meta from whatever it hits:
extends Node
const DEFAULT_SURFACE := preload("res://surfaces/default.tres")
func get_surface_at(world_pos: Vector3, mask: int = 1) -> SurfaceType:
var space := get_viewport().find_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(
world_pos + Vector3.UP * 0.15,
world_pos + Vector3.DOWN * 0.4,
mask
)
var hit := space.intersect_ray(query)
if hit.is_empty():
return DEFAULT_SURFACE
var body = hit.collider
if body.has_meta(&"surface_type"):
return body.get_meta(&"surface_type")
return DEFAULT_SURFACETag your floor nodes in the editor or at runtime: $GrassFloor.set_meta(&"surface_type", preload("res://surfaces/grass.tres")). Characters call SurfaceDetector.get_surface_at(foot_pos) on foot plant frames. Projectile impacts just cast from the hit point. Surprisingly clean once it's wired up.
The part I haven't solved: terrain with multiple surface materials. If you're using Terrain3D or a splatmap setup, meta on the collision body doesn't tell you which texture is under the foot. I'm thinking about sampling the splatmap via Image.get_pixel at the projected UV, but it feels janky and I don't love reading from the CPU side every footstep. Has anyone tackled blended surface detection on terrain? Curious if there's a cleaner approach without a bunch of extra raycasts or texture readback.