Galaxy Generator Study

GLSL shader by guinetik · created 2026-03-06 · 10s loop · 2 passes

Procedural galaxy renderer using the ring-loop technique by BigWIngs. Overlapping rotated elliptical orbits with Keplerian motion. Added different galaxy morphologies: spiral, barred spiral, elliptical, lenticular, irregular. They generate every 7 secs.

Tags: Procedural, Space, Galaxy, Generative, Astronomy, Artofcode, Spiralgalaxy

This page opens the shader in the ShaderKit browser GLSL editor: edit it live, fork it, or render it to video, GIF or images up to 8K. Also available as a full-screen view and an embeddable player.

Shader source (GLSL)

Common

/**
 * 2D hash — maps a vec2 to a pseudo-random float in [0, 1).
 */
float hashN2(vec2 p) {
    float h = dot(p, vec2(127.1, 311.7));
    return fract(sin(h) * 43758.5453123);
}

/**
 * 2D value noise with Hermite interpolation.
 *
 * @param p  2D position to sample
 * @return Noise value in [0, 1)
 */
float valueNoise2D(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);
    return mix(mix(hashN2(i + vec2(0.0, 0.0)), hashN2(i + vec2(1.0, 0.0)), u.x),
               mix(hashN2(i + vec2(0.0, 1.0)), hashN2(i + vec2(1.0, 1.0)), u.x), u.y);
}

/**
 * Galaxy Generator Library
 * @author guinetik
 * @date 2026-03-05
 *
 * Provides Galaxy struct and polymorphic renderGalaxy() dispatcher.
 * Strategy pattern: render implementation varies by galaxy type.
 *
 * TECHNIQUE: Overlapping Rotated Elliptical Orbits (Megaparsecs)
 * A galaxy is rendered as many concentric elliptical rings, each slightly
 * rotated. Spiral structure emerges from cumulative rotation (twist param).
 * Inner rings are more elongated and orbit faster (Keplerian).
 * Procedural valueNoise2D provides dust detail.
 *
 * Based on "Megaparsecs" by Martijn Steinrucken (BigWings), CC BY-NC-SA 3.0.
 * Each type renderer is independently replaceable — today all delegate to
 * renderRingLoop(), but any can be swapped for a different technique later.
 *
 * Requires: noise-value common (valueNoise2D, hashN2)
 */

#ifndef _GAL_TAU
#define _GAL_TAU 6.2831853
#endif

// ─────────────────────────────────────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────────────────────────────────────

#define GAL_MAX_RADIUS 1.5              // Early-out distance (in tilted UV space)
#define GAL_MIN_COS_TILT 0.15           // Minimum cos(tilt) — clamps max edge-on stretch
#define GAL_RING_PHASE_OFFSET 100.0     // Per-ring orbital phase spread
#define GAL_ORBIT_SPEED 0.1             // Time multiplier for orbital motion
#define GAL_DUST_UV_SCALE 0.2           // UV scale for dust sampling
#define GAL_DUST_NOISE_FREQ 4.0         // Noise frequency multiplier
#define GAL_STAR_GLOW_RADIUS 0.5        // Smoothstep falloff for star points
#define GAL_STAR_BRIGHTNESS 0.2         // Star point intensity
#define GAL_SUPERNOVA_THRESH 0.9999     // sin() threshold for supernova flash
#define GAL_SUPERNOVA_MULT 10.0         // Supernova brightness boost
#define GAL_INNER_RADIUS 0.1            // Innermost ring radius (normalized)
#define GAL_OUTER_RADIUS 1.0            // Outermost ring radius (normalized)
#define GAL_MAX_RINGS 25                // Fixed upper bound for ring loop (integer)
#define GAL_RING_DECORR_A 563.2         // Ring-to-ring decorrelation seed A
#define GAL_RING_DECORR_B 673.2         // Ring-to-ring decorrelation seed B
#define GAL_STAR_OFFSET_A 17.3          // Star ID offset multiplier (decorrelation)
#define GAL_STAR_OFFSET_B 31.7          // Star ID offset multiplier (decorrelation)
#define GAL_TWINKLE_FREQ 784.0          // Star twinkle oscillation frequency
#define GAL_SUPERNOVA_TIME_SCALE 0.05   // Supernova pulse time multiplier (slow)
#define GAL_STAR_COLOR_FREQ 100.0       // Star color variation frequency

