Metaballs
This spatial-shader ray-marches with multiple metaballs inside a unit cube, smoothly blending them with a soft-min (smooth union) controlled by blend_r. Each ball’s center (ball_pos[]) and radius (radi[]) are exposed so you can animate them from a script. The surface normal is estimated in-shader for crisp lighting, and a customizable Fresnel glow (fresnel_color, fresnel_strength, fresnel_emition) adds an ethereal edge highlight. Depth writing is preserved (DEPTH is set manually), letting the metaballs occlude other geometry correctly.
Drop this on a MeshInstance with a simple cube mesh; drive the uniforms from GDScript to get looping blob animations or physics-based motion.
Shader code
shader_type spatial;
render_mode unshaded, cull_disabled, depth_draw_opaque ;
const int n_balls = 8;
uniform vec3 ball_pos[n_balls];
uniform float radi[n_balls];
uniform float blend_r;
const float EPS = 1e-3;
uniform vec3 base_color : source_color;
uniform vec3 fresnel_color : source_color;
uniform float fresnel_strength;
uniform float fresnel_emition;
bool ray_box_intersect(vec3 ray_origin, vec3 ray_dir,vec3 b_min, vec3 b_max, out float t_min, out float t_max){
vec3 t1 = (b_min - ray_origin)/ray_dir;
vec3 t2 = (b_max - ray_origin)/ray_dir;
vec3 t_min_vec = min(t1,t2);
vec3 t_max_vec = max(t1,t2);
t_max =min(t_max_vec.x ,min(t_max_vec.y, t_max_vec.z));
t_min =max(t_min_vec.x ,max(t_min_vec.y, t_min_vec.z));
//if (t_min < 0.0){
//return false;
//}
if (t_max < t_min){
return false;
}
return true;
}
float s_min(float a, float b, float k){
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
float sdf_metaballs(vec3 p){
float fields[n_balls];
for (int i = 0; i < n_balls; i++){
fields[i] = length(p - ball_pos[i]) - radi[i];
}
float d = fields[0];
for (int i = 0; i < n_balls; i++){
d = s_min(d,fields[i], blend_r);
}
return d;
}
vec3 estimate_normal(vec3 p) {
float dx = sdf_metaballs(p + vec3( EPS, 0.0, 0.0)) -
sdf_metaballs(p - vec3( EPS, 0.0, 0.0));
float dy = sdf_metaballs(p + vec3(0.0, EPS, 0.0)) -
sdf_metaballs(p - vec3(0.0, EPS, 0.0));
float dz = sdf_metaballs(p + vec3(0.0, 0.0, EPS)) -
sdf_metaballs(p - vec3(0.0, 0.0, EPS));
return normalize(vec3(dx, dy, dz));
}
bool ray_march(vec3 ray_origin, vec3 ray_dir, float t_min, float t_max, out vec3 p) {
float t = t_min;
const int MAX_STEPS = 64;
for (int i = 0; i < MAX_STEPS; i++) {
p = ray_origin + t * ray_dir;
float d = sdf_metaballs(p); // distance to surface
if (d < 0.001) { // hit!
return true;
}
t += d; // advance by the distance field
if (t > t_max)
break;
}
return false;
}
vec3 fresnel_glow(float amount, float intensity, vec3 color, vec3 normal, vec3 view)
{
return pow((1.0 - dot(normalize(normal), normalize(view))), amount) * color * intensity;
}
void fragment() {
vec3 world_position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
vec3 ray_origin = CAMERA_POSITION_WORLD;
vec3 ray_dir = normalize(world_position - ray_origin);
ray_origin = (inverse(MODEL_MATRIX) * vec4(ray_origin, 1.0)).xyz;
ray_dir = normalize((inverse(MODEL_MATRIX) * vec4(ray_dir, 0.0)).xyz);
// 2. Find where the ray enters and exits the cube (ray–box intersection)
float t_min, t_max;
if (!ray_box_intersect(ray_origin, ray_dir, vec3(-0.5), vec3(0.5), t_min, t_max))
discard;
// keep cull_disabled
if (!FRONT_FACING && t_min > 0.0) {
discard; // skip back faces only when the eye is outside
}
// misses the box
// 3. March the ray through the SDF field
vec3 p = vec3(0);
if(!ray_march(ray_origin, ray_dir, t_min, t_max, p))
discard;
vec3 normals = estimate_normal(p);
NORMAL = normals;
vec3 fresnel = fresnel_glow(fresnel_strength, fresnel_emition, fresnel_color, NORMAL, normalize(-ray_dir));
ALBEDO = base_color + fresnel;
ALPHA = 1.0;
vec4 world_p = MODEL_MATRIX * vec4(p, 1.0);
vec4 clip_p = PROJECTION_MATRIX * VIEW_MATRIX * world_p;
float depth = clip_p.z / clip_p.w; // −1 … 1
DEPTH = depth ; // 0 … 1 // 0 … 1
}

This is great! Here’s a simple script I used to get it working:
extends MeshInstance3D @export var motion_radius: float = 0.3 @export var min_radius: float = 0.05 @export var max_radius: float = 0.15 @export var speed: float = 1.0 @export var blend_r: float = 0.2 const BALL_COUNT := 8 var phases_pos: Array = [] var phases_dir: Array = [] var phases_rad: Array = [] var mat: ShaderMaterial func _ready(): # Make sure we have a unique material instance if get_surface_override_material(0) == null: set_surface_override_material(0, mesh.surface_get_material(0).duplicate()) mat = get_surface_override_material(0) # Random phases for i in range(BALL_COUNT): phases_pos.append(Vector3(randf() * TAU, randf() * TAU, randf() * TAU)) phases_dir.append(Vector3(randf_range(0.5, 1.5), randf_range(0.5, 1.5), randf_range(0.5, 1.5))) phases_rad.append(randf() * TAU) mat.set_shader_parameter("blend_r", blend_r) func _process(delta: float): var time = Time.get_ticks_msec() / 1000.0 * speed var positions := PackedVector3Array() var radii := PackedFloat32Array() for i in range(BALL_COUNT): var phase = phases_pos[i] + phases_dir[i] * time var pos = Vector3(sin(phase.x), sin(phase.y * 1.2), sin(phase.z * 0.8)) * motion_radius positions.append(pos) var r_phase = phases_rad[i] + time radii.append(lerp(min_radius, max_radius, 0.5 + 0.5 * sin(r_phase))) mat.set_shader_parameter("ball_pos", positions) mat.set_shader_parameter("radi", radii)this looks great!
one of the best of the website
this script cool asf vro