SDF Range Rings (3D)
This is a performant solution to adding intersecting range rings to 3D terrain.
You can mix the baseColor from the shader in the fragment of your own terrain shader.
This requires that object positions be passed to the shader in a PackedVector3Array.
Here’s a snippet of a script I’m using:
var selected_unit_locations:PackedVector3Array
var selected_units:Array = [
]
func _process(_delta):
if Globals.selected_objects.size() > 0: # using a "Globals" singleton/autoload to store currently selected objects
selected_unit_locations.clear() # Clear the array before appending
var excluded_units_count = 0
for unit in Globals.selected_objects:
if unit.is_in_group("range_ring_exclude"):
excluded_units_count += 1
continue # Skip adding this unit's location
selected_unit_locations.append(unit.global_position)
# Set the number of active circles after accounting for exclusions
var active_circles = Globals.selected_objects.size() - excluded_units_count
$"YOUR TERRAIN NODE".get_surface_override_material(0).set_shader_parameter("num_active_circles", active_circles)
$"YOUR TERRAIN NODE".get_surface_override_material(0).set_shader_parameter("range_positions", selected_unit_locations)
else:
selected_unit_locations.clear()
$"YOUR TERRAIN NODE".get_surface_override_material(0).set_shader_parameter("num_active_circles", 0)
Shader code
uniform int num_active_circles = 0;
uniform vec3 range_color : source_color;
uniform vec3 range_positions[60];
uniform float range_radius;
uniform float outlineThickness = 0.8;
float circleSDF(vec2 point, int index) {
return distance(point, range_positions[index].xz) - range_radius;
}
void fragment() {
if (num_active_circles > 0) {
vec2 position2D = worldPosition.xz;
float totalSDF = 1e5;
for (int i = 0; i < num_active_circles; i++) {
float currentSDF = circleSDF(position2D, i);
totalSDF = min(totalSDF, currentSDF);
}
if (abs(totalSDF) < outlineThickness) {
float emissionBlendFactor = smoothstep(outlineThickness, outlineThickness * 0.5, abs(totalSDF));
baseColor = mix(baseColor, range_color.rgb, emissionBlendFactor);
}
}
}