// ─────────────────────────────────────────────────────────────────────────────
// UTILITIES
// ─────────────────────────────────────────────────────────────────────────────

/** 2D rotation matrix (galaxy-local to avoid name clashes) */
mat2 _galRot(float a) {
  float s = sin(a), c = cos(a);
  return mat2(c, -s, s, c);
}

// ─────────────────────────────────────────────────────────────────────────────
// DATA STRUCTURES
// ─────────────────────────────────────────────────────────────────────────────

/** Galaxy entity with morphology and physical parameters */
struct Galaxy {
  int type;            // 0=spiral, 1=barred, 2=elliptical, 3=lenticular, 4=irregular
  uint seed;           // deterministic randomness
  vec2 center;         // center position in screen pixels
  float scale;         // radius in pixels (galaxy extends to this distance)
  float angleX;        // tilt angle — fake 3D via UV compression
  float angleY;        // secondary tilt (reserved for future use)
  float angleZ;        // in-plane rotation angle
  vec3 color;          // base tint color
  float axialRatio;    // b/a elongation (0.3–1.0, from DB axial_ratio)
  float mass_log10;    // log10 stellar mass (9–12, from DB)
  float velocity_kmps; // CMB velocity km/s (reserved for future use)
  float distance_mpc;  // distance in Mpc (reserved for future use)
  float time;          // animation time (caller passes g.time)
};

/**
 * Ring-loop rendering style parameters.
 * Each galaxy type constructs its own GalaxyStyle to drive the ring loop.
 */
struct GalaxyStyle {
  float twist;         // Spiral winding per ring. 0.0=no arms, 1.0=classic spiral, 1.5+=tight.
  float innerStretch;  // Inner ring X elongation. 1.0=circular, 3.5=strong bar.
  float ringWidth;     // Gaussian sharpness of rings. 8=diffuse, 25=tight bands.
  float numRings;      // Ring count. 15–25 range. More=smoother, slower.
  float diskThickness; // Ring-to-ring Y perturbation amplitude. 0.01–0.1.
  float bulgeSize;     // Center glow Gaussian tightness. Higher=smaller bulge.
  float bulgeBright;   // Center glow intensity. 0.5–2.0.
  float dustContrast;  // Dust pow() exponent. Lower=softer, higher=sharper.
  float starDensity;   // Star grid resolution. 4–12. More=denser star field.
};

// ─────────────────────────────────────────────────────────────────────────────
// SHARED HELPERS
// ─────────────────────────────────────────────────────────────────────────────

/**
 * 3D tilt via UV Y-stretch (ray-plane intersection approximation).
 *
 * TECHNIQUE: When viewing a tilted disk, screen-space Y maps to disk-space
 * positions that are FARTHER apart (not closer). A point 0.3 above center
 * on screen corresponds to a point 0.6 on the disk if tilted 60 degrees.
 * This stretches Y so points off the disk plane map to large UV distances
 * where the ring Gaussian is near zero — creating natural thin edge-on shapes.
 *
 * cos(0)=1.0 → face-on (no stretch). cos(PI/2)→0 → edge-on (max stretch).
 * Clamped to GAL_MIN_COS_TILT to prevent infinite stretch at exactly 90 degrees.
 */
vec2 _galApplyTilt(vec2 uv, float angleX) {
  uv.y /= max(abs(cos(angleX)), GAL_MIN_COS_TILT);
  return uv;
}

/**
 * Gaussian center glow (galaxy bulge/core).
 * Returns warm-tinted radial glow at UV origin.
 */
vec3 _galRenderBulge(vec2 uv, float size, float brightness, vec3 tint) {
  return vec3(exp(-0.5 * dot(uv, uv) * size)) * brightness * tint;
}

