init
This commit is contained in:
commit
d30f9ac56c
56 files changed
+40776
No files matched your search
+378
@@ -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};
|
||||
}
|
||||
Reference in new issue
Block a user