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
- Black Hole Sepia
- Caustic Study #04: Beach
- Nothern Lights
- Quantum Orbitals
- 24h in Arrakis
- signal/noise
Browse all public shaders · All shaders by guinetik · ShaderKit home
Unread comments
×Projects with new comments. Click to open and read.