/**
 * Core ring-loop renderer — Megaparsecs technique.
 *
 * TECHNIQUE: Overlapping Rotated Elliptical Orbits
 * For NUM_RINGS concentric rings at increasing radius:
 * 1. Rotate UV by (i * TAU * twist) — creates spiral from overlap
 * 2. Stretch inner rings (creates bar/elongation)
 * 3. Gaussian brightness at ring radius
 * 4. Procedural noise for dust detail
 * 5. Grid-based point stars with twinkle + supernova
 * 6. Inner rings orbit faster (Keplerian: phase / radius)
 *
 * @param g     Galaxy (seed, color used for dust tint and rotation direction)
 * @param uv    Normalized UV centered at galaxy, roughly [-1, 1]
 * @param style Type-specific ring parameters
 * @return HDR color (may exceed 1.0, caller handles tonemapping)
 */
vec3 _galRenderRingLoop(Galaxy g, vec2 uv, GalaxyStyle style) {
  vec3 col = vec3(0.0);

  // Dust base color: bright blue-white (Megaparsecs original).
  // Galaxy color is applied as a post-multiply tint by each type renderer.
  vec3 dustCol = vec3(0.3, 0.6, 1.0);

  float flip = 1.0;
  float t = g.time * GAL_ORBIT_SPEED;
  // Seed-based rotation direction (clockwise vs counter-clockwise)
  t *= (float(g.seed % 2u) * 2.0 - 1.0);

  for (int j = 0; j < GAL_MAX_RINGS; j++) {
    float i = float(j) / style.numRings;
    if (i >= 1.0) break;
    flip *= -1.0;

    // Ring-to-ring Y perturbation (disk thickness, decorrelates rings)
    float z = mix(style.diskThickness, 0.0, i) * flip * fract(sin(i * GAL_RING_DECORR_A) * GAL_RING_DECORR_B);

    // Ring radius: inner to outer
    float r = mix(GAL_INNER_RADIUS, GAL_OUTER_RADIUS, i);

    // Slight UV perturbation from disk thickness
    vec2 ringUv = uv + vec2(0.0, z * 0.5);

    // Spiral twist: progressive rotation per ring
    vec2 st = ringUv * _galRot(i * _GAL_TAU * style.twist);

    // Inner ring elongation (bar effect on inner, circular on outer)
    st.x *= mix(style.innerStretch, 1.0, i);

    // Ring brightness: Gaussian peak at radius r
    float ell = exp(-0.5 * abs(dot(st, st) - r) * style.ringWidth);

    // Orbital motion UV — inner rings orbit faster (Kepler: t/r)
    vec2 texUv = GAL_DUST_UV_SCALE * st * _galRot(i * GAL_RING_PHASE_OFFSET + t / r);

    // Dust detail: procedural noise (replaces Megaparsecs texture lookup)
    vec3 dust = vec3(valueNoise2D((texUv + vec2(i)) * GAL_DUST_NOISE_FREQ));

    // Combined brightness with contrast shaping
    vec3 dL = pow(max(ell * dust / r, vec3(0.0)), vec3(0.5 + style.dustContrast));

    // Accumulate dust contribution
    col += dL * dustCol;

    // === Point Stars ===
    vec2 starId = floor(texUv * style.starDensity);
    vec2 starUv = fract(texUv * style.starDensity) - 0.5;
    float n = hashN2(starId + vec2(i * GAL_STAR_OFFSET_A, i * GAL_STAR_OFFSET_B));
    float starDist = length(starUv);

    // Star glow: bright point with 1/distance falloff
    float sL = smoothstep(GAL_STAR_GLOW_RADIUS, 0.0, starDist)
             * pow(max(dL.r, 0.0), 2.0) * GAL_STAR_BRIGHTNESS
             / max(starDist, 0.001);

    // Twinkle + rare supernova
    float sN = sL;
    sL *= sin(n * GAL_TWINKLE_FREQ + g.time) * 0.5 + 0.5;
    sL += sN * smoothstep(GAL_SUPERNOVA_THRESH, 1.0, sin(n * GAL_TWINKLE_FREQ + g.time * GAL_SUPERNOVA_TIME_SCALE))
        * GAL_SUPERNOVA_MULT;

    // Add stars (skip innermost rings to avoid center clutter)
    if (i > 3.0 / style.starDensity) {
      // Star color: mix galaxy tint with hot white (bright stars are whiter)
      vec3 starCol = mix(dustCol, vec3(1.0), 0.3 + n * 0.5);
      col += sL * starCol;
    }
  }

  // Normalize accumulated brightness by ring count
  col /= style.numRings;

  return col;
}

