This commit is contained in:
wyj committed 2026-08-25 22:45:43 -04:00
commit d30f9ac56c
56 files changed
+40776

No files matched your search

+378
View File
@@ -0,0 +1,378 @@
#include "frame.h"
#include "optics.h"
#include <limits.h>
#include <math.h>
#include <omp.h>
#include <stdint.h>
#include <stdlib.h>
/* Private HDR buffers make splatting independent across workers. This is an
* allocation cap, not a rendering parameter: callers transparently fall back
* to the serial implementation when the frame is too large for two buffers. */
#define FRAME_SPLAT_MAX_PRIVATE_HDR_BYTES ((size_t)512 * 1024 * 1024)
static const double pi = 3.14159265358979323846;
static double dot(const double a[3], const double b[3]) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
static void cross(const double a[3], const double b[3], double out[3]) {
out[0] = a[1] * b[2] - a[2] * b[1];
out[1] = a[2] * b[0] - a[0] * b[2];
out[2] = a[0] * b[1] - a[1] * b[0];
}
static double normalize(double vector[3]) {
const double length = sqrt(dot(vector, vector));
if (length > 0.0)
for (int i = 0; i < 3; ++i)
vector[i] /= length;
return length;
}
static size_t vertex_index(int column, int row, int columns) {
return (size_t)row * (columns + 1) + column;
}
int frame_lens_mesh_build_coarse(FrameLensMesh *mesh, int width, int height,
int cell_pixels, double horizontal_fov_deg) {
if (mesh == NULL || width <= 0 || height <= 0 || cell_pixels <= 0 ||
horizontal_fov_deg <= 0.0 || horizontal_fov_deg >= 179.0)
return -1;
const int columns = (width + cell_pixels - 1) / cell_pixels;
const int rows = (height + cell_pixels - 1) / cell_pixels;
const size_t vertex_count = (size_t)(columns + 1) * (rows + 1);
const size_t triangle_count = (size_t)columns * rows * 2;
LensVertex *vertices = calloc(vertex_count, sizeof *vertices);
LensTriangle *triangles = malloc(triangle_count * sizeof *triangles);
if (vertices == NULL || triangles == NULL) {
free(vertices);
free(triangles);
return -1;
}
const double tan_half_x = tan(horizontal_fov_deg * pi / 360.0);
const double tan_half_y = tan_half_x * (double)height / width;
for (int row = 0; row <= rows; ++row) {
const double image_y = (double)row * height / rows;
for (int column = 0; column <= columns; ++column) {
LensVertex *vertex = &vertices[vertex_index(column, row, columns)];
vertex->image_x = (double)column * width / columns;
vertex->image_y = image_y;
vertex->camera_direction[0] = 1.0;
vertex->camera_direction[1] = (0.5 - image_y / height) * 2.0 * tan_half_y;
vertex->camera_direction[2] =
(vertex->image_x / width - 0.5) * 2.0 * tan_half_x;
normalize(vertex->camera_direction);
}
}
size_t next_triangle = 0;
for (int row = 0; row < rows; ++row)
for (int column = 0; column < columns; ++column) {
const size_t top_left = vertex_index(column, row, columns);
const size_t top_right = vertex_index(column + 1, row, columns);
const size_t bottom_left = vertex_index(column, row + 1, columns);
const size_t bottom_right = vertex_index(column + 1, row + 1, columns);
triangles[next_triangle++] =
(LensTriangle){{top_left, bottom_left, bottom_right}};
triangles[next_triangle++] =
(LensTriangle){{top_left, bottom_right, top_right}};
}
*mesh = (FrameLensMesh){.vertices = vertices,
.triangles = triangles,
.vertex_count = vertex_count,
.triangle_count = triangle_count};
return 0;
}
int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
const ObserverState *observer,
const GeodesicTraceConfig *trace) {
if (mesh == NULL || spacetime == NULL || observer == NULL || trace == NULL)
return -1;
/* Each iteration exclusively owns one vertex. SpacetimeSource is shared
* read-only here; backends with mutable evaluation state must keep it
* thread-local. */
#pragma omp parallel for schedule(static)
for (size_t i = 0; i < mesh->vertex_count; ++i) {
LensVertex *vertex = &mesh->vertices[i];
RayEndpoint endpoint = geodesic_trace_past(spacetime, observer,
vertex->camera_direction, trace);
vertex->status = endpoint.status;
if (endpoint.status == RAY_ENDPOINT_ESCAPED) {
for (int axis = 0; axis < 3; ++axis)
vertex->n_infinity[axis] = endpoint.n_infinity[axis];
vertex->log_frequency_ratio = log(endpoint.frequency_ratio);
}
}
return 0;
}
static double spherical_area(const double a[3], const double b[3],
const double c[3]) {
double b_cross_c[3];
cross(b, c, b_cross_c);
return 2.0 * atan2(fabs(dot(a, b_cross_c)),
1.0 + dot(a, b) + dot(b, c) + dot(c, a));
}
static int spherical_barycentric(const double point[3], const double a[3],
const double b[3], const double c[3],
double weights[3]) {
const double area = spherical_area(a, b, c);
double edge_cross[3];
if (area < 1e-14)
return -1;
const double *corners[3] = {a, b, c};
for (int edge = 0; edge < 3; ++edge) {
const double *left = corners[edge];
const double *right = corners[(edge + 1) % 3];
const double *opposite = corners[(edge + 2) % 3];
cross(left, right, edge_cross);
/* This is a sign test, so its tolerance must scale with the source
* triangle. A fixed absolute threshold turns sufficiently fine triangles
* into near-all-sky queries. */
if (dot(edge_cross, point) * dot(edge_cross, opposite) <
-1e-14 * dot(edge_cross, edge_cross))
return -1;
}
weights[0] = spherical_area(point, b, c) / area;
weights[1] = spherical_area(point, c, a) / area;
weights[2] = spherical_area(point, a, b) / area;
return 0;
}
static int usable_triangle(const FrameLensMesh *mesh,
const LensTriangle *triangle,
const LensVertex *vertices[3]) {
for (int i = 0; i < 3; ++i) {
vertices[i] = &mesh->vertices[triangle->vertex[i]];
if (vertices[i]->status != RAY_ENDPOINT_ESCAPED)
return 0;
}
return spherical_area(vertices[0]->n_infinity, vertices[1]->n_infinity,
vertices[2]->n_infinity) >= 1e-14;
}
/* Give a source lying exactly on a shared source edge to one triangle only.
* Interior overlaps remain valid separate lens images. */
static int owns_source_boundary(const LensTriangle *triangle,
const double weights[3]) {
for (int opposite = 0; opposite < 3; ++opposite) {
if (weights[opposite] > 1e-11)
continue;
const size_t left = triangle->vertex[(opposite + 1) % 3];
const size_t right = triangle->vertex[(opposite + 2) % 3];
if (left > right)
return 0;
}
return 1;
}
static size_t splat_catalog_triangles(const FrameLensMesh *mesh,
const StarCatalog *catalog, double *hdr,
int width, int height, double exposure,
const PointSpreadFunction *psf,
size_t first_triangle,
size_t last_triangle) {
size_t images = 0;
for (size_t t = first_triangle; t < last_triangle; ++t) {
const LensVertex *vertex[3];
if (!usable_triangle(mesh, &mesh->triangles[t], vertex))
continue;
const double source_area = spherical_area(
vertex[0]->n_infinity, vertex[1]->n_infinity, vertex[2]->n_infinity);
const double image_area =
spherical_area(vertex[0]->camera_direction, vertex[1]->camera_direction,
vertex[2]->camera_direction);
const double magnification = image_area / source_area;
for (size_t s = 0; s < catalog->count; ++s) {
const Star *star = &catalog->stars[s];
double weights[3];
if (spherical_barycentric(star->direction, vertex[0]->n_infinity,
vertex[1]->n_infinity, vertex[2]->n_infinity,
weights))
continue;
if (!owns_source_boundary(&mesh->triangles[t], weights))
continue;
const double image_x = weights[0] * vertex[0]->image_x +
weights[1] * vertex[1]->image_x +
weights[2] * vertex[2]->image_x;
const double image_y = weights[0] * vertex[0]->image_y +
weights[1] * vertex[1]->image_y +
weights[2] * vertex[2]->image_y;
const double log_g = weights[0] * vertex[0]->log_frequency_ratio +
weights[1] * vertex[1]->log_frequency_ratio +
weights[2] * vertex[2]->log_frequency_ratio;
const LinearRgb color =
blackbody_to_linear_rgb(star->temperature_K * exp(log_g));
splat_moffat(hdr, width, height, image_x, image_y, color,
exposure * star->amplitude * magnification, psf);
++images;
}
}
return images;
}
size_t frame_splat_catalog(const FrameLensMesh *mesh,
const StarCatalog *catalog, double *hdr, int width,
int height, double exposure,
const PointSpreadFunction *psf) {
if (mesh == NULL || catalog == NULL || hdr == NULL || exposure <= 0.0 ||
psf == NULL || width <= 0 || height <= 0)
return 0;
const size_t pixel_count = (size_t)width * height * 3;
if (pixel_count > SIZE_MAX / sizeof(double) ||
pixel_count * sizeof(double) > FRAME_SPLAT_MAX_PRIVATE_HDR_BYTES / 2)
return splat_catalog_triangles(mesh, catalog, hdr, width, height, exposure,
psf, 0, mesh->triangle_count);
const size_t buffer_bytes = pixel_count * sizeof(double);
size_t worker_count = FRAME_SPLAT_MAX_PRIVATE_HDR_BYTES / buffer_bytes;
const int max_threads = omp_get_max_threads();
if (worker_count > (size_t)max_threads)
worker_count = (size_t)max_threads;
if (worker_count < 2 || worker_count > INT_MAX)
return splat_catalog_triangles(mesh, catalog, hdr, width, height, exposure,
psf, 0, mesh->triangle_count);
double **private_hdr = calloc(worker_count, sizeof *private_hdr);
if (private_hdr == NULL)
return splat_catalog_triangles(mesh, catalog, hdr, width, height, exposure,
psf, 0, mesh->triangle_count);
size_t allocated = 0;
for (; allocated < worker_count; ++allocated) {
private_hdr[allocated] = calloc(pixel_count, sizeof **private_hdr);
if (private_hdr[allocated] == NULL)
break;
}
if (allocated != worker_count) {
while (allocated > 0)
free(private_hdr[--allocated]);
free(private_hdr);
return splat_catalog_triangles(mesh, catalog, hdr, width, height, exposure,
psf, 0, mesh->triangle_count);
}
size_t images = 0;
#pragma omp parallel num_threads((int)worker_count) reduction(+ : images)
{
const size_t worker = (size_t)omp_get_thread_num();
const size_t first = mesh->triangle_count * worker / worker_count;
const size_t last = mesh->triangle_count * (worker + 1) / worker_count;
images += splat_catalog_triangles(mesh, catalog, private_hdr[worker], width,
height, exposure, psf, first, last);
}
#pragma omp parallel for schedule(static)
for (size_t pixel = 0; pixel < pixel_count; ++pixel)
for (size_t worker = 0; worker < worker_count; ++worker)
hdr[pixel] += private_hdr[worker][pixel];
for (size_t worker = 0; worker < worker_count; ++worker)
free(private_hdr[worker]);
free(private_hdr);
return images;
}
static void blend_gray(double *hdr, int width, int height, int x, int y,
double gray, double alpha) {
if (x < 0 || x >= width || y < 0 || y >= height)
return;
double *pixel = &hdr[3 * (y * width + x)];
for (int channel = 0; channel < 3; ++channel)
pixel[channel] = (1.0 - alpha) * pixel[channel] + alpha * gray;
}
static double fractional_part(double value) { return value - floor(value); }
static void plot_aa(double *hdr, int width, int height, int steep, int x, int y,
double coverage, double gray, double opacity) {
if (coverage > 0.0)
blend_gray(hdr, width, height, steep ? y : x, steep ? x : y, gray,
coverage * opacity);
}
/* Xiaolin Wu line rasterization: a one-pixel line with coverage-based alpha. */
static void draw_line(double *hdr, int width, int height,
const LensVertex *from, const LensVertex *to, double gray,
double opacity) {
double x0 = from->image_x, y0 = from->image_y;
double x1 = to->image_x, y1 = to->image_y;
const int steep = fabs(y1 - y0) > fabs(x1 - x0);
if (steep) {
double swap = x0;
x0 = y0;
y0 = swap;
swap = x1;
x1 = y1;
y1 = swap;
}
if (x0 > x1) {
double swap = x0;
x0 = x1;
x1 = swap;
swap = y0;
y0 = y1;
y1 = swap;
}
const double dx = x1 - x0;
if (dx == 0.0) {
plot_aa(hdr, width, height, steep, (int)lround(x0), (int)floor(y0), 1.0,
gray, opacity);
return;
}
const double gradient = (y1 - y0) / dx;
double x_end = round(x0);
double y_end = y0 + gradient * (x_end - x0);
double x_gap = 1.0 - fractional_part(x0 + 0.5);
int x_pixel_start = (int)x_end;
int y_pixel = (int)floor(y_end);
plot_aa(hdr, width, height, steep, x_pixel_start, y_pixel,
(1.0 - fractional_part(y_end)) * x_gap, gray, opacity);
plot_aa(hdr, width, height, steep, x_pixel_start, y_pixel + 1,
fractional_part(y_end) * x_gap, gray, opacity);
double inter_y = y_end + gradient;
x_end = round(x1);
y_end = y1 + gradient * (x_end - x1);
x_gap = fractional_part(x1 + 0.5);
const int x_pixel_end = (int)x_end;
y_pixel = (int)floor(y_end);
plot_aa(hdr, width, height, steep, x_pixel_end, y_pixel,
(1.0 - fractional_part(y_end)) * x_gap, gray, opacity);
plot_aa(hdr, width, height, steep, x_pixel_end, y_pixel + 1,
fractional_part(y_end) * x_gap, gray, opacity);
for (int x = x_pixel_start + 1; x < x_pixel_end; ++x) {
y_pixel = (int)floor(inter_y);
plot_aa(hdr, width, height, steep, x, y_pixel,
1.0 - fractional_part(inter_y), gray, opacity);
plot_aa(hdr, width, height, steep, x, y_pixel + 1, fractional_part(inter_y),
gray, opacity);
inter_y += gradient;
}
}
void frame_draw_mesh(const FrameLensMesh *mesh, double *hdr, int width,
int height, double gray, double opacity) {
if (mesh == NULL || hdr == NULL || width <= 0 || height <= 0 || gray < 0.0 ||
opacity < 0.0 || opacity > 1.0)
return;
for (size_t i = 0; i < mesh->triangle_count; ++i) {
const LensTriangle *triangle = &mesh->triangles[i];
for (int edge = 0; edge < 3; ++edge) {
const size_t from_id = triangle->vertex[edge];
const size_t to_id = triangle->vertex[(edge + 1) % 3];
if (from_id < to_id)
draw_line(hdr, width, height, &mesh->vertices[from_id],
&mesh->vertices[to_id], gray, opacity);
}
}
}
void frame_lens_mesh_destroy(FrameLensMesh *mesh) {
if (mesh == NULL)
return;
free(mesh->vertices);
free(mesh->triangles);
*mesh = (FrameLensMesh){0};
}