From b2ac642de72811f5a7c4d128376f531a4cf0c53c Mon Sep 17 00:00:00 2001 From: Yingjie Wang Date: Mon, 31 Aug 2026 00:13:37 -0400 Subject: [PATCH] Fix: correct ICRS camera orientation --- README.md | 9 ++++++--- src/catalog.c | 14 ++++++++------ src/main.c | 6 ++++-- src/observer.c | 22 ++++++++++++---------- src/observer.h | 5 +++-- tests/test_geodesic.c | 8 ++++++++ tests/test_schwarzschild.c | 10 ++++++---- 7 files changed, 47 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 48b6012..3fc2d63 100644 --- a/README.md +++ b/README.md @@ -210,9 +210,12 @@ newly inserted vertices and geometry-only longest-edge probes for its new leaves are collected together, so both ray sets use the same parallel `RayPool` pass. -`--look-ra-deg` and `--look-dec-deg` rotate that fixed tetrad so its forward -axis is the corresponding catalog direction; their defaults reproduce the -original `-Z` view. `--exposure` converts a catalog's physical flux +Catalog directions and `--look-ra-deg`/`--look-dec-deg` use standard +right-handed ICRS Cartesian axes: `+X` is RA 0 degrees/Dec 0 degrees, `+Y` is +RA 90 degrees/Dec 0 degrees, and `+Z` is the north celestial pole. The local +camera axes are forward, celestial north, and celestial west, so an image with +north up has decreasing RA to the right. The defaults preserve the original +`-Z` view. `--exposure` converts a catalog's physical flux normalization to the prototype HDR scale. The current synthetic catalog is calibrated for default exposure `1e-3`; a 2MASS blackbody normalization in steradians requires a much larger display exposure such as the example above. diff --git a/src/catalog.c b/src/catalog.c index 96c3242..5289be5 100644 --- a/src/catalog.c +++ b/src/catalog.c @@ -91,9 +91,11 @@ int catalog_load_csv(StarCatalog *catalog, const char *path) const double lat = latitude * PI / 180.0; const double cos_lat = cos(lat); Star *star = &catalog->stars[catalog->count++]; + /* Standard right-handed ICRS Cartesian axes: X is (RA, Dec) = + * (0, 0), Y is (90, 0), and Z is the north celestial pole. */ star->direction[0] = cos_lat * cos(lon); - star->direction[1] = sin(lat); - star->direction[2] = cos_lat * sin(lon); + star->direction[1] = cos_lat * sin(lon); + star->direction[2] = sin(lat); star->temperature_K = temperature; star->amplitude = amplitude; } @@ -170,10 +172,10 @@ int catalog_load_all_sky(StarCatalog *catalog, const char *directory) static void lon_lat_from_direction(const double direction[3], double *longitude, double *latitude) { - *longitude = atan2(direction[2], direction[0]) * 180.0 / PI; + *longitude = atan2(direction[1], direction[0]) * 180.0 / PI; if (*longitude < 0.0) *longitude += 360.0; - *latitude = asin(fmax(-1.0, fmin(1.0, direction[1]))) * 180.0 / PI; + *latitude = asin(fmax(-1.0, fmin(1.0, direction[2]))) * 180.0 / PI; } static double dot(const double a[3], const double b[3]) @@ -195,8 +197,8 @@ static void direction_from_lon_lat(double longitude, double latitude, const double lat = latitude * PI / 180.0; const double cos_lat = cos(lat); direction[0] = cos_lat * cos(lon); - direction[1] = sin(lat); - direction[2] = cos_lat * sin(lon); + direction[1] = cos_lat * sin(lon); + direction[2] = sin(lat); } /* The minimum of a plane dot-product over a longitude/latitude rectangle is diff --git a/src/main.c b/src/main.c index 19b3775..bf37df8 100644 --- a/src/main.c +++ b/src/main.c @@ -183,8 +183,10 @@ static int parse_args(int argc, char **argv, Settings *s, .verbose = 1, #endif .horizontal_fov_deg = 30.0, - .look_ra_deg = 270.0, - .look_dec_deg = 0.0, + /* Preserve the legacy -Z view after switching to standard + * right-handed ICRS Cartesian axes. */ + .look_ra_deg = 90.0, + .look_dec_deg = -90.0, .exposure = 1e-3, .max_magnification = INFINITY, .max_cache_psf_flux = 1.0, diff --git a/src/observer.c b/src/observer.c index 2afd6e4..c4cbd11 100644 --- a/src/observer.c +++ b/src/observer.c @@ -19,9 +19,11 @@ ObserverState observer_fixed_at_origin_look_at(double ra_deg, double dec_deg) { const double dec = dec_deg * pi / 180.0; const double cos_ra = cos(ra), sin_ra = sin(ra); const double cos_dec = cos(dec), sin_dec = sin(dec); - const double forward[3] = {cos_dec * cos_ra, sin_dec, cos_dec * sin_ra}; - const double up[3] = {-sin_dec * cos_ra, cos_dec, -sin_dec * sin_ra}; - const double right[3] = {-sin_ra, 0.0, cos_ra}; + const double forward[3] = {cos_dec * cos_ra, cos_dec * sin_ra, sin_dec}; + const double up[3] = {-sin_dec * cos_ra, -sin_dec * sin_ra, cos_dec}; + /* forward cross celestial north is celestial west: with north up, image + * right is decreasing RA. */ + const double right[3] = {sin_ra, -cos_ra, 0.0}; return (ObserverState){.coordinate_time = 0.0, .coordinate_position = {0.0, 0.0, 0.0}, .tetrad = {{1.0, 0.0, 0.0, 0.0}, @@ -41,14 +43,14 @@ static int schwarzschild_look_direction(double ra_deg, double dec_deg, const double cos_ra = cos(ra), sin_ra = sin(ra); const double cos_dec = cos(dec), sin_dec = sin(dec); direction[0] = cos_dec * cos_ra; - direction[1] = sin_dec; - direction[2] = cos_dec * sin_ra; + direction[1] = cos_dec * sin_ra; + direction[2] = sin_dec; up[0] = -sin_dec * cos_ra; - up[1] = cos_dec; - up[2] = -sin_dec * sin_ra; - right[0] = -sin_ra; - right[1] = 0.0; - right[2] = cos_ra; + up[1] = -sin_dec * sin_ra; + up[2] = cos_dec; + right[0] = sin_ra; + right[1] = -cos_ra; + right[2] = 0.0; return 0; } diff --git a/src/observer.h b/src/observer.h index 1d9f1d1..6b7cb15 100644 --- a/src/observer.h +++ b/src/observer.h @@ -9,8 +9,9 @@ typedef struct { } ObserverState; ObserverState observer_fixed_at_origin(void); -/* Point the fixed inertial observer at an ICRS-style RA/Dec direction. - * The local spatial axes remain (forward, celestial north, increasing RA). */ +/* Point the fixed inertial observer at a standard right-handed ICRS RA/Dec + * direction. The local spatial axes are (forward, celestial north, + * celestial west), so a north-up image has decreasing RA to the right. */ ObserverState observer_fixed_at_origin_look_at(double ra_deg, double dec_deg); /* Static camera at Cartesian Kerr--Schild position -radius * look_direction, * directed toward the Schwarzschild black hole at the origin. look_direction diff --git a/tests/test_geodesic.c b/tests/test_geodesic.c index 164be8e..6cbf7a2 100644 --- a/tests/test_geodesic.c +++ b/tests/test_geodesic.c @@ -45,6 +45,14 @@ int main(void) { result = result || check_ray(&source, &look_at_ra_zero, (double[]){1.0, 0.0, 0.0}, (double[]){1.0, 0.0, 0.0}); + /* Standard ICRS has +Z at the north celestial pole and +Y at increasing + * RA. A right-handed north-up camera consequently has west to its right. */ + result = result || check_ray(&source, &look_at_ra_zero, + (double[]){0.0, 1.0, 0.0}, + (double[]){0.0, 0.0, 1.0}) || + check_ray(&source, &look_at_ra_zero, + (double[]){0.0, 0.0, 1.0}, + (double[]){0.0, -1.0, 0.0}); ObserverTrack accelerated = {0}; ObserverState final_observer; if (observer_track_generate_minkowski_acceleration(&accelerated, 1.52, 2.0, diff --git a/tests/test_schwarzschild.c b/tests/test_schwarzschild.c index c0cb151..3a7109d 100644 --- a/tests/test_schwarzschild.c +++ b/tests/test_schwarzschild.c @@ -22,12 +22,14 @@ int main(void) { observer_static_schwarzschild_ks_look_at(1.0, 40.0, 270.0, 30.0, &oriented_observer) || fabs(oriented_observer.coordinate_position[0]) > 1e-12 || - fabs(oriented_observer.coordinate_position[1] + 20.0) > 1e-12 || - fabs(oriented_observer.coordinate_position[2] - 20.0 * sqrt(3.0)) > + fabs(oriented_observer.coordinate_position[1] - 20.0 * sqrt(3.0)) > + 1e-12 || + fabs(oriented_observer.coordinate_position[2] + 20.0) > 1e-12 || fabs(oriented_observer.tetrad[1][1]) > 1e-12 || - fabs(oriented_observer.tetrad[1][2] - 0.5 * sqrt(0.95)) > 1e-12 || - fabs(oriented_observer.tetrad[1][3] + sqrt(0.95) * sqrt(3.0) / 2.0) > + fabs(oriented_observer.tetrad[1][2] + sqrt(0.95) * sqrt(3.0) / 2.0) > + 1e-12 || + fabs(oriented_observer.tetrad[1][3] - 0.5 * sqrt(0.95)) > 1e-12 || observer_inward_schwarzschild_ks(1.0, 30.0, 0.5, &inward_observer) ||