// ─────────────────────────────────────────────────────────────────────────────
// TYPE-SPECIFIC RENDERERS
// Each type owns its rendering. Today all call _galRenderRingLoop() with
// type-specific GalaxyStyle. Any renderer can be rewritten independently
// with a completely different technique without touching the others.
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Render spiral galaxy (type 0).
 * Classic 2-armed spiral: moderate twist, inner elongation, visible arms.
 */
vec3 renderSpiral(Galaxy g, vec2 fragCoord) {
  vec2 uv = (fragCoord - g.center) / g.scale;
  uv = _galApplyTilt(uv * _galRot(g.angleZ), g.angleX);
  if (length(uv) > GAL_MAX_RADIUS) return vec3(0.0);

  GalaxyStyle s;
  s.twist        = 1.0;
  s.innerStretch = mix(1.8, 2.2, g.axialRatio);
  s.ringWidth    = 15.0;
  s.numRings     = 20.0;
  s.diskThickness = 0.04;
  s.bulgeSize    = 25.0;
  s.bulgeBright  = 1.2;
  s.dustContrast = 0.5;
  s.starDensity  = 8.0;

  vec3 col = _galRenderRingLoop(g, uv, s);
  col += _galRenderBulge(uv, s.bulgeSize, s.bulgeBright,
           mix(vec3(1.0, 0.9, 0.8), g.color, 0.6));
  col *= g.color;
  return col;
}

/**
 * Render barred spiral galaxy (type 1).
 * Strong inner bar (high stretch), arms emerge from bar ends.
 */
vec3 renderBarredSpiral(Galaxy g, vec2 fragCoord) {
  vec2 uv = (fragCoord - g.center) / g.scale;
  uv = _galApplyTilt(uv * _galRot(g.angleZ), g.angleX);
  if (length(uv) > GAL_MAX_RADIUS) return vec3(0.0);

  GalaxyStyle s;
  s.twist        = 1.3;
  s.innerStretch = mix(3.0, 4.0, g.axialRatio);
  s.ringWidth    = 12.0;
  s.numRings     = 20.0;
  s.diskThickness = 0.04;
  s.bulgeSize    = 20.0;
  s.bulgeBright  = 1.0;
  s.dustContrast = 0.5;
  s.starDensity  = 8.0;

  vec3 col = _galRenderRingLoop(g, uv, s);
  col += _galRenderBulge(uv, s.bulgeSize, s.bulgeBright,
           mix(vec3(1.0, 0.9, 0.7), g.color, 0.6));
  col *= g.color;
  return col;
}

/**
 * Render elliptical galaxy (type 2).
 * No twist, smooth round glow, bright bulge, minimal dust.
 * Replaceable later with Sersic profile or volumetric technique.
 */
vec3 renderElliptical(Galaxy g, vec2 fragCoord) {
  vec2 uv = (fragCoord - g.center) / g.scale;
  uv = _galApplyTilt(uv * _galRot(g.angleZ), g.angleX);
  if (length(uv) > GAL_MAX_RADIUS) return vec3(0.0);

  GalaxyStyle s;
  s.twist        = 0.0;
  s.innerStretch = mix(1.0, 1.4, 1.0 - g.axialRatio);
  s.ringWidth    = 8.0;
  s.numRings     = 15.0;
  s.diskThickness = 0.08;
  s.bulgeSize    = 15.0;
  s.bulgeBright  = 2.0;
  s.dustContrast = 0.8;
  s.starDensity  = 4.0;

  vec3 col = _galRenderRingLoop(g, uv, s);
  col += _galRenderBulge(uv, s.bulgeSize, s.bulgeBright,
           mix(vec3(1.0, 0.8, 0.6), g.color, 0.7));
  col *= g.color;
  return col;
}

