// Copyright (c) 2023-2025 Cory Petkovsek and Contributors
// Copyright (c) 2021 J. Cuellar
shader_type sky;
render_mode use_debanding;
group_uniforms general;
uniform bool sky_visible = true;
uniform vec2 color_correction = vec2(0.0, 1.0);
uniform vec4 ground_color: source_color = vec4(0.3, 0.3, 0.3, 1.0);
uniform float horizon_offset = 0.0;
group_uniforms atmosphere;
uniform float atm_darkness = 0.5;
uniform float atm_sun_intensity = 18.0;
uniform vec4 atm_day_tint: source_color = vec4(0.808, 0.91, 1.0, 1.0);
uniform vec4 atm_horizon_light_tint: source_color = vec4(0.98, 0.635, 0.463, 1.0);
uniform vec4 atm_night_tint: source_color = vec4 (0.041, 0.049, 0.061, 0.245);
uniform vec3 atm_level_params = vec3(1.0, 0.0, 0.0);
uniform float atm_thickness = 0.7;
uniform vec3 atm_beta_ray = vec3(0.00001, 0.00001, 0.00003);
uniform vec3 atm_beta_mie = vec3(0.00000003038); // Value computed by multiplying Mie, turbidity, and 0.000434.
group_uniforms sun_disk;
uniform vec4 sun_light_color: source_color = vec4(1.0);
uniform vec4 sun_disk_color: source_color = vec4(0.996, 0.541, 0.14, 1.0);
uniform float sun_disk_intensity = 2.0;
uniform float sun_disk_size = 0.015;
group_uniforms sun_mie;
uniform vec4 atm_sun_mie_tint: source_color = vec4(1.0);
uniform float atm_sun_mie_intensity = 1.0;
uniform vec3 atm_sun_partial_mie_phase = vec3(0.36, 1.64, 1.6);
group_uniforms moon;
uniform vec4 moon_color: source_color = vec4(1.0);
uniform sampler2D moon_texture: source_color, repeat_disable;
uniform vec3 moon_texture_alignment = vec3(7.0, 1.4, 4.8);
uniform bool moon_texture_flip_u;
uniform bool moon_texture_flip_v;
uniform float moon_size = 0.07;
// Moon phase override: -1.0 = auto (use sun position), 0.0 = new moon, 0.5 = full, 1.0 = new again
uniform float moon_phase_override : hint_range(-1.0, 1.0) = -1.0;
// Moon disk brightness multiplier — reflects sunlight, so should scale with sun energy
uniform float moon_intensity : hint_range(0.0, 10.0) = 2.0;
group_uniforms moon_mie;
uniform vec4 atm_moon_mie_tint: source_color = vec4(0.137, 0.184, 0.292, 1.0);
uniform float atm_moon_mie_intensity = 0.17152;
uniform vec3 atm_moon_partial_mie_phase = vec3(0.36, 1.64, 1.6);
group_uniforms star_map;
uniform bool starmap_enabled = false;
uniform vec4 starmap_color: source_color = vec4(0.71, 0.71, 0.71, 0.855);
uniform sampler2D starmap_texture: source_color;
uniform bool starmap_flip_u = false;
uniform bool starmap_flip_v = false;
uniform vec3 starmap_alignment = vec3(2.68288, -0.25891, 0.40101);
uniform float star_rotation_offset = 9.38899;
uniform float star_rotation = 0.0; // For simulating Earth rotation.
uniform float star_tilt = 0.0; // For tilting the sky according to observer latitude in radians. (90° - latitude)
group_uniforms milky_way;
uniform samplerCube milky_way_cubemap : filter_linear, source_color;
uniform float mw_brightness : hint_range(0.0, 5.0) = 1.5;
uniform float mw_rotation : hint_range(0.0, 6.283) = 0.0;
uniform bool mw_enabled = true;
group_uniforms procedural_stars;
uniform bool procedural_stars_enabled = false;
uniform float proc_star_density : hint_range(10.0, 200.0) = 80.0;
uniform float proc_star_brightness : hint_range(0.1, 5.0) = 1.5;
/* Sky alignment values for a star map that is in galactic coordinates. Works for most star maps.
X: 2.6555
Y: -0.23935
Z: 0.4505
These alignment values are consistent with a sky view from the North Pole (latitude zero)
at midnight. The positions of Polaris, Vega, Rigel, and Alpha Centauri confirmed
through various sources and calculations.
*/
// Volumetric cloud uniforms removed — cloud rendering moved to nubis_clouds addon
group_uniforms overlays;
uniform bool show_azimuthal_grid = false;
uniform vec4 azimuthal_grid_color : source_color = vec4(.871, .722, .529, 1.);
uniform float azimuthal_grid_rotation_offset = 0.03;
uniform bool show_equatorial_grid = false;
uniform vec4 equatorial_grid_color : source_color = vec4(0., .75, 1., 1.);
uniform float equatorial_grid_rotation_offset = 0.03;
group_uniforms transform_matrices;
uniform mat3 moon_matrix; // Calculated externally via GDScript.
uniform mat3 star_combined_matrix = mat3(1.0); // Precomputed: alignment * rotation * tilt (avoids 18 trig calls/pixel)
#include "Common.gdshaderinc"
// Provides a transformation matrix when passed a vec3.
mat3 create_rotation_matrix(vec3 r) {
float c1 = cos(r.x), s1 = sin(r.x);
float c2 = cos(r.y), s2 = sin(r.y);
float c3 = cos(r.z), s3 = sin(r.z);
mat3 rotX = mat3(
vec3(1.0, 0.0, 0.0),
vec3(0.0, c1, -s1),
vec3(0.0, s1, c1)
);
mat3 rotY = mat3(
vec3(c2, 0.0, s2),
vec3(0.0, 1.0, 0.0),
vec3(-s2, 0.0, c2)
);
mat3 rotZ = mat3(
vec3(c3, -s3, 0.0),
vec3(s3, c3, 0.0),
vec3(0.0, 0.0, 1.0)
);
return rotZ * rotY * rotX; // Combine rotations in ZYX order
}
vec2 calc_equirect_uv(vec3 norm) {
float flipu = starmap_flip_u ? -1.0 : 1.0;
float flipv = starmap_flip_v ? -1.0 : 1.0;
float u = flipu * ((atan(norm.y, norm.x) + PI) * INV_TAU); // 0 to 1 horizontally
float v = -flipv * (acos(norm.z) * INV_PI); // 0 (north pole) to 1 (south pole)
return vec2(u, v);
}
vec3 sample_moon_texture(vec3 dir) {
dir = moon_matrix * dir * create_rotation_matrix(moon_texture_alignment);
dir.x = -dir.x;
dir.z = -dir.z;
float flipu = moon_texture_flip_u ? 1.0 : -1.0;
float flipv = moon_texture_flip_v ? 1.0 : -1.0;
float u = 0.5 + flipu * atan(dir.z, dir.x) * INV_TAU; // Longitude
float v = 0.5 + flipv * asin(dir.y) * INV_PI; // Latitude
return texture(moon_texture, vec2(u, v)).rgb;
}
float calc_disk_mask(vec3 norm, vec3 coords, lowp float size) {
float dist = length(norm - coords);
return 1.0 - step(size, dist);
}
// For rendering the moon. This function courtesy of Kelvin van Hoorn.
// https://kelvinvanhoorn.com/2024/12/07/skybox-tutorial-godot/
float simple_sphere_intersect(vec3 ray_dir, vec3 sphere_pos, float radius) {
vec3 oc = -sphere_pos;
float b = dot(oc, ray_dir);
float c = dot(oc, oc) - radius * radius;
float h = b * b - c;
float valid = step(0.0, h); // 1.0 if h >= 0.0, else 0.0
h = sqrt(max(h, 0.0)); // Ensure sqrt is non-negative
return mix(-1.0, -b - h, valid);
}
// Procedural Stars
//------------------------------------------------------------------------------
float hash21_star(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
vec2 hash22_star(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);
}
float procedural_star_field(vec3 dir) {
vec2 cell_uv = dir.xz / (abs(dir.y) + 0.001) * proc_star_density;
vec2 cell_id = floor(cell_uv);
vec2 cell_frac = fract(cell_uv);
vec2 star_pos = hash22_star(cell_id);
float d = length(cell_frac - star_pos);
float mag = hash21_star(cell_id + 42.0);
float threshold = 0.02 * (1.0 - mag * 0.8);
float star = smoothstep(threshold, threshold * 0.3, d) * mag;
return star * proc_star_brightness;
}
// Atmosphere
//------------------------------------------------------------------------------
vec3 calc_atmospheric_scatter(float sr, float sm, vec2 mu, vec3 mult) {
vec3 beta_mie = atm_beta_mie;
vec3 beta_ray = atm_beta_ray * atm_thickness;
vec3 extc_factor = clamp_rgb_to_unit(exp(-(beta_ray * sr + beta_mie * sm)));
float extc_ff = mix(clamp_to_unit(atm_thickness * 0.5), 1.0, mult.x);
vec3 final_extc_factor = mix(1.0 - extc_factor, (1.0 - extc_factor) * extc_factor, extc_ff);
float rayleigh_phase = calc_rayleigh_phase(mu.x);
vec3 BRT = beta_ray * rayleigh_phase;
vec3 BMT = beta_mie * calc_mie_phase(mu.x, atm_sun_partial_mie_phase);
BMT *= atm_sun_mie_intensity * atm_sun_mie_tint.rgb;
vec3 BRMT = (BRT + BMT) / (beta_ray + beta_mie);
vec3 scatter = atm_sun_intensity * (BRMT * final_extc_factor) * atm_day_tint.rgb * mult.y;
scatter = mix(scatter, scatter * (1.0 - extc_factor), atm_darkness);
vec3 lcol = mix(atm_day_tint.rgb, atm_horizon_light_tint.rgb, mult.x);
vec3 nscatter = (1.0 - extc_factor) * atm_night_tint.rgb;
nscatter += calc_mie_phase(mu.y, atm_moon_partial_mie_phase) *
atm_moon_mie_tint.rgb * atm_moon_mie_intensity * 0.005;
return (scatter * lcol) + nscatter;
}
// Volumetric cloud functions removed — cloud rendering moved to nubis_clouds addon
// Main
//------------------------------------------------------------------------------
vec3 render_sky(vec3 world_pos, vec3 clouds_pos, vec3 sun_pos, vec3 moon_pos, float p_time) {
vec3 col = vec3(0.0);
vec4 angle_mult;
angle_mult.x = clamp_to_unit(1.0 - sun_pos.y);
angle_mult.y = clamp_to_unit(sun_pos.y + 0.45);
angle_mult.z = clamp_to_unit(-sun_pos.y + 0.30);
angle_mult.w = clamp_to_unit(-sun_pos.y + 0.60);
// Atmosphere
vec2 mu = vec2(dot(sun_pos, world_pos), dot(moon_pos, world_pos));
float sr, sm;
calc_simple_optical_depth(world_pos.y + atm_level_params.z - horizon_offset, sr, sm, atm_level_params.xy);
world_pos.y -= horizon_offset;
float horizonBlend = clamp_to_unit((world_pos.y - 0.03) * 3.0);
vec3 scatter = calc_atmospheric_scatter(sr, sm, mu.xy, angle_mult.xyz);
col.rgb += scatter.rgb;
// Sun
vec3 sun_disk = calc_disk_mask(world_pos, sun_pos, sun_disk_size) * sun_disk_color.rgb * scatter.rgb;
sun_disk *= sun_disk_intensity;
// Deep space + Moon — skip when not visible
// Thin atmosphere allows stars to bleed through during daytime:
// atm_thickness=0.7 (default): no bleed. atm_thickness<0.5: stars visible in daylight.
float atm_star_factor = clamp_to_unit(1.0 - atm_thickness * 1.5);
float star_fade = max(angle_mult.z, atm_star_factor);
vec3 deep_space = vec3(0.0);
if (star_fade > 0.01) {
// Moon
float moon_intersect = simple_sphere_intersect(world_pos, moon_pos, moon_size);
float moon_mask = moon_intersect > (-1. + sqrt(moon_size)) ? 1.0 : 0.0;
vec3 moon_normal = normalize(world_pos * moon_intersect - moon_pos);
float moon_ndotl;
if (moon_phase_override < -0.5) {
// Auto: use real sun position to light the moon
moon_ndotl = clamp(dot(moon_normal, sun_pos), 0.0, 1.0);
} else {
// Manual phase: synthetic light direction from phase angle
// phase 0.0 = new moon (light from behind), 0.5 = full (light from viewer side)
float phase_angle = moon_phase_override * TAU;
vec3 phase_light = normalize(moon_pos * cos(phase_angle) + cross(vec3(0.0, 1.0, 0.0), moon_pos) * sin(phase_angle));
moon_ndotl = clamp(dot(moon_normal, phase_light), 0.0, 1.0);
}
vec3 moon_tex = sample_moon_texture(moon_normal);
vec3 moon_output = moon_mask * moon_ndotl * moon_intensity * moon_tex * moon_color.rgb;
float moonMask = (1.0 - moon_mask);
// Star field — uses precomputed rotation matrix (CPU-side)
// instead of 3x create_rotation_matrix() = 18 trig calls per pixel
vec3 final_alignment = normalize(star_combined_matrix * world_pos);
vec2 deep_space_uv = calc_equirect_uv(normalize(final_alignment));
// Star map (disabled by default — milky_way cubemap is preferred)
if (starmap_enabled) {
vec3 deep_space_background = textureLod(starmap_texture, deep_space_uv, 0.0).rgb;
deep_space_background *= starmap_color.rgb;
deep_space_background = adjust_contrast(deep_space_background, starmap_color.a);
deep_space = deep_space_background.rgb * moonMask + moon_output;
} else {
deep_space = moon_output;
}
// Milky Way cubemap
if (mw_enabled) {
float c_mw = cos(mw_rotation);
float s_mw = sin(mw_rotation);
vec3 mw_dir = vec3(
final_alignment.x * c_mw - final_alignment.z * s_mw,
final_alignment.y,
final_alignment.x * s_mw + final_alignment.z * c_mw
);
vec3 mw_color = texture(milky_way_cubemap, mw_dir).rgb * mw_brightness;
deep_space.rgb += mw_color * moonMask;
}
// Procedural faint stars
if (procedural_stars_enabled) {
float proc_stars = procedural_star_field(final_alignment);
deep_space.rgb += vec3(proc_stars);
}
// Fade stars: nighttime factor + thin atmosphere factor
deep_space.rgb *= star_fade;
}
col.rgb += sun_disk.rgb + deep_space.rgb * horizonBlend;
col.rgb = mix(col.rgb, ground_color.rgb * scatter, clamp_to_unit((-world_pos.y - atm_level_params.z) * 100.0));
return col;
}
vec4 create_axis_angle_quat(vec3 axis, float angle) {
float s = sin(angle * 0.5);
return vec4(axis * s, cos(angle * 0.5));
}
vec3 rotate_vector_by_quat(vec3 v, vec4 q) {
vec3 t = 2.0 * cross(q.xyz, v);
return v + q.w * t + cross(q.xyz, t);
}
vec3 align_and_rotate_vector(vec3 dir, vec3 target, float rotation_angle) {
vec3 z_axis = vec3(0.0, 0.0, 1.0); // Original grid alignment
// First, align Z-axis with target (Polaris)
target = normalize(target);
vec3 axis = cross(z_axis, target);
float angle = acos(dot(z_axis, target));
float valid = step(0.0001, length(axis)); // 1.0 if length(axis) >= 0.0001
axis = normalize(axis);
vec4 align_quat = create_axis_angle_quat(axis, angle);
dir = mix(dir, rotate_vector_by_quat(dir, align_quat), valid);
vec4 rot_quat = create_axis_angle_quat(vec3(0.0, 0.0, 1.0), rotation_angle);
dir = mix(dir, rotate_vector_by_quat(dir, rot_quat), valid);
return dir;
}
void render_grid(vec3 dir, vec3 pole_dir, float rotation_angle, vec4 line_color, float grid_spacing, float line_thickness, inout vec3 color) {
float spacing = max(grid_spacing, 0.001);
float thickness = max(line_thickness, 0.0001);
// Rotate dir with sidereal motion and align with pole_dir
dir = align_and_rotate_vector(dir, pole_dir, rotation_angle);
// Latitude / Circles / Theta Spherical coordinates
float theta = acos(dir.z);
float theta_deriv = fwidth(theta);
float circle_thickness = max(thickness, theta_deriv);
float circle = mod(theta, spacing);
float circle_line = 1.0 - smoothstep(0.0, circle_thickness * 1.5, abs(circle - circle_thickness * 0.5));
circle_line *= clamp_to_unit(thickness / circle_thickness);
// Add poles
float pole_thickness = circle_thickness;
float north_pole = 1.0 - smoothstep(0.0, pole_thickness, theta);
float south_pole = 1.0 - smoothstep(0.0, pole_thickness, abs(theta - PI));
circle_line = max(circle_line, max(north_pole, south_pole));
// Darken Lat at poles
float circle_adjust = sin(theta);
circle_line *= clamp(circle_adjust, 0.2, 1.0);
// Longitude / Radial lines / Phi Spherical coordinates
float phi = atan(dir.y, dir.x);
float phi_deriv = fwidth(phi);
float radial_thickness = max(thickness, phi_deriv);
float radial = mod(phi + PI, spacing); // PI w/ less precision to remove moire
float radial_line = 1.0 - smoothstep(0.0, radial_thickness * 1.5, abs(radial - radial_thickness));
radial_line *= clamp_to_unit(thickness / radial_thickness);
float grid = max(radial_line, circle_line);
color += line_color.rgb * line_color.a * grid;
}
void sky() {
// Volumetric cloud half-res pass removed — cloud rendering moved to nubis_clouds addon
{
float current_time = TIME;
vec3 col = vec3(0.0);
vec3 world_pos = EYEDIR;
vec3 clouds_pos = EYEDIR;
vec3 sun_pos = LIGHT0_DIRECTION;
vec3 moon_pos = LIGHT1_DIRECTION;
if (sky_visible) {
col = render_sky(world_pos, clouds_pos, sun_pos, moon_pos, current_time);
}
// Tonemap (before grids)
col.rgb = apply_photo_tonemap(col.rgb, color_correction.y, color_correction.x);
// Draw overlay grids
float grid_thickness = 0.0002;
float grid_spacing = 0.261799; // 15 degrees
vec3 zenith_dir = vec3(0.0, 1.0, 0.0);
vec3 north_celestial_pole = create_rotation_matrix(vec3(star_tilt, 0.0, 0.0)) * zenith_dir;
float rotation_angle = star_rotation + star_rotation_offset;
if (show_azimuthal_grid) {
render_grid(EYEDIR, zenith_dir, azimuthal_grid_rotation_offset, azimuthal_grid_color, grid_spacing, grid_thickness, col);
}
if (show_equatorial_grid) {
render_grid(EYEDIR, north_celestial_pole, rotation_angle + equatorial_grid_rotation_offset, equatorial_grid_color, grid_spacing, grid_thickness, col);
}
COLOR = col.rgb;
}
}