/* * Copyright (C) 2021 Christopher J. Howard * * This file is part of Antkeeper source code. * * Antkeeper source code is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Antkeeper source code is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Antkeeper source code. If not, see . */ #include "game/system/astronomy.hpp" #include "astro/apparent-size.hpp" #include "game/component/blackbody.hpp" #include "game/component/transform.hpp" #include "game/component/diffuse-reflector.hpp" #include "geom/intersection.hpp" #include "geom/cartesian.hpp" #include "color/color.hpp" #include "physics/orbit/orbit.hpp" #include "physics/time/ut1.hpp" #include "physics/time/jd.hpp" #include "physics/light/photometry.hpp" #include "physics/light/luminosity.hpp" #include "physics/light/refraction.hpp" #include "physics/gas/atmosphere.hpp" #include "geom/cartesian.hpp" #include "astro/apparent-size.hpp" #include "geom/solid-angle.hpp" #include "math/polynomial.hpp" #include namespace game { namespace system { astronomy::astronomy(entity::registry& registry): updatable(registry), time_days(0.0), time_centuries(0.0), time_scale(1.0), observer_eid(entt::null), reference_body_eid(entt::null), transmittance_samples(0), sun_light(nullptr), sky_light(nullptr), moon_light(nullptr), bounce_light(nullptr), bounce_albedo{0, 0, 0}, sky_pass(nullptr), starlight_illuminance{0, 0, 0} { // Construct ENU to EUS transformation enu_to_eus = math::transformation::se3 { {0, 0, 0}, math::quaternion::rotate_x(-math::half_pi) }; registry.on_construct().connect<&astronomy::on_observer_modified>(this); registry.on_update().connect<&astronomy::on_observer_modified>(this); registry.on_destroy().connect<&astronomy::on_observer_destroyed>(this); registry.on_construct().connect<&astronomy::on_celestial_body_modified>(this); registry.on_update().connect<&astronomy::on_celestial_body_modified>(this); registry.on_destroy().connect<&astronomy::on_celestial_body_destroyed>(this); registry.on_construct().connect<&astronomy::on_orbit_modified>(this); registry.on_update().connect<&astronomy::on_orbit_modified>(this); registry.on_destroy().connect<&astronomy::on_orbit_destroyed>(this); registry.on_construct().connect<&astronomy::on_atmosphere_modified>(this); registry.on_update().connect<&astronomy::on_atmosphere_modified>(this); registry.on_destroy().connect<&astronomy::on_atmosphere_destroyed>(this); } astronomy::~astronomy() { registry.on_construct().disconnect<&astronomy::on_observer_modified>(this); registry.on_update().disconnect<&astronomy::on_observer_modified>(this); registry.on_destroy().disconnect<&astronomy::on_observer_destroyed>(this); registry.on_construct().disconnect<&astronomy::on_celestial_body_modified>(this); registry.on_update().disconnect<&astronomy::on_celestial_body_modified>(this); registry.on_destroy().disconnect<&astronomy::on_celestial_body_destroyed>(this); registry.on_construct().disconnect<&astronomy::on_orbit_modified>(this); registry.on_update().disconnect<&astronomy::on_orbit_modified>(this); registry.on_destroy().disconnect<&astronomy::on_orbit_destroyed>(this); registry.on_construct().disconnect<&astronomy::on_atmosphere_modified>(this); registry.on_update().disconnect<&astronomy::on_atmosphere_modified>(this); registry.on_destroy().disconnect<&astronomy::on_atmosphere_destroyed>(this); } void astronomy::update(double t, double dt) { double3 sky_light_illuminance = {0.0, 0.0, 0.0}; // Add scaled timestep to current time set_time(time_days + dt * time_scale); // Abort if no valid observer entity or reference body entity if (observer_eid == entt::null || reference_body_eid == entt::null) return; // Get pointer to observer component const auto observer = registry.try_get(observer_eid); // Abort if no observer component if (!observer) return; // Get pointers to reference body components const auto [ reference_body, reference_orbit, reference_atmosphere ] = registry.try_get(reference_body_eid); // Abort if no reference body or reference orbit if (!reference_body || !reference_orbit) return; // Update ICRF to EUS transformation update_icrf_to_eus(*reference_body, *reference_orbit); // Set the transform component translations of orbiting bodies to their topocentric positions registry.view().each( [&](entity::id entity_id, const auto& body, const auto& orbit, auto& transform) { // Skip reference body entity if (entity_id == reference_body_eid) return; // Transform orbital Cartesian position (r) from the ICRF frame to the EUS frame const double3 r_eus = icrf_to_eus * orbit.position; // Evaluate body orientation polynomials const double body_pole_ra = math::polynomial::horner(body.pole_ra.begin(), body.pole_ra.end(), time_centuries); const double body_pole_dec = math::polynomial::horner(body.pole_dec.begin(), body.pole_dec.end(), time_centuries); const double body_prime_meridian = math::polynomial::horner(body.prime_meridian.begin(), body.prime_meridian.end(), time_days); // Determine body orientation in the ICRF frame math::quaternion rotation_icrf = physics::orbit::frame::bcbf::to_bci ( body_pole_ra, body_pole_dec, body_prime_meridian ).r; // Transform body orientation from the ICRF frame to the EUS frame. math::quaternion rotation_eus = math::normalize(icrf_to_eus.r * rotation_icrf); // Update local transform if (orbit.parent != entt::null) { transform.local.translation = math::normalize(float3(r_eus)); transform.local.rotation = math::quaternion(rotation_eus); transform.local.scale = {1.0f, 1.0f, 1.0f}; } }); constexpr double3 bounce_normal = {0, 1, 0}; double3 bounce_illuminance = {0, 0, 0}; // Update blackbody lighting registry.view().each( [&, bounce_normal](entity::id entity_id, const auto& blackbody_body, const auto& blackbody_orbit, const auto& blackbody) { // Transform blackbody position from ICRF frame to EUS frame const double3 blackbody_position_eus = icrf_to_eus * blackbody_orbit.position; // Measure distance and direction, in EUS frame, from observer to blackbody const double observer_blackbody_distance = math::length(blackbody_position_eus); const double3 observer_blackbody_direction_eus = blackbody_position_eus / observer_blackbody_distance; // Measure blackbody solid angle as seen by observer const double observer_blackbody_angular_radius = astro::angular_radius(blackbody_body.radius, observer_blackbody_distance); const double observer_blackbody_solid_angle = geom::solid_angle::cone(observer_blackbody_angular_radius); // Calculate illuminance from blackbody reaching observer const double3 observer_blackbody_illuminance = blackbody.luminance * observer_blackbody_solid_angle; // Calculate illuminance from blackbody reaching observer after atmospheric extinction double3 observer_blackbody_transmitted_illuminance = observer_blackbody_illuminance; if (reference_atmosphere) { // Construct ray at observer pointing towards the blackbody const geom::ray ray = {{0, 0, 0}, observer_blackbody_direction_eus}; // Integrate atmospheric spectral transmittance factor between observer and blackbody const double3 transmittance = integrate_transmittance(*observer, *reference_body, *reference_atmosphere, ray); // Attenuate illuminance from blackbody reaching observer by spectral transmittance factor observer_blackbody_transmitted_illuminance *= transmittance; } // Update sun light if (sun_light != nullptr) { const double3 blackbody_up_eus = icrf_to_eus.r * double3{0, 0, 1}; sun_light->set_rotation ( math::look_rotation ( float3(-observer_blackbody_direction_eus), float3(blackbody_up_eus) ) ); sun_light->set_color(float3(observer_blackbody_transmitted_illuminance)); // Bounce sun light bounce_illuminance += std::max(0.0, math::dot(bounce_normal, -observer_blackbody_direction_eus)) * observer_blackbody_transmitted_illuminance * bounce_albedo; } // Update sky light if (sky_light != nullptr) { // Calculate sky illuminance double3 blackbody_position_enu_spherical = physics::orbit::frame::enu::spherical(enu_to_eus.inverse() * blackbody_position_eus); const double sky_illuminance = 25000.0 * std::max(0.0, std::sin(blackbody_position_enu_spherical.y())); // Add sky illuminance to sky light illuminance sky_light_illuminance += {sky_illuminance, sky_illuminance, sky_illuminance}; // Add starlight illuminance to sky light illuminance sky_light_illuminance += starlight_illuminance; // Update sky light sky_light->set_color(float3(sky_light_illuminance)); // Bounce sky light bounce_illuminance += sky_light_illuminance * bounce_albedo; } // Upload blackbody params to sky pass if (this->sky_pass) { this->sky_pass->set_sun_position(float3(blackbody_position_eus)); this->sky_pass->set_sun_luminance(float3(blackbody.luminance)); this->sky_pass->set_sun_illuminance(float3(observer_blackbody_illuminance), float3(observer_blackbody_transmitted_illuminance)); this->sky_pass->set_sun_angular_radius(static_cast(observer_blackbody_angular_radius)); } // Update diffuse reflectors this->registry.view().each( [&, bounce_normal](entity::id entity_id, const auto& reflector_body, const auto& reflector_orbit, const auto& reflector, const auto& transform) { // Transform reflector position from ICRF frame to EUS frame const double3 reflector_position_eus = icrf_to_eus * reflector_orbit.position; // Measure distance and direction, in EUS frame, from observer to reflector const double observer_reflector_distance = math::length(reflector_position_eus); const double3 observer_reflector_direction_eus = reflector_position_eus / observer_reflector_distance; // Measure distance and direction, in EUS frame, from reflector to blackbody double3 reflector_blackbody_direction_eus = blackbody_position_eus - reflector_position_eus; const double reflector_blackbody_distance = math::length(reflector_blackbody_direction_eus); reflector_blackbody_direction_eus /= reflector_blackbody_distance; // Measure blackbody solid angle as seen by reflector const double reflector_blackbody_angular_radius = astro::angular_radius(blackbody_body.radius, reflector_blackbody_distance); const double reflector_blackbody_solid_angle = geom::solid_angle::cone(reflector_blackbody_angular_radius); // Calculate blackbody illuminance reaching reflector const double3 reflector_blackbody_illuminance = blackbody.luminance * reflector_blackbody_solid_angle; // Measure reflector solid angle as seen by observer const double observer_reflector_angular_radius = astro::angular_radius(reflector_body.radius, observer_reflector_distance); const double observer_reflector_solid_angle = geom::solid_angle::cone(observer_reflector_angular_radius); // Determine phase factor of reflector as seen by observer const double observer_reflector_phase_factor = dot(observer_reflector_direction_eus, -reflector_blackbody_direction_eus) * 0.5 + 0.5; // Measure observer reference body solid angle as seen by reflector const double reflector_observer_angular_radius = astro::angular_radius(reference_body->radius, observer_reflector_distance); const double reflector_observer_solid_angle = geom::solid_angle::cone(reflector_observer_angular_radius); // Determine phase factor of observer reference body as by reflector const double reflector_observer_phase_factor = dot(-observer_reflector_direction_eus, -observer_blackbody_direction_eus) * 0.5 + 0.5; // Calculate spectral transmittance between observer and reflector factor due to atmospheric extinction double3 observer_reflector_transmittance = {1, 1, 1}; if (reference_atmosphere) { const geom::ray ray = {{0, 0, 0}, observer_reflector_direction_eus}; observer_reflector_transmittance = integrate_transmittance(*observer, *reference_body, *reference_atmosphere, ray); } // Measure luminance of observer reference body as seen by reflector const double3 reflector_observer_luminance = observer_blackbody_illuminance * reference_body->albedo * observer_reflector_transmittance * reflector_observer_phase_factor * math::inverse_pi; // Measure illuminance from observer reference body reaching reflector const double3 reflector_observer_illuminance = reflector_observer_luminance * reflector_observer_solid_angle; // Measure luminance of reflector as seen by observer const double3 observer_reflector_luminance = (reflector_blackbody_illuminance * observer_reflector_phase_factor + reflector_observer_illuminance) * reflector.albedo * observer_reflector_transmittance * math::inverse_pi; // Measure illuminance from reflector reaching observer const double3 observer_reflector_illuminance = observer_reflector_luminance * observer_reflector_solid_angle; if (this->sky_pass) { this->sky_pass->set_moon_position(transform.local.translation); this->sky_pass->set_moon_rotation(transform.local.rotation); this->sky_pass->set_moon_angular_radius(static_cast(observer_reflector_angular_radius)); this->sky_pass->set_moon_sunlight_direction(float3(-reflector_blackbody_direction_eus)); this->sky_pass->set_moon_sunlight_illuminance(float3(reflector_blackbody_illuminance * observer_reflector_transmittance)); this->sky_pass->set_moon_planetlight_direction(float3(observer_reflector_direction_eus)); this->sky_pass->set_moon_planetlight_illuminance(float3(reflector_observer_illuminance * observer_reflector_transmittance)); this->sky_pass->set_moon_illuminance(float3(observer_reflector_illuminance / observer_reflector_transmittance), float3(observer_reflector_illuminance)); } if (this->moon_light) { const float3 reflector_up_eus = float3(icrf_to_eus.r * double3{0, 0, 1}); this->moon_light->set_color(float3(observer_reflector_illuminance)); this->moon_light->set_rotation ( math::look_rotation ( float3(-observer_reflector_direction_eus), reflector_up_eus ) ); // Bounce moon light bounce_illuminance += std::max(0.0, math::dot(bounce_normal, -observer_reflector_direction_eus)) * observer_reflector_illuminance * bounce_albedo; } }); }); if (bounce_light) { bounce_light->set_color(float3(bounce_illuminance)); } } void astronomy::set_time(double t) { time_days = t; time_centuries = time_days * physics::time::jd::centuries_per_day; } void astronomy::set_time_scale(double scale) { time_scale = scale; } void astronomy::set_observer(entity::id eid) { if (observer_eid != eid) { observer_eid = eid; if (observer_eid != entt::null) observer_modified(); else reference_body_eid = entt::null; } } void astronomy::set_transmittance_samples(std::size_t samples) { transmittance_samples = samples; } void astronomy::set_sun_light(scene::directional_light* light) { sun_light = light; } void astronomy::set_sky_light(scene::ambient_light* light) { sky_light = light; } void astronomy::set_moon_light(scene::directional_light* light) { moon_light = light; } void astronomy::set_bounce_light(scene::directional_light* light) { bounce_light = light; } void astronomy::set_bounce_albedo(const double3& albedo) { bounce_albedo = albedo; } void astronomy::set_starlight_illuminance(const double3& illuminance) { starlight_illuminance = illuminance; } void astronomy::set_sky_pass(::render::sky_pass* pass) { this->sky_pass = pass; if (sky_pass) { if (observer_eid != entt::null) { // Get pointer to observer const auto observer = registry.try_get(reference_body_eid); sky_pass->set_observer_elevation(static_cast(observer->elevation)); } if (reference_body_eid != entt::null) { // Get pointer to reference celestial body const auto reference_body = registry.try_get(reference_body_eid); if (reference_body) sky_pass->set_planet_radius(static_cast(reference_body->radius)); else sky_pass->set_planet_radius(0.0f); } } } void astronomy::on_observer_modified(entity::registry& registry, entity::id entity_id) { if (entity_id == observer_eid) observer_modified(); } void astronomy::on_observer_destroyed(entity::registry& registry, entity::id entity_id) { if (entity_id == observer_eid) observer_modified(); } void astronomy::on_celestial_body_modified(entity::registry& registry, entity::id entity_id) { if (entity_id == reference_body_eid) reference_body_modified(); } void astronomy::on_celestial_body_destroyed(entity::registry& registry, entity::id entity_id) { if (entity_id == reference_body_eid) reference_body_modified(); } void astronomy::on_orbit_modified(entity::registry& registry, entity::id entity_id) { if (entity_id == reference_body_eid) reference_orbit_modified(); } void astronomy::on_orbit_destroyed(entity::registry& registry, entity::id entity_id) { if (entity_id == reference_body_eid) reference_orbit_modified(); } void astronomy::on_atmosphere_modified(entity::registry& registry, entity::id entity_id) { if (entity_id == reference_body_eid) reference_atmosphere_modified(); } void astronomy::on_atmosphere_destroyed(entity::registry& registry, entity::id entity_id) { if (entity_id == reference_body_eid) reference_atmosphere_modified(); } void astronomy::observer_modified() { // Get pointer to observer component const auto observer = registry.try_get(observer_eid); if (observer) { if (reference_body_eid != observer->reference_body_eid) { // Reference body changed reference_body_eid = observer->reference_body_eid; reference_body_modified(); reference_orbit_modified(); reference_atmosphere_modified(); } if (reference_body_eid != entt::null) { // Get pointer to reference celestial body const auto reference_body = registry.try_get(reference_body_eid); // Update BCBF to EUS transformation if (reference_body) update_bcbf_to_eus(*observer, *reference_body); } // Upload observer elevation to sky pass if (sky_pass) sky_pass->set_observer_elevation(static_cast(observer->elevation)); } } void astronomy::reference_body_modified() { // Get pointer to reference celestial body const auto reference_body = registry.try_get(reference_body_eid); if (reference_body) { // Get pointer to observer const auto observer = registry.try_get(observer_eid); // Update BCBF to EUS transformation if (observer) update_bcbf_to_eus(*observer, *reference_body); } // Update reference celestial body-related sky pass parameters if (sky_pass) { if (reference_body) sky_pass->set_planet_radius(static_cast(reference_body->radius)); else sky_pass->set_planet_radius(0.0f); } } void astronomy::reference_orbit_modified() { } void astronomy::reference_atmosphere_modified() { } void astronomy::update_bcbf_to_eus(const game::component::observer& observer, const game::component::celestial_body& body) { // Construct BCBF to EUS transformation bcbf_to_eus = physics::orbit::frame::bcbf::to_enu ( body.radius + observer.elevation, observer.latitude, observer.longitude ) * enu_to_eus; } void astronomy::update_icrf_to_eus(const game::component::celestial_body& body, const game::component::orbit& orbit) { // Evaluate reference body orientation polynomials const double body_pole_ra = math::polynomial::horner(body.pole_ra.begin(), body.pole_ra.end(), time_centuries); const double body_pole_dec = math::polynomial::horner(body.pole_dec.begin(), body.pole_dec.end(), time_centuries); const double body_prime_meridian = math::polynomial::horner(body.prime_meridian.begin(), body.prime_meridian.end(), time_days); // Construct ICRF frame to BCBF transformation math::transformation::se3 icrf_to_bcbf = physics::orbit::frame::bci::to_bcbf ( body_pole_ra, body_pole_dec, body_prime_meridian ); icrf_to_bcbf.t = icrf_to_bcbf.r * -orbit.position; /// Construct ICRF to EUS transformation icrf_to_eus = icrf_to_bcbf * bcbf_to_eus; // Pass ICRF to EUS transformation to sky pass if (sky_pass) { // Upload topocentric frame to sky pass sky_pass->set_icrf_to_eus ( math::transformation::se3 { float3(icrf_to_eus.t), math::quaternion(icrf_to_eus.r) } ); } } double3 astronomy::integrate_transmittance(const game::component::observer& observer, const game::component::celestial_body& body, const game::component::atmosphere& atmosphere, geom::ray ray) const { double3 transmittance = {1, 1, 1}; // Make ray height relative to center of reference body ray.origin.y() += body.radius + observer.elevation; // Construct sphere representing upper limit of the atmosphere geom::sphere atmosphere_sphere; atmosphere_sphere.center = {0, 0, 0}; atmosphere_sphere.radius = body.radius + atmosphere.upper_limit; // Check for intersection between the ray and atmosphere auto intersection = geom::ray_sphere_intersection(ray, atmosphere_sphere); if (std::get<0>(intersection)) { // Get point of intersection const double3 intersection_point = ray.extrapolate(std::get<2>(intersection)); // Integrate optical of Rayleigh, Mie, and ozone particles const double optical_depth_r = physics::gas::atmosphere::optical_depth_exp(ray.origin, intersection_point, body.radius, atmosphere.rayleigh_scale_height, transmittance_samples); const double optical_depth_m = physics::gas::atmosphere::optical_depth_exp(ray.origin, intersection_point, body.radius, atmosphere.mie_scale_height, transmittance_samples); const double optical_depth_o = physics::gas::atmosphere::optical_depth_tri(ray.origin, intersection_point, body.radius, atmosphere.ozone_lower_limit, atmosphere.ozone_upper_limit, atmosphere.ozone_mode, transmittance_samples); // Calculate transmittance factor due to scattering and absorption const double3 extinction_r = atmosphere.rayleigh_scattering * optical_depth_r; const double extinction_m = atmosphere.mie_extinction * optical_depth_m; const double3 extinction_o = atmosphere.ozone_absorption * optical_depth_o; transmittance = extinction_r + double3{extinction_m, extinction_m, extinction_m} + extinction_o; transmittance.x() = std::exp(-transmittance.x()); transmittance.y() = std::exp(-transmittance.y()); transmittance.z() = std::exp(-transmittance.z()); } return transmittance; } } // namespace system } // namespace game