/**
 * Render lenticular galaxy (type 3).
 * Very thin disk (tight rings), bright dominant bulge, nearly no arms.
 * Replaceable later with disk+bulge decomposition.
 */
vec3 renderLenticular(Galaxy g, vec2 fragCoord) {
  vec2 uv = (fragCoord - g.center) / g.scale;
  uv = _galApplyTilt(uv * _galRot(g.angleZ), g.angleX);
  if (length(uv) > GAL_MAX_RADIUS) return vec3(0.0);

  GalaxyStyle s;
  s.twist        = 0.05;
  s.innerStretch = mix(1.5, 2.0, 1.0 - g.axialRatio);
  s.ringWidth    = 20.0;
  s.numRings     = 18.0;
  s.diskThickness = 0.02;
  s.bulgeSize    = 30.0;
  s.bulgeBright  = 1.5;
  s.dustContrast = 0.6;
  s.starDensity  = 6.0;

  vec3 col = _galRenderRingLoop(g, uv, s);
  col += _galRenderBulge(uv, s.bulgeSize, s.bulgeBright,
           mix(vec3(1.0, 0.85, 0.65), g.color, 0.6));
  col *= g.color;
  return col;
}

/**
 * Render irregular galaxy (type 4).
 * Moderate twist, high disk thickness, lots of dust — clumpy and chaotic.
 * Replaceable later with clump-based or particle technique.
 */
vec3 renderIrregular(Galaxy g, vec2 fragCoord) {
  vec2 uv = (fragCoord - g.center) / g.scale;
  uv = _galApplyTilt(uv * _galRot(g.angleZ), g.angleX);
  if (length(uv) > GAL_MAX_RADIUS) return vec3(0.0);

  GalaxyStyle s;
  s.twist        = 0.3;
  s.innerStretch = 1.5;
  s.ringWidth    = 10.0;
  s.numRings     = 16.0;
  s.diskThickness = 0.1;
  s.bulgeSize    = 40.0;
  s.bulgeBright  = 0.6;
  s.dustContrast = 0.4;
  s.starDensity  = 10.0;

  vec3 col = _galRenderRingLoop(g, uv, s);
  col += _galRenderBulge(uv, s.bulgeSize, s.bulgeBright,
           mix(vec3(0.9, 0.85, 1.0), g.color, 0.6));
  col *= g.color;
  return col;
}

// ─────────────────────────────────────────────────────────────────────────────
// POLYMORPHIC DISPATCHER
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Render galaxy by dispatching to type-specific renderer.
 *
 * @param g         Galaxy to render
 * @param fragCoord Fragment coordinate (screen pixels)
 * @return Color contribution (HDR)
 */
vec3 renderGalaxy(Galaxy g, vec2 fragCoord) {
  switch(g.type) {
    case 0: return renderSpiral(g, fragCoord);
    case 1: return renderBarredSpiral(g, fragCoord);
    case 2: return renderElliptical(g, fragCoord);
    case 3: return renderLenticular(g, fragCoord);
    case 4: return renderIrregular(g, fragCoord);
  }
  return vec3(0.0);
}

Buffer A (iChannel0)

/**
 * Galaxy Generator — Grid Layout
 * @author guinetik
 * @date 2026-03-05
 *
 * Renders a 3x3 grid of galaxies, cycling through 5 types every 7 seconds.
 * Uses galaxy.glsl library for polymorphic ring-loop rendering.
 * Each galaxy gets randomized orientation, color, and physical params.
 */

#define GRID_COLS 3
#define GRID_ROWS 3
#define CYCLE_DURATION 7.0          // Seconds per galaxy set
#define GALAXY_FILL 0.35            // Galaxy radius as fraction of cell size

// ─────────────────────────────────────────────────────────────────────────────
// UTILITIES
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Integer hash for deterministic pseudo-random numbers.
 * PCG-style — better distribution than sin-hash for seed-based generation.
 */
float _gridHash(uint x) {
  x = ((x >> 16u) ^ x) * 0x7feb352du;
  x = ((x >> 15u) ^ x) * 0x846ca68bu;
  return float((x >> 16u) ^ x) / 4294967296.0;
}

