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



# 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)



$"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);
ring, rings, rts, SDF, terrain
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 thepathgame

Volumetric Raymarched (animated) Clouds v2

Volumetric Clouds

CRT with variable fisheye

Related shaders

3D Color Range

Notify of

Inline Feedbacks
View all comments