SDF Metaballs

This is a metaballs shader that uses ray marching to perfectly blend one ball into another.

Set up :

1. Create a mesh instance and assign box mesh and set its size large enough (100,100,100 etc).

2. Attach this material to the mesh’s first material slot.

3. Pass the uniforms (ball_positions, ball_colors, and active_balls) from a script.

This is the script im using for passing uniforms (attached to the mesh) :

@tool
extends MeshInstance3D

# if you want to see changes in editor, keep this true
@export var run_in_editor: bool = true
@export var mat : ShaderMaterial
@export var max_active : int = 20

func _ready() -> void:
	mat = get_active_material(0) as ShaderMaterial

func _process(delta):
	if Engine.is_editor_hint() and not run_in_editor:
		return

	var positions = []
	var colors = []
	var active_count = 0
	for child in get_children():
		if child is Metaball:
			var radius = child.radius
			positions.append(Vector4(child.global_position.x,child.global_position.y,child.global_position.z, radius))
			
			colors.append(child.color)
			active_count += 1
			
			if child is MeshInstance3D and child.visible:
				child.visible = false
			
			if active_count >= max_active:
				break
				
	if active_count > 0:
		mat.set_shader_parameter("active_balls", active_count)
		mat.set_shader_parameter("ball_positions", positions)
		mat.set_shader_parameter("ball_colors", colors)
		

you can pass the uniforms by adding nodes as children of the mesh or a group :

extends Node3D

class_name Metaball

@export var color : Color
@export var radius : float

 

Shader code
shader_type spatial;
render_mode unshaded, cull_disabled; //we handle lighting manually for performance

uniform int max_steps = 20;  //max number of steps in ray marching loop, reduce this to increase performance but reduce accuracy
uniform float max_dist = 20.0;    // max ray distance
uniform float surf_dist = 0.01;   // precision of the surface hit

uniform vec4 ball_positions[20]; //array to hold ball positions, vec4.w is radius of the metaball
uniform vec4 ball_colors[20];  // array to hold ball colors
uniform int active_balls = 0;     // how many balls are currently active
uniform float smoothness = 0.8;   // smoothness of the connection

float smin(float d1, float d2, float k) {
    float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
    return mix(d2, d1, h) - k * h * (1.0 - h);
}

vec3 smin_col(float d1, float d2, vec3 c1, vec3 c2, float k) {
    float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
    return mix(c2, c1, h);
}

vec4 get_scene(vec3 p) {
    float d = max_dist;
    vec3 col = vec3(0.0);

    for(int i = 0; i < active_balls; i++) {
        float sphere_dist = length(p - ball_positions[i].xyz) - ball_positions[i].w;
        vec3 sphere_col = ball_colors[i].rgb;
		
        if (i == 0) {
            d = sphere_dist;
            col = sphere_col;
        } else {
            col = smin_col(d, sphere_dist, col, sphere_col, smoothness);
            d = smin(d, sphere_dist, smoothness);
        }
    }
    return vec4(d, col);
}

vec3 get_normal(vec3 p) {
    float d = get_scene(p).x;
    vec2 e = vec2(0.01, 0);

    vec3 n = d - vec3(
        get_scene(p - e.xyy).x,
        get_scene(p - e.yxy).x,
        get_scene(p - e.yyx).x
    );

    return normalize(n);
}

void fragment() {
	
    vec3 ro = (inverse(VIEW_MATRIX) * vec4(0.0, 0.0, 0.0, 1.0)).xyz; //ray origin
    vec3 rd = normalize((inverse(VIEW_MATRIX) * vec4(VERTEX, 1.0)).xyz - ro); //ray direction

    // raymarching loop
    float d = 0.0; // distance traveled
    bool hit = false;
    vec3 p;
	vec4 result;
    for(int i = 0; i < max_steps; i++) {
        vec3 p = ro + rd * d;
        result = get_scene(p); //get distance and color
        float dS = result.x;
        d += dS;
        if(d > max_dist || dS < surf_dist) break;
    }

    if(d < max_dist) {
        vec3 p = ro + rd * d;
        vec3 n = get_normal(p);

        // rim lighting
        float rim = 1.0 - max(dot(n, -rd), 0.0);
        rim = pow(rim, 3.0);
		
        ALBEDO = result.yzw + (vec3(1.0) * rim * 0.5);
    } else {
        discard; // draw nothing if missed 
    }
}
Live Preview
Tags
Blob, metaball, metaballs, ray marching, raymarching, SDF
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

guest

0 Comments
Oldest
Newest Most Voted