Moon Jellyfish
A volumetric WebGPU raymarch of a moon jellyfish (Aurelia aurita). A hollow translucent bell is rendered as a soft density field so the drifting tentacles and inner core glow show through, with golden-angle sub-ray bokeh for a shallow depth-of-field.
60 FPS • WebGPU
WGSL Shader Source
struct Uniforms {
resolution: vec2f,
time: f32,
padding: f32,
mouse: vec2f,
speed: f32,
translucency: f32,
bokeh: f32,
glow: f32,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
const TAU: f32 = 6.2831853;
const LIGHT: vec3f = vec3f(0.35, 0.92, 0.45);
@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
return vec4f(pos[vi], 0.0, 1.0);
}
fn sdEllipsoid(p: vec3f, r: vec3f) -> f32 {
let k0 = length(p / r);
let k1 = length(p / (r * r));
return k0 * (k0 - 1.0) / max(k1, 0.0001);
}
fn sdBell(p: vec3f, t: f32) -> f32 {
let pulse = 1.0 + 0.13 * sin(t * 2.0);
let q = vec3f(p.x / (2.0 - pulse), p.y / pulse, p.z / (2.0 - pulse));
let e = sdEllipsoid(q, vec3f(1.0, 0.60, 1.0));
let shell = abs(e) - 0.020;
let rimAng = atan2(q.z, q.x);
let scallop = sin(rimAng * 10.0) * 0.04;
let dome = (-0.18 + scallop) - q.y;
return max(shell, dome);
}
fn sdTentacles(p: vec3f, t: f32) -> f32 {
let pulse = 1.0 + 0.13 * sin(t * 2.0);
let N = 48.0;
let ang = atan2(p.z, p.x);
let a = ang * N / TAU;
let ai = floor(a + 0.5);
let dtheta = (a - ai) * TAU / N;
let h = fract(sin(ai * 12.9898) * 43758.5453);
let r = length(p.xz);
let yTop = -0.15 * pulse;
let yBot = -1.35 - h * 1.35;
let yc = clamp(p.y, yBot, yTop);
let depth = yTop - yc;
let wave = sin(yc * 2.1 + t * 2.0 + ai * 1.7 + h * 6.2831) * (0.02 + 0.03 * depth);
let R = (0.90 + wave - depth * 0.02) * (2.0 - pulse);
let dr = r - R;
let arc = r * dtheta;
let dy = p.y - yc;
return sqrt(dr * dr + arc * arc + dy * dy) - 0.003;
}
fn sdArms(p: vec3f, t: f32) -> f32 {
let N = 10.0;
let ang = atan2(p.z, p.x);
let a = ang * N / TAU;
let ai = floor(a + 0.5);
let dtheta = (a - ai) * TAU / N;
let h = fract(sin(ai * 78.233) * 43758.5453);
let r = length(p.xz);
let yc = clamp(p.y, -1.55 - h * 0.7, -0.08);
let depth = -yc;
let wave = sin(yc * 2.2 + t * 2.3 + ai * 1.9 + h * 6.2831) * (0.05 + 0.05 * depth);
let R = max(0.12 + h * 0.04 + wave + depth * 0.05, 0.02);
let dr = r - R;
let arc = r * dtheta;
let dy = p.y - yc;
let thick = max(0.012 - depth * 0.006, 0.0015);
return sqrt(dr * dr + arc * arc + dy * dy) - thick;
}
// Four horseshoe gonads visible through the translucent bell.
fn gonadGlow(p: vec3f, t: f32) -> f32 {
var g = 0.0;
for (var i = 0; i < 4; i = i + 1) {
let a = f32(i) * TAU / 4.0 + 0.5 + t * 0.06;
let c = vec3f(cos(a) * 0.34, 0.10, sin(a) * 0.34);
let radial = vec3f(cos(a), 0.0, sin(a));
let tangent = vec3f(-sin(a), 0.0, cos(a));
let d = p - c;
let du = dot(d, tangent) / 0.15;
let dv = dot(d, radial) / 0.055;
let dw = d.y / 0.07;
let dd = sqrt(du * du + dv * dv + dw * dw);
g = max(g, 1.0 - smoothstep(0.7, 1.5, dd));
}
return g;
}
fn shadeTentacle(p: vec3f, t: f32) -> vec3f {
let down = 1.0 - smoothstep(-1.4, -0.1, p.y);
return mix(vec3f(0.62, 0.90, 1.0), vec3f(0.46, 0.78, 1.0), down);
}
fn shadeBell(p: vec3f, n: vec3f, rd: vec3f, t: f32) -> vec3f {
let viewDir = -rd;
let fres = pow(1.0 - max(dot(n, viewDir), 0.0), 3.0);
let ndl = max(dot(n, LIGHT), 0.0);
let r = length(p.xz);
let bodyCol = vec3f(0.34, 0.60, 0.84);
let rimCol = vec3f(0.88, 0.80, 0.96);
var col = mix(bodyCol, rimCol, smoothstep(0.25, 1.0, r));
col = col * (0.28 + 0.95 * fres + 0.22 * ndl);
let rimAng = atan2(p.z, p.x);
let canals = pow(abs(sin(rimAng * 4.0)), 18.0);
col = col + rimCol * canals * 0.35 * smoothstep(0.05, 0.3, r) * (1.0 - smoothstep(0.7, 0.95, r));
let halfV = normalize(LIGHT + viewDir);
let spec = pow(max(dot(n, halfV), 0.0), 64.0) * 0.7;
col = col + vec3f(1.0, 0.97, 0.98) * spec;
col = col * (0.92 + 0.08 * sin(fres * 6.2831 + t * 0.5));
return col;
}
fn renderRay(uv: vec2f, t: f32) -> vec3f {
let ro = vec3f(0.0, -0.7, -9.0);
let rd = normalize(vec3f(uv, 2.1));
var glow = vec3f(0.0);
var hitT = -1.0;
var tt = 0.0;
for (var i = 0; i < 96; i = i + 1) {
let p = ro + rd * tt;
let q = vec3f(
p.x + sin(t * 0.6) * 0.06,
p.y - sin(t * 1.1) * 0.12,
p.z + cos(t * 0.5) * 0.05
);
let bell = sdBell(q, t);
var tent = sdTentacles(q, t);
if (q.y < -0.04 && q.y > -2.1) {
tent = min(tent, sdArms(q, t));
}
let adv = max(min(bell, tent) * 0.6, 0.004);
let ad = abs(tent);
if (ad < 0.008) {
let core = 1.0 - smoothstep(0.0, 0.008, ad);
glow = glow + core * shadeTentacle(p, t) * adv * 10.0 * u.translucency;
}
let gg = gonadGlow(q, t);
glow = glow + gg * vec3f(1.0, 0.62, 0.86) * adv * 0.7 * u.translucency;
if (hitT < 0.0 && bell < 0.004) {
hitT = tt;
}
tt = tt + adv;
if (tt > 14.0) { break; }
}
var col = glow;
if (hitT > 0.0) {
let p = ro + rd * hitT;
let q = vec3f(
p.x + sin(t * 0.6) * 0.06,
p.y - sin(t * 1.1) * 0.12,
p.z + cos(t * 0.5) * 0.05
);
let e = 0.008;
let n = normalize(vec3f(
sdBell(q + vec3f(e, 0.0, 0.0), t) - sdBell(q - vec3f(e, 0.0, 0.0), t),
sdBell(q + vec3f(0.0, e, 0.0), t) - sdBell(q - vec3f(0.0, e, 0.0), t),
sdBell(q + vec3f(0.0, 0.0, e), t) - sdBell(q - vec3f(0.0, 0.0, e), t)
));
let fres = pow(1.0 - max(dot(n, -rd), 0.0), 3.0);
let alpha = clamp((0.05 + 0.95 * fres) * u.translucency, 0.0, 1.0);
let bellCol = shadeBell(p, n, rd, t);
col = mix(glow, bellCol + glow * 0.5, alpha);
}
return col;
}
fn hash21(p: vec2f) -> f32 {
var p3 = fract(vec3f(p.xyx) * 0.1031);
p3 = p3 + dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
// Deep-water gradient plus drifting marine snow. The bokeh control grows the
// out-of-focus discs of the particles rather than blurring the jellyfish, so
// the subject stays sharp and only the background goes soft.
fn background(uv: vec2f, t: f32) -> vec3f {
let r = length(uv);
var col = mix(vec3f(0.010, 0.028, 0.062), vec3f(0.0, 0.006, 0.020), smoothstep(0.0, 1.4, r));
col = col + vec3f(0.025, 0.06, 0.12) * pow(max(0.0, 1.0 - r * 0.65), 3.0);
let disc = 0.045 + u.bokeh * 0.10;
var snow = vec3f(0.0);
for (var i = 0; i < 4; i = i + 1) {
let z = f32(i) + 0.6;
let scale = 6.0 + z * 5.0;
let drift = vec2f(sin(t * 0.12 + z) * 0.4, -t * (0.04 + z * 0.03));
let p = uv * scale + drift;
let id = floor(p);
let f = fract(p) - 0.5;
let h = hash21(id + z * 7.0);
let d = length(f);
let glow = smoothstep(disc, 0.0, d) * step(0.6, h);
let twinkle = 0.6 + 0.4 * sin(t * 1.5 + h * 40.0);
snow = snow + vec3f(0.5, 0.72, 0.95) * glow * twinkle * (0.30 / z);
}
return col + snow;
}
@fragment
fn fs_main(@builtin(position) fc: vec4f) -> @location(0) vec4f {
var uv = (fc.xy - 0.5 * u.resolution) / u.resolution.y;
uv = vec2f(uv.x, -uv.y);
let t = u.time * u.speed;
uv = uv + (u.mouse - 0.5) * 0.06;
let bg = background(uv, t);
let jelly = renderRay(uv, t);
var col = bg + jelly * u.glow;
col = col * (1.0 - 0.35 * smoothstep(0.55, 1.4, length(uv)));
col = col / (col + vec3f(0.75));
col = pow(max(col, vec3f(0.0)), vec3f(0.4545));
return vec4f(col, 1.0);
}
Properties
#webgpu#jellyfish#raymarching#volumetric#translucency#bokeh#creatures
Author: Math Art Core Target: 60 FPS