438

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
ESC
↑↓ Navigate ↵ Select
113 Mathematical Artworks
Made by Fazley Rabbi
☕

Support the Project

Keep mathematical creative coding alive & open source

Or via Direct Payoneer ($0 Fee)
Payoneer Customer ID: $0 Fee Direct
24076084

💡 Payoneer app: Go to Pay → Pay to recipient → Enter ID 24076084.

Thank you for supporting generative mathematical animations! ✨