GLSL Starfield Rendering with Hash Point Distribution
Rendering a realistic, procedural night sky starfield in GLSL relies on dividing space into a regular grid and applying deterministic hash functions to position and style stars within each cell. This article explains how hash-based point distribution operates, covering coordinate space partitioning, pseudo-random value generation, neighboring cell boundary management, and visual enhancements such as brightness falloff, twinkling, and color variation.
Spatial Partitioning
To generate an infinite starfield without memory-heavy particle systems or textures, screen-space UV coordinates or 3D celestial sphere directions are subdivided into a uniform grid.
Using floor(), continuous space is segmented into
discrete integer coordinates representing cell IDs. Using
fract(), the local coordinate within each cell is
extracted, running from 0.0 to 1.0.
vec2 gridId = floor(uv * scale);
vec2 gridUv = fract(uv * scale);Each cell acts as an independent bounding area that contains one or more pseudo-randomly placed stars.
Hash Functions for Deterministic Randomness
Standard GLSL lacks stateful random number generators. Instead,
deterministic hash functions map the integer gridId to
consistent pseudo-random values. A commonly used 2D-to-2D hash function
leverages high-frequency trigonometric functions:
vec2 hash22(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}Passing the gridId into this function produces a fixed
vector between (0.0, 0.0) and (1.0, 1.0). This
vector serves as the star’s offset within that cell, ensuring that the
star remains locked to that coordinate frame across multiple frames.
Neighbor Traversal for Seamless Boundaries
If a star's randomized position lies near the edge of a cell, its circular glow will cross into neighboring cells. If a fragment only samples its own cell, the star will be clipped abruptly at the boundary.
To resolve this, the shader iterates through a \(3 \times 3\) grid of adjacent cells (or \(3 \times 3 \times 3\) in 3D sphere mapping). For each neighboring offset:
- The neighbor's
gridIdis calculated (gridId + offset). - The neighbor's ID is hashed to determine the star’s position relative to the current fragment.
- The Euclidean distance from the fragment to that star is evaluated.
float stars = 0.0;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
vec2 offset = vec2(float(x), float(y));
vec2 starPos = hash22(gridId + offset);
// Vector from current fragment to star center
vec2 delta = offset + starPos - gridUv;
float dist = length(delta);
// Star shape and intensity falloff
stars += smoothstep(0.05, 0.0, dist);
}
}Intensity Falloff and Glow
A linear disc creates harsh, artificial dots. Real stars exhibit intense cores with rapid exponential or inverse-distance light decay.
Light decay can be modeled using an inverse curve or smoothstep falloff:
float star = 0.005 / (dist + 0.001);
star = smoothstep(0.8, 1.0, star);Alternatively, multiplying a sharp core
(smoothstep(radius, 0.0, dist)) with a softer, wider
falloff (pow(max(1.0 - dist / glowRadius, 0.0), 3.0))
creates a balanced balance between faint stars and prominent focal
stars.
Twinkle and Color Modulation
To add dynamism and celestial variety, additional hash values generated from the cell ID modulate brightness and tint:
Size and Brightness Variation: A single float hash scales the star radius and intensity, creating a natural mix of faint background stars and rare bright ones using power distributions (e.g.,
pow(hash.x, 8.0)).Twinkling: Modulate the output over time with a sinusoidal wave whose frequency and phase are offset by a hash:
float twinkle = sin(u_time * (starRand.x * 5.0 + 2.0) + starRand.y * 6.2831); intensity *= 0.7 + 0.3 * twinkle;Color Temperature: Blending between blue, white, and red-orange based on another hash value replicates real-world spectral classifications.
Layering Across Scales
A single grid layer creates stars of uniform density. Realistic starfields combine two or three distinct layers at different scales:
- Dense, fine layer: High grid scale, small radii, faint intensities to simulate distant, unresolved star fields.
- Medium layer: Balanced scale for standard visible constellations.
- Sparse layer: Low grid scale, large stars with broad bloom profiles and prominent twinkle effects.
Accumulating these layers produces a deep, non-repeating procedural night sky directly inside the fragment shader with minimal performance cost.