/*
* 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 "entity/systems/astronomy.hpp"
#include "astro/apparent-size.hpp"
#include "entity/components/blackbody.hpp"
#include "entity/components/transform.hpp"
#include "entity/components/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 entity {
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_replace().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_replace().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_replace().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_replace().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_replace().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_replace().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_replace().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_replace().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(math::type_cast(r_eus));
transform.local.rotation = math::type_cast(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(
[&](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
(
math::type_cast(-observer_blackbody_direction_eus),
math::type_cast(blackbody_up_eus)
)
);
sun_light->set_color(math::type_cast(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(math::type_cast(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(math::type_cast(blackbody_position_eus));
this->sky_pass->set_sun_luminance(math::type_cast(blackbody.luminance));
this->sky_pass->set_sun_illuminance(math::type_cast(observer_blackbody_illuminance), math::type_cast(observer_blackbody_transmitted_illuminance));
this->sky_pass->set_sun_angular_radius(static_cast(observer_blackbody_angular_radius));
}
// Update diffuse reflectors
this->registry.view().each(
[&](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(math::type_cast(-reflector_blackbody_direction_eus));
this->sky_pass->set_moon_sunlight_illuminance(math::type_cast(reflector_blackbody_illuminance * observer_reflector_transmittance));
this->sky_pass->set_moon_planetlight_direction(math::type_cast(observer_reflector_direction_eus));
this->sky_pass->set_moon_planetlight_illuminance(math::type_cast(reflector_observer_illuminance * observer_reflector_transmittance));
this->sky_pass->set_moon_illuminance(math::type_cast(observer_reflector_illuminance / observer_reflector_transmittance), math::type_cast(observer_reflector_illuminance));
}
if (this->moon_light)
{
const float3 reflector_up_eus = math::type_cast(icrf_to_eus.r * double3{0, 0, 1});
this->moon_light->set_color(math::type_cast(observer_reflector_illuminance));
this->moon_light->set_rotation
(
math::look_rotation
(
math::type_cast(-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(math::type_cast(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, entity::component::observer& component)
{
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, entity::component::celestial_body& component)
{
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, entity::component::orbit& component)
{
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, entity::component::atmosphere& component)
{
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 entity::component::observer& observer, const entity::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 entity::component::celestial_body& body, const entity::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
{
math::type_cast(icrf_to_eus.t),
math::type_cast(icrf_to_eus.r)
}
);
}
}
double3 astronomy::integrate_transmittance(const entity::component::observer& observer, const entity::component::celestial_body& body, const entity::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 entity