/** Hash with seed + offset for multiple independent random values */
float _gridHashSeed(uint seed, uint offset) {
  return _gridHash(seed + offset);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  vec3 col = vec3(0.0);

  // Grid cell dimensions
  vec2 cellSize = iResolution.xy / vec2(float(GRID_COLS), float(GRID_ROWS));
  float galaxyRadius = min(cellSize.x, cellSize.y) * GALAXY_FILL;

  int typeIndex = 0;
  for (int y = 0; y < GRID_ROWS; y++) {
    for (int x = 0; x < GRID_COLS; x++) {
      // Cell center in screen pixels
      vec2 cellCenter = vec2(
        (float(x) + 0.5) * cellSize.x,
        (float(y) + 0.5) * cellSize.y
      );

      // Early-out: skip if fragment is far from this galaxy
      if (length(fragCoord - cellCenter) > galaxyRadius * GAL_MAX_RADIUS) {
        typeIndex++;
        continue;
      }

      // Deterministic seed per galaxy per cycle
      uint cycleSeed = uint(int(iTime / CYCLE_DURATION)) * 12345u + uint(typeIndex);

      // Build Galaxy
      Galaxy g;
      g.type       = typeIndex % 5;
      g.seed       = cycleSeed;
      g.center     = cellCenter;
      g.scale      = galaxyRadius;
      g.time       = iTime;

      // Orientation
      g.angleX     = _gridHashSeed(cycleSeed, 1u) * _GAL_TAU;
      g.angleY     = _gridHashSeed(cycleSeed, 2u) * _GAL_TAU;
      g.angleZ     = _gridHashSeed(cycleSeed, 3u) * _GAL_TAU;

      // Color tint — bright values with type-specific hue bias.
      // Applied as post-multiply on the bright blue-white dust rendering.
      // All tints stay on the stellar blackbody sequence: red-orange-yellow-white-blue.
      // No green — blackbody peak at green wavelengths produces perceived white.
      float h1 = _gridHashSeed(cycleSeed, 4u);
      int gtype = typeIndex % 5;
      if (gtype == 0) {
        // Spiral: cool blue tint (young O/B star population)
        g.color = mix(vec3(0.7, 0.8, 1.0), vec3(0.85, 0.85, 1.0), h1);
      } else if (gtype == 1) {
        // Barred spiral: warm gold tint (older bar + blue arms)
        g.color = mix(vec3(1.0, 0.8, 0.5), vec3(1.0, 0.9, 0.65), h1);
      } else if (gtype == 2) {
        // Elliptical: red-orange tint (old K/M star population)
        g.color = mix(vec3(1.0, 0.55, 0.3), vec3(1.0, 0.75, 0.45), h1);
      } else if (gtype == 3) {
        // Lenticular: warm yellow-white tint (transitional population)
        g.color = mix(vec3(1.0, 0.7, 0.45), vec3(1.0, 0.85, 0.6), h1);
      } else {
        // Irregular: blue to pink-magenta tint (starburst + HII emission)
        g.color = mix(vec3(0.65, 0.7, 1.0), vec3(1.0, 0.55, 0.7), h1);
      }

      // Physical parameters (from DB schema)
      g.axialRatio    = 0.3 + _gridHashSeed(cycleSeed, 7u) * 0.7;
      g.mass_log10    = 9.0 + _gridHashSeed(cycleSeed, 8u) * 3.0;
      g.velocity_kmps = 3000.0 + _gridHashSeed(cycleSeed, 9u) * 6000.0;
      g.distance_mpc  = 10.0 + _gridHashSeed(cycleSeed, 10u) * 90.0;

      // Render
      col += renderGalaxy(g, fragCoord);

      typeIndex++;
    }
  }

  fragColor = vec4(col, 1.0);
}

Image

Not used

More shaders by guinetik

Browse all public shaders · All shaders by guinetik · ShaderKit home

Vibe Mode BETA
💰 ~0 credits
Uniforms
FPS: 0
Time: 0
Resolution: 0 x 0

Account

FPS: -- Time: -- --×-- 1x