bevy_sun_move/lib.rs
1pub mod random_stars;
2
3use bevy::prelude::*;
4use std::f32::consts::PI;
5
6// Helper constants
7pub const DEGREES_TO_RADIANS: f32 = PI / 180.0;
8pub const RADIANS_TO_DEGREES: f32 = 180.0 / PI;
9
10pub struct SunMovePlugin;
11
12impl Plugin for SunMovePlugin {
13 fn build(&self, app: &mut App) {
14 app.add_systems(Update, update_sky_center::<Time>);
15 }
16}
17
18pub trait ISunTime {
19 fn delta_secs(&self) -> f32;
20 fn elapsed_secs(&self) -> f32;
21}
22
23impl<T: Default + Send + Sync + 'static> ISunTime for Time<T> {
24 fn delta_secs(&self) -> f32 {
25 self.delta_secs()
26 }
27
28 fn elapsed_secs(&self) -> f32 {
29 self.elapsed_secs()
30 }
31}
32
33pub struct TypedSunMovePlugin<T: ISunTime + Resource> {
34 _marker: std::marker::PhantomData<T>,
35}
36
37impl<T: ISunTime + Resource> Default for TypedSunMovePlugin<T> {
38 fn default() -> Self {
39 Self {
40 _marker: std::marker::PhantomData,
41 }
42 }
43}
44
45impl<T: ISunTime + Resource> Plugin for TypedSunMovePlugin<T> {
46 fn build(&self, app: &mut App) {
47 app.add_systems(Update, update_sky_center::<T>);
48 }
49}
50
51// Determine latitude and year fraction from day and night fractions of full cycle
52#[derive(Component, Debug, Clone)]
53pub struct TimedSkyConfig {
54 pub planet_tilt_degrees: f32,
55 /// Desired duration of daylight in seconds.
56 pub day_duration_secs: f32,
57 /// Desired duration of nighttime in seconds.
58 pub night_duration_secs: f32,
59 /// Desired maximum sun height (altitude) in degrees during the day.
60 pub max_sun_height_deg: f32,
61 /// The entity representing the sun (usually a DirectionalLight).
62 pub sun_entity: Entity,
63}
64
65impl Default for TimedSkyConfig {
66 fn default() -> Self {
67 Self {
68 planet_tilt_degrees: 23.5, // Earth's tilt
69 sun_entity: Entity::PLACEHOLDER,
70 day_duration_secs: 15.0, // Example: 15s day
71 night_duration_secs: 15.0, // Example: 15s night (total cycle 30s)
72 max_sun_height_deg: 45.0,
73 }
74 }
75}
76
77/// Calculates required latitude and year fraction to achieve a specific day/night
78/// duration ratio and maximum sun height (noon altitude) for a given planet tilt.
79///
80/// Based on standard astronomical formulas relating day length, noon altitude,
81/// latitude, and declination.
82///
83/// Args:
84/// - planet_tilt_degrees: The axial tilt of the planet in degrees.
85/// - day_duration_secs: The target duration of daylight in seconds.
86/// - night_duration_secs: The target duration of nighttime in seconds.
87/// - max_sun_height_deg: The target maximum altitude of the sun in degrees.
88///
89/// Returns:
90/// An `Option<(latitude_degrees, year_fraction, calculated_declination_degrees)>`.
91/// Returns `None` if the requested parameters are impossible for the given tilt
92/// (e.g., max height too high/low for the day length, or required declination
93/// exceeds the planet tilt).
94#[allow(non_snake_case)]
95pub fn calculate_latitude_yearfraction(
96 planet_tilt_degrees: f32,
97 day_duration_secs: f32,
98 night_duration_secs: f32,
99 max_sun_height_deg: f32,
100) -> Option<(f32, f32, f32)> {
101 let total_duration_secs = day_duration_secs + night_duration_secs;
102 let tilt_rad = planet_tilt_degrees.abs() * DEGREES_TO_RADIANS;
103
104 if total_duration_secs <= f32::EPSILON || day_duration_secs < 0.0 || night_duration_secs < 0.0 {
105 warn!(
106 "Invalid timed durations: day={}s, night={}s. Cannot calculate.",
107 day_duration_secs, night_duration_secs
108 );
109 return None;
110 }
111
112 if max_sun_height_deg < -0.1 || max_sun_height_deg > 90.0 + 0.1 {
113 // Allow slight floating point deviations
114 warn!(
115 "Max sun height {:.2}° is outside valid range [0°, 90°]. Cannot calculate.",
116 max_sun_height_deg
117 );
118 return None;
119 }
120
121 // Handle edge cases: Perpetual Day/Night or 12/12 cycle
122 if day_duration_secs < f32::EPSILON && night_duration_secs > f32::EPSILON {
123 // Perpetual Night (day_fraction = 0)
124 // Requires sun never rises, i.e. max altitude <= 0.
125 if max_sun_height_deg > f32::EPSILON {
126 warn!(
127 "Perpetual night requested but max sun height is {:.2}°. Impossible.",
128 max_sun_height_deg
129 );
130 return None;
131 }
132 // Max height is 0. This happens at latitudes where sun circles the horizon.
133 // This occurs at latitude = 90 - |dec|. For perpetual night at a pole-like lat,
134 // we need dec to be -tilt (NH winter) or +tilt (SH winter).
135 // Latitude is 90 - tilt. Year fraction is 0.75 (NH) or 0.25 (SH).
136 if tilt_rad < f32::EPSILON {
137 warn!("Perpetual night with 0 tilt is impossible unless at equator (12/12 cycle).");
138 return None; // 0 tilt implies 12/12 cycle everywhere.
139 }
140 let calculated_latitude_degrees =
141 (90.0 - planet_tilt_degrees.abs()).copysign(-planet_tilt_degrees); // Choose pole opposite tilt
142 let calculated_declination_degrees = -planet_tilt_degrees.copysign(planet_tilt_degrees); // Winter solstice dec
143 let calculated_year_fraction = if planet_tilt_degrees > 0.0 {
144 0.75
145 } else {
146 0.25
147 }; // NH Winter or SH Winter
148 // info!("Perpetual night calculation: Lat {:.2}°, Dec {:.2}°, YF {:.2}", calculated_latitude_degrees, calculated_declination_degrees, calculated_year_fraction);
149 return Some((
150 calculated_latitude_degrees,
151 calculated_year_fraction,
152 calculated_declination_degrees,
153 ));
154 }
155
156 if night_duration_secs < f32::EPSILON && day_duration_secs > f32::EPSILON {
157 // Perpetual Day (day_fraction = 1)
158 // Requires sun never sets, i.e. min altitude >= 0.
159 // Max height must be > 0 (unless at pole/equinox/tilt=0 which implies 12/12 max height 0).
160 if max_sun_height_deg < f32::EPSILON {
161 warn!(
162 "Perpetual day requested but max sun height is {:.2}°. Impossible (must be > 0 unless 12/12).",
163 max_sun_height_deg
164 );
165 return None; // Perpetual day usually has max height > 0. Max height 0 is the 12/12 case.
166 }
167 // Max height > 0. Perpetual day happens at latitudes polewards of 90 - tilt during summer solstice.
168 // Max height = 90 - |lat - dec|. Min height = 90 - |lat + dec|.
169 // At lat = 90 - tilt, summer solstice (dec=tilt), max height = 90 - (90-tilt - tilt) = 2*tilt. Min height = 90 - (90-tilt + tilt) = 0.
170 // For max height H > 0 and perpetual day, required dec = H/2, required lat = 90 - H/2.
171 if tilt_rad < f32::EPSILON {
172 warn!("Perpetual day with 0 tilt is impossible unless at equator (12/12 cycle).");
173 return None; // 0 tilt implies 12/12 cycle everywhere.
174 }
175 let max_height_rad = max_sun_height_deg * DEGREES_TO_RADIANS;
176 let required_dec_rad = max_height_rad / 2.0;
177 if required_dec_rad.abs() > tilt_rad + f32::EPSILON {
178 warn!(
179 "Required declination {:.2}° for perpetual day with max height {:.2}° exceeds planet tilt {:.2}°. Impossible.",
180 required_dec_rad * RADIANS_TO_DEGREES,
181 max_sun_height_deg,
182 planet_tilt_degrees
183 );
184 return None;
185 }
186 let calculated_latitude_degrees =
187 (90.0 * DEGREES_TO_RADIANS - required_dec_rad) * RADIANS_TO_DEGREES;
188 let calculated_declination_degrees = required_dec_rad * RADIANS_TO_DEGREES;
189 // Summer solstice requires dec > 0 if lat > 0, or dec < 0 if lat < 0.
190 // We aim for positive latitude hemisphere:
191 let final_lat_deg = calculated_latitude_degrees.copysign(planet_tilt_degrees); // Use tilt sign to pick hemisphere
192 let final_dec_deg = calculated_declination_degrees.copysign(planet_tilt_degrees); // Dec must match hemi for summer
193 let sin_yf_angle = final_dec_deg * DEGREES_TO_RADIANS / tilt_rad;
194 let phi = sin_yf_angle.clamp(-1.0, 1.0).asin();
195 let calculated_year_fraction = if final_dec_deg >= 0.0 {
196 phi / (2.0 * PI)
197 } else {
198 0.5 - phi / (2.0 * PI)
199 };
200
201 // info!("Perpetual day calculation: Lat {:.2}°, Dec {:.2}°, YF {:.2}", final_lat_deg, final_dec_deg, calculated_year_fraction);
202 return Some((final_lat_deg, calculated_year_fraction, final_dec_deg));
203 }
204
205 if total_duration_secs <= f32::EPSILON {
206 warn!("Total duration is zero.");
207 return None;
208 }
209
210 let day_fraction = day_duration_secs / total_duration_secs;
211 let max_height_rad = max_sun_height_deg * DEGREES_TO_RADIANS;
212
213 let C = (PI * day_fraction).cos();
214 let S_h = max_height_rad.sin();
215
216 // Derived relations:
217 // cos(lat_rad - dec_rad) = sin(max_height_rad)
218 // cos(lat_rad + dec_rad) = sin(max_height_rad) * (1 + cos(PI * day_fraction)) / (1 - cos(PI * day_fraction))
219
220 let term_for_cos_sum = if (1.0 - C).abs() < f32::EPSILON {
221 // Handle day_fraction near 0 (C near 1)
222 if S_h > f32::EPSILON {
223 // Max height > 0 with day fraction near 0 (perpetual night)
224 warn!(
225 "Impossible combination: Max height {:.2}° requires sun rise, but day fraction {:.2} requests near perpetual night.",
226 max_sun_height_deg, day_fraction
227 );
228 return None;
229 } else {
230 // Max height near 0 with day fraction near 0 (perpetual night on horizon)
231 // This case should be handled by the perpetual night block above.
232 // If we reach here, something is slightly off. Return None or default.
233 warn!("Reached indeterminate case for cos(lat+dec) near day_fraction 0.");
234 return None;
235 }
236 } else {
237 S_h * (1.0 + C) / (1.0 - C)
238 };
239
240 if term_for_cos_sum.abs() > 1.0 + f32::EPSILON {
241 warn!(
242 "Impossible combination: Max height {:.2}° and day fraction {:.2} requires cos(lat+dec) value {:.2} outside [-1, 1].",
243 max_sun_height_deg, day_fraction, term_for_cos_sum
244 );
245 return None;
246 }
247
248 let beta = term_for_cos_sum.clamp(-1.0, 1.0).acos(); // angle for lat + dec
249 let alpha = PI / 2.0 - max_height_rad; // angle for |lat - dec| (zenith distance at noon)
250
251 // Note: cos(lat-dec) = sin(h) implies |lat-dec| = PI/2 - h for h in [0, PI/2]
252 // The sign of (lat-dec) determines if sun culminates South (+ve) or North (-ve) of zenith.
253 // cos(lat+dec) = term_for_cos_sum
254 // The sign of (lat+dec) determines the average position relative to equator/solstices.
255
256 // We need to solve the system:
257 // lat - dec = +/- alpha
258 // lat + dec = +/- beta
259
260 // Let's find candidate lat/dec pairs. There are 4 mathematical pairs, but only 1 or 2
261 // will have |dec| <= |tilt| and |lat| <= PI/2.
262 // Pairs (lat, dec) in radians:
263 let candidates = [
264 ((alpha + beta) / 2.0, (beta - alpha) / 2.0), // lat-dec = +alpha, lat+dec = +beta
265 ((alpha - beta) / 2.0, (-beta - alpha) / 2.0), // lat-dec = +alpha, lat+dec = -beta
266 ((-alpha + beta) / 2.0, (beta + alpha) / 2.0), // lat-dec = -alpha, lat+dec = +beta
267 ((-alpha - beta) / 2.0, (-beta + alpha) / 2.0), // lat-dec = -alpha, lat+dec = -beta
268 ];
269
270 let mut found_lat_rad = None;
271 let mut found_dec_rad = None;
272
273 for (lat_candidate, dec_candidate) in candidates.iter() {
274 let lat_deg = lat_candidate * RADIANS_TO_DEGREES;
275 let dec_deg = dec_candidate * RADIANS_TO_DEGREES;
276
277 // Check if dec is achievable with the planet tilt
278 if dec_deg.abs() <= planet_tilt_degrees.abs() + f32::EPSILON {
279 // Check if latitude is valid
280 if lat_deg.abs() <= 90.0 + f32::EPSILON {
281 // Found a valid pair. Check if it matches our preferred sign combo.
282 let current_lat_sign = lat_deg.signum();
283 let current_dec_sign = dec_deg.signum();
284
285 let signs_match_preference = (day_fraction > 0.5 && current_lat_sign * current_dec_sign >= 0.0) || // Long day: lat and dec same sign
286 (day_fraction < 0.5 && current_lat_sign * current_dec_sign <= 0.0); // Short day: lat and dec opposite sign
287
288 // If it matches preference, pick it immediately and break.
289 // If not, keep searching in case there's another valid one that does.
290 // If multiple match preference, the first found in the list order is used.
291 if signs_match_preference {
292 found_lat_rad = Some(*lat_candidate);
293 found_dec_rad = Some(*dec_candidate);
294 break; // Found preferred solution
295 }
296
297 // If no preferred solution found yet, store *any* valid solution
298 // (the last one found in the loop order will be kept if no preferred is found)
299 if found_lat_rad.is_none() {
300 found_lat_rad = Some(*lat_candidate);
301 found_dec_rad = Some(*dec_candidate);
302 }
303 }
304 }
305 }
306
307 match (found_lat_rad, found_dec_rad) {
308 (Some(lat_rad), Some(dec_rad)) => {
309 let calculated_latitude_degrees = lat_rad * RADIANS_TO_DEGREES;
310 let calculated_declination_degrees = dec_rad * RADIANS_TO_DEGREES;
311
312 // Now find the year fraction corresponding to this declination and tilt
313 if tilt_rad < f32::EPSILON {
314 // Handle 0 tilt separately
315 if dec_rad.abs() > f32::EPSILON {
316 warn!(
317 "Calculated non-zero declination {:.2}° but tilt is 0°. Impossible.",
318 calculated_declination_degrees
319 );
320 return None;
321 }
322 // If dec is 0 and tilt is 0, any year fraction works, but let's pick equinox.
323 return Some((
324 calculated_latitude_degrees,
325 0.0,
326 calculated_declination_degrees,
327 ));
328 }
329
330 let sin_yf_angle = (dec_rad / tilt_rad).clamp(-1.0, 1.0); // Should be <= 1 from checks, but clamp for safety
331 let phi = sin_yf_angle.asin(); // phi is in [-PI/2, PI/2]
332
333 // There are two year fractions per declination (unless at solstice)
334 // yf1 maps dec >= 0 to [0, 0.25] and dec < 0 to [0.75, 1)
335 let yf1 = if dec_rad >= 0.0 {
336 phi / (2.0 * PI)
337 } else {
338 1.0 + phi / (2.0 * PI)
339 };
340 // yf2 maps dec >= 0 to [0.25, 0.5] and dec < 0 to (0.5, 0.75]
341 let yf2 = if dec_rad >= 0.0 {
342 0.5 - phi / (2.0 * PI)
343 } else {
344 0.5 - phi / (2.0 * PI)
345 };
346
347 // Let's choose the year fraction that is closer to the 'expected' season for the day length
348 // Long day (df > 0.5) suggests summer-like conditions (yf near 0.25 or 0.75 depending on hemi/tilt sign)
349 // Short day (df < 0.5) suggests winter-like conditions (yf near 0.75 or 0.25 depending on hemi/tilt sign)
350 // Given we aimed for lat/dec signs matching df, dec > 0 implies NH summer/SH winter half year.
351 // dec > 0 is yf in (0, 0.5). yf1 is [0, 0.25], yf2 is [0.25, 0.5]. Pick one closest to 0.25?
352 // dec < 0 is yf in (0.5, 1). yf1 is [0.75, 1), yf2 is (0.5, 0.75]. Pick one closest to 0.75?
353
354 let target_yf = if dec_rad >= 0.0 { 0.25 } else { 0.75 };
355 let calculated_year_fraction = if (target_yf - yf1).abs() < (target_yf - yf2).abs() {
356 yf1
357 } else {
358 yf2
359 };
360 // Ensure year fraction is in [0, 1) range
361 let final_yf = calculated_year_fraction.fract();
362 let final_yf = if final_yf < 0.0 {
363 final_yf + 1.0
364 } else {
365 final_yf
366 };
367
368 // info!("Calculated parameters: Latitude {:.2}°, Declination {:.2}°, Year Fraction {:.4}",
369 // calculated_latitude_degrees, calculated_declination_degrees, final_yf);
370
371 Some((
372 calculated_latitude_degrees,
373 final_yf,
374 calculated_declination_degrees,
375 ))
376 }
377 _ => {
378 warn!("No valid latitude/declination found for the given constraints.");
379 None
380 }
381 }
382}
383
384#[derive(Component, Debug, Clone)]
385#[require(Transform, Visibility)]
386pub struct SkyCenter {
387 pub latitude_degrees: f32,
388 pub planet_tilt_degrees: f32,
389
390 /// Fraction of the year (0.0 to 1.0), where 0.0 is Vernal Equinox.
391 pub year_fraction: f32,
392
393 /// Duration of a full day/night cycle in seconds.
394 pub cycle_duration_secs: f32,
395
396 /// The entity representing the sun (usually a DirectionalLight).
397 pub sun: Entity,
398
399 /// Time elapsed within the current cycle (seconds).
400 /// Stored here to allow pausing/setting time easily.
401 pub current_cycle_time: f32,
402}
403
404impl Default for SkyCenter {
405 fn default() -> Self {
406 Self {
407 latitude_degrees: 0.0,
408 planet_tilt_degrees: 23.5,
409 year_fraction: 0.0,
410 cycle_duration_secs: 600.0, // 10 minutes by default
411 sun: Entity::PLACEHOLDER,
412 current_cycle_time: 0.0,
413 }
414 }
415}
416
417impl SkyCenter {
418 pub fn from_timed_config(timed_config: &TimedSkyConfig) -> Option<Self> {
419 let calc = calculate_latitude_yearfraction(
420 timed_config.planet_tilt_degrees,
421 timed_config.day_duration_secs,
422 timed_config.night_duration_secs,
423 timed_config.max_sun_height_deg,
424 );
425
426 if let Some((latitude, year_fraction, _)) = calc {
427 Some(Self {
428 latitude_degrees: latitude,
429 planet_tilt_degrees: timed_config.planet_tilt_degrees,
430 year_fraction,
431 cycle_duration_secs: timed_config.day_duration_secs
432 + timed_config.night_duration_secs,
433 sun: timed_config.sun_entity,
434 current_cycle_time: 0.0,
435 })
436 } else {
437 warn!("Failed to calculate latitude/year_fraction/declination for timed sky config.");
438 None
439 }
440 }
441
442 #[allow(dead_code)]
443 fn update_from_timed_config(&mut self, timed_config: &TimedSkyConfig) {
444 let calc = calculate_latitude_yearfraction(
445 timed_config.planet_tilt_degrees,
446 timed_config.day_duration_secs,
447 timed_config.night_duration_secs,
448 timed_config.max_sun_height_deg,
449 );
450
451 if let Some((latitude, year_fraction, _)) = calc {
452 self.latitude_degrees = latitude;
453 self.year_fraction = year_fraction;
454 self.cycle_duration_secs =
455 timed_config.day_duration_secs + timed_config.night_duration_secs;
456 self.sun = timed_config.sun_entity;
457 } else {
458 warn!("Failed to calculate latitude/year_fraction/declination for timed sky config.");
459 }
460 }
461}
462
463/// Calculates the sun's direction vector in the observer's local coordinate frame (Y up, X east, Z north).
464/// This vector points *from* the observer *towards* the sun.
465///
466/// Based on standard astronomical formulas converting equatorial coordinates (declination, hour angle)
467/// to horizontal coordinates (altitude, azimuth).
468///
469/// Args:
470/// - hour_fraction: Fraction of the day (0.0 to 1.0), where 0.0 is midnight, 0.5 is noon.
471/// - latitude_rad: Observer's latitude in radians (-PI/2 to PI/2).
472/// - axial_tilt_rad: Planet's axial tilt in radians (e.g., 23.5 degrees for Earth).
473/// - year_fraction: Fraction of the year (0.0 to 1.0), where 0.0 is Vernal Equinox.
474///
475/// Returns:
476/// A `Vec3` representing the sun's direction relative to the observer.
477/// The vector length is arbitrary, usually normalized.
478pub fn calculate_sun_direction(
479 hour_fraction: f32,
480 latitude_rad: f32,
481 axial_tilt_rad: f32,
482 year_fraction: f32,
483) -> Vec3 {
484 // Calculate sun's declination based on axial tilt and time of year.
485 // Assuming year_fraction 0.0 is Vernal Equinox (dec=0), 0.25 is Summer Solstice (dec=tilt), etc.
486 let year_angle_rad = year_fraction * 2.0 * PI;
487 let dec_rad = axial_tilt_rad * year_angle_rad.sin();
488
489 // Calculate Local Hour Angle (LHA). This is angle from local meridian (South/North line).
490 // hour_fraction 0.0 is midnight, 0.5 is noon. LHA is 0 at noon, PI 12 hours later.
491 // hour_angle_rad from midnight = hour_fraction * 2.0 * PI.
492 // Local Hour Angle (HA) is angle west of meridian. HA=0 at noon.
493 let hour_angle_rad_from_midnight = hour_fraction * 2.0 * PI;
494 let local_hour_angle_rad = hour_angle_rad_from_midnight - PI; // Angle from noon meridian, positive West
495
496 // Calculate sun's altitude (elevation above horizon) and components in local frame.
497 // Standard formulas for converting equatorial (Dec, HA) to horizontal (Alt, Azi):
498 // sin(alt) = sin(lat)sin(dec) + cos(lat)cos(dec)cos(HA)
499 // cos(alt)sin(azi) = cos(dec)sin(HA) (X component in East-Up-North)
500 // cos(alt)cos(azi) = cos(lat)sin(dec) - sin(lat)cos(dec)cos(HA) (Z component in East-Up-North)
501
502 // Y (up) component = sin(altitude)
503 let sin_alt = latitude_rad.sin() * dec_rad.sin()
504 + latitude_rad.cos() * dec_rad.cos() * local_hour_angle_rad.cos();
505
506 // X (east) component = cos(altitude) * sin(azimuth from North towards East)
507 // Z (north) component = cos(altitude) * cos(azimuth from North towards East)
508 // We can get these components directly without calculating azimuth explicitly:
509 let x_east = dec_rad.cos() * local_hour_angle_rad.sin();
510 let z_north = latitude_rad.cos() * dec_rad.sin()
511 - latitude_rad.sin() * dec_rad.cos() * local_hour_angle_rad.cos();
512
513 // Construct the direction vector in the observer's local Bevy frame (X east, Y up, Z north)
514 let sun_direction_local = Vec3::new(
515 x_east, // X: East
516 sin_alt, // Y: Up (sin_alt is already calculated)
517 z_north, // Z: North
518 );
519
520 // Normalize the vector
521 sun_direction_local.normalize()
522}
523
524fn update_sky_center<T: ISunTime + Resource>(
525 mut q_sky_center: Query<(&mut Transform, &mut SkyCenter)>,
526 mut q_sun: Query<&mut Transform, Without<SkyCenter>>,
527 time: Res<T>,
528) {
529 for (mut sky_transforms, mut sky_center) in q_sky_center.iter_mut() {
530 // Update time
531 sky_center.current_cycle_time = time.elapsed_secs();
532 sky_center.current_cycle_time %= sky_center.cycle_duration_secs; // Cycle time loops
533
534 let hour_fraction = sky_center.current_cycle_time / sky_center.cycle_duration_secs;
535
536 let latitude_rad = sky_center.latitude_degrees * DEGREES_TO_RADIANS;
537 let tilt_rad = sky_center.planet_tilt_degrees * DEGREES_TO_RADIANS;
538 let year_fraction = sky_center.year_fraction;
539
540 sky_transforms.translation = Vec3::ZERO;
541 // Sky sphere rotation axis. Useful for attach stars and celestial bodies to the sky sphere.
542 let celestial_pole_axis_local = Vec3::new(0.0, latitude_rad.sin(), latitude_rad.cos());
543
544 // Sky sphere rotation
545 let rotation_angle_rad = PI - hour_fraction * 2.0 * PI;
546 sky_transforms.rotation =
547 Quat::from_axis_angle(celestial_pole_axis_local, rotation_angle_rad);
548
549 let sun_direction_local =
550 calculate_sun_direction(hour_fraction, latitude_rad, tilt_rad, year_fraction);
551
552 if let Ok(mut sun_transform) = q_sun.get_mut(sky_center.sun) {
553 sun_transform.translation = sun_direction_local;
554 sun_transform.look_at(Vec3::ZERO, Vec3::Y); // Ensure the light points towards the origin
555 }
556 }
557}