proof_engine/nishita_sky/
mod.rs1use glam::{Vec3, Vec4};
10use std::f32::consts::PI;
11
12const EARTH_RADIUS: f32 = 6_371_000.0; const ATMO_RADIUS: f32 = 6_471_000.0; const RAYLEIGH_SCALE_HEIGHT: f32 = 8_500.0; const MIE_SCALE_HEIGHT: f32 = 1_200.0; const RAYLEIGH_COEFF: Vec3 = Vec3::new(5.5e-6, 13.0e-6, 22.4e-6); const MIE_COEFF: f32 = 21.0e-6; const MIE_G: f32 = 0.76; #[derive(Debug, Clone)]
23pub struct SkyConfig {
24 pub sun_direction: Vec3,
25 pub sun_intensity: f32,
26 pub sun_color: Vec3,
27 pub num_samples: u32,
28 pub num_light_samples: u32,
29 pub ground_albedo: Vec3,
31 pub observer_height: f32,
33}
34
35impl Default for SkyConfig {
36 fn default() -> Self {
37 Self {
38 sun_direction: Vec3::new(0.0, 0.3, -1.0).normalize(),
39 sun_intensity: 22.0,
40 sun_color: Vec3::ONE,
41 num_samples: 16,
42 num_light_samples: 8,
43 ground_albedo: Vec3::splat(0.3),
44 observer_height: 1.0,
45 }
46 }
47}
48
49fn rayleigh_phase(cos_theta: f32) -> f32 {
51 3.0 / (16.0 * PI) * (1.0 + cos_theta * cos_theta)
52}
53
54fn mie_phase(cos_theta: f32, g: f32) -> f32 {
56 let g2 = g * g;
57 let num = 3.0 * (1.0 - g2) * (1.0 + cos_theta * cos_theta);
58 let denom = (8.0 * PI) * (2.0 + g2) * (1.0 + g2 - 2.0 * g * cos_theta).powf(1.5);
59 num / denom
60}
61
62fn ray_sphere(origin: Vec3, dir: Vec3, center: Vec3, radius: f32) -> Option<(f32, f32)> {
64 let oc = origin - center;
65 let b = oc.dot(dir);
66 let c = oc.dot(oc) - radius * radius;
67 let disc = b * b - c;
68 if disc < 0.0 { return None; }
69 let sqrt_disc = disc.sqrt();
70 Some((-b - sqrt_disc, -b + sqrt_disc))
71}
72
73pub fn compute_sky_color(view_dir: Vec3, config: &SkyConfig) -> Vec3 {
75 let origin = Vec3::new(0.0, EARTH_RADIUS + config.observer_height, 0.0);
76
77 let (_, t_atmo) = match ray_sphere(origin, view_dir, Vec3::ZERO, ATMO_RADIUS) {
79 Some(t) => t,
80 None => return Vec3::ZERO,
81 };
82
83 let t_max = if let Some((t_near, _)) = ray_sphere(origin, view_dir, Vec3::ZERO, EARTH_RADIUS) {
85 if t_near > 0.0 { t_near } else { t_atmo }
86 } else {
87 t_atmo
88 };
89
90 let segment_length = t_max / config.num_samples as f32;
91 let sun_dir = config.sun_direction.normalize();
92 let cos_theta = view_dir.dot(sun_dir);
93
94 let phase_r = rayleigh_phase(cos_theta);
95 let phase_m = mie_phase(cos_theta, MIE_G);
96
97 let mut total_rayleigh = Vec3::ZERO;
98 let mut total_mie = Vec3::ZERO;
99 let mut optical_depth_r = 0.0f32;
100 let mut optical_depth_m = 0.0f32;
101
102 for i in 0..config.num_samples {
103 let t = (i as f32 + 0.5) * segment_length;
104 let sample_pos = origin + view_dir * t;
105 let height = sample_pos.length() - EARTH_RADIUS;
106 if height < 0.0 { break; }
107
108 let hr = (-height / RAYLEIGH_SCALE_HEIGHT).exp();
110 let hm = (-height / MIE_SCALE_HEIGHT).exp();
111
112 optical_depth_r += hr * segment_length;
113 optical_depth_m += hm * segment_length;
114
115 let (_, t_sun) = match ray_sphere(sample_pos, sun_dir, Vec3::ZERO, ATMO_RADIUS) {
117 Some(t) => t,
118 None => continue,
119 };
120
121 let light_segment = t_sun / config.num_light_samples as f32;
122 let mut od_light_r = 0.0f32;
123 let mut od_light_m = 0.0f32;
124 let mut shadow = false;
125
126 for j in 0..config.num_light_samples {
127 let tl = (j as f32 + 0.5) * light_segment;
128 let light_pos = sample_pos + sun_dir * tl;
129 let light_height = light_pos.length() - EARTH_RADIUS;
130 if light_height < 0.0 { shadow = true; break; }
131 od_light_r += (-light_height / RAYLEIGH_SCALE_HEIGHT).exp() * light_segment;
132 od_light_m += (-light_height / MIE_SCALE_HEIGHT).exp() * light_segment;
133 }
134
135 if shadow { continue; }
136
137 let tau_r = RAYLEIGH_COEFF * (optical_depth_r + od_light_r);
139 let tau_m = MIE_COEFF * (optical_depth_m + od_light_m);
140 let attenuation = Vec3::new(
141 (-tau_r.x - tau_m).exp(),
142 (-tau_r.y - tau_m).exp(),
143 (-tau_r.z - tau_m).exp(),
144 );
145
146 total_rayleigh += attenuation * hr * segment_length;
147 total_mie += attenuation * hm * segment_length;
148 }
149
150 let sun = config.sun_color * config.sun_intensity;
151 let sky = sun * (total_rayleigh * RAYLEIGH_COEFF * phase_r + total_mie * MIE_COEFF * phase_m);
152
153 sky
154}
155
156pub fn generate_sky_lut(config: &SkyConfig) -> Vec<[f32; 3]> {
158 let w = 256;
159 let h = 128;
160 let mut lut = Vec::with_capacity(w * h);
161
162 for y in 0..h {
163 let phi = PI * y as f32 / (h - 1) as f32; for x in 0..w {
165 let theta = 2.0 * PI * x as f32 / (w - 1) as f32;
166 let dir = Vec3::new(
167 phi.sin() * theta.cos(),
168 phi.cos(),
169 phi.sin() * theta.sin(),
170 );
171 let color = compute_sky_color(dir, config);
172 lut.push([color.x, color.y, color.z]);
173 }
174 }
175
176 lut
177}
178
179pub struct SkyPresets;
181
182impl SkyPresets {
183 pub fn noon() -> SkyConfig {
184 SkyConfig {
185 sun_direction: Vec3::new(0.0, 1.0, 0.0).normalize(),
186 sun_intensity: 22.0,
187 ..Default::default()
188 }
189 }
190
191 pub fn sunset() -> SkyConfig {
192 SkyConfig {
193 sun_direction: Vec3::new(0.5, 0.05, -0.5).normalize(),
194 sun_intensity: 20.0,
195 sun_color: Vec3::new(1.0, 0.6, 0.3),
196 ..Default::default()
197 }
198 }
199
200 pub fn night() -> SkyConfig {
201 SkyConfig {
202 sun_direction: Vec3::new(0.0, -0.5, -1.0).normalize(),
203 sun_intensity: 0.1,
204 sun_color: Vec3::new(0.3, 0.3, 0.5),
205 ..Default::default()
206 }
207 }
208
209 pub fn dawn() -> SkyConfig {
210 SkyConfig {
211 sun_direction: Vec3::new(-0.8, 0.1, -0.3).normalize(),
212 sun_intensity: 15.0,
213 sun_color: Vec3::new(1.0, 0.5, 0.3),
214 ..Default::default()
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_sky_color_noon() {
225 let config = SkyPresets::noon();
226 let zenith = compute_sky_color(Vec3::Y, &config);
227 assert!(zenith.x > 0.0 && zenith.y > 0.0 && zenith.z > 0.0);
228 assert!(zenith.z > zenith.x, "zenith should be blue-ish");
230 }
231
232 #[test]
233 fn test_sky_color_horizon() {
234 let config = SkyPresets::sunset();
235 let horizon = compute_sky_color(Vec3::new(0.0, 0.01, -1.0).normalize(), &config);
236 assert!(horizon.x > horizon.z * 0.5, "horizon at sunset should be warm");
238 }
239
240 #[test]
241 fn test_rayleigh_phase_symmetry() {
242 let a = rayleigh_phase(0.5);
244 let b = rayleigh_phase(-0.5);
245 assert!((a - b).abs() < 0.001);
246 }
247
248 #[test]
249 fn test_sky_lut_size() {
250 let config = SkyConfig { num_samples: 4, num_light_samples: 2, ..Default::default() };
251 let lut = generate_sky_lut(&config);
252 assert_eq!(lut.len(), 256 * 128);
253 }
254}