// Star Point Spread Function shader - billboarded quads with scintillation
shader_type spatial;
render_mode blend_add, depth_draw_never, cull_disabled, unshaded;
uniform sampler2D psf_texture : filter_linear_mipmap;
uniform float scintillation_strength : hint_range(0.0, 1.0) = 0.3;
uniform float brightness_mult : hint_range(0.1, 20.0) = 3.0;
uniform float size_mult : hint_range(0.1, 5.0) = 1.0;
// Moon occlusion — stars behind the moon disk should not render
uniform vec3 moon_direction = vec3(0.0, 1.0, 0.0);
uniform float moon_angular_size = 0.07;
// Per-instance: INSTANCE_CUSTOM.x = magnitude, .y = B-V color, .z = random seed
varying vec3 world_dir;
void vertex() {
// Pass world direction from camera to star for moon occlusion
vec3 star_world_pos = (MODEL_MATRIX * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
world_dir = normalize(star_world_pos - (INV_VIEW_MATRIX * vec4(0.0, 0.0, 0.0, 1.0)).xyz);
// Billboard: face camera always
MODELVIEW_MATRIX = VIEW_MATRIX * mat4(
INV_VIEW_MATRIX[0],
INV_VIEW_MATRIX[1],
INV_VIEW_MATRIX[2],
MODEL_MATRIX[3]
);
// Scale by magnitude (brighter = larger quad)
// Minimum 0.15 prevents sub-pixel quads that cause sparkle artifacts
float mag = INSTANCE_CUSTOM.x;
float size_scale = max(0.15, (7.0 - mag) / 7.0);
VERTEX *= size_scale * size_mult;
}
void fragment() {
// Moon occlusion: discard stars behind the moon disk
float moon_dot = dot(normalize(world_dir), normalize(moon_direction));
if (moon_dot > cos(moon_angular_size * 1.5)) discard;
// PSF texture (gaussian core + optional spikes)
float psf = texture(psf_texture, UV).r;
// B-V to RGB color (smooth interpolation instead of hard bands)
float bv = INSTANCE_CUSTOM.y;
vec3 star_color;
if (bv < 0.0) {
star_color = mix(vec3(0.6, 0.7, 1.0), vec3(0.8, 0.85, 1.0), clamp(bv + 0.5, 0.0, 1.0) * 2.0);
} else if (bv < 0.5) {
star_color = mix(vec3(0.8, 0.85, 1.0), vec3(1.0, 0.95, 0.8), bv * 2.0);
} else if (bv < 1.0) {
star_color = mix(vec3(1.0, 0.95, 0.8), vec3(1.0, 0.7, 0.4), (bv - 0.5) * 2.0);
} else {
star_color = vec3(1.0, 0.7, 0.4);
}
// Scintillation — slow gentle twinkle (1.5-3.5 Hz), not rapid sparkle
float seed = INSTANCE_CUSTOM.z;
float twinkle = sin(TIME * (1.5 + seed * 2.0) + seed * 100.0) * 0.5 + 0.5;
// Second slower harmonic for organic variation
twinkle += sin(TIME * (0.7 + seed * 0.5) + seed * 50.0) * 0.2;
twinkle = clamp(twinkle, 0.0, 1.0);
twinkle = mix(1.0, twinkle, scintillation_strength);
// Brightness from magnitude — boosted for additive blending visibility
float mag_val = INSTANCE_CUSTOM.x;
float brightness = clamp(pow(10.0, (1.0 - mag_val) * 0.4) * 0.3, 0.0, 3.0) * brightness_mult;
ALBEDO = star_color * psf * brightness * twinkle;
ALPHA = psf * twinkle * brightness;
}