Frame: refine critical lens regions

This commit is contained in:
wyj committed 2026-08-29 00:44:20 -04:00
1 parent 48dcf4e707
commit 4c575fa8b5
7 files changed
+92 -10

No files matched your search

+20 -5
View File
@@ -147,13 +147,15 @@ production refinement threshold.
Adaptive refinement is disabled by default (`--refine-max-level 0`), so the Adaptive refinement is disabled by default (`--refine-max-level 0`), so the
existing coarse-mesh renders remain unchanged. When enabled, its defaults are existing coarse-mesh renders remain unchanged. When enabled, its defaults are
an absolute direction error of `1e-3` degrees, relative error `0.1`, minimum an absolute direction error of `1e-3` degrees, relative error `0.1`, minimum
long edge `0.5` pixels, and minimum area `0.25` pixel-squared. Each value can long edge `0.5` pixels, minimum area `0.25` pixel-squared, and a provisional
be overridden independently: minimum discrete-Jacobian magnitude of `1e-3`. Each value can be overridden
independently:
```text ```text
--refine-max-level N --refine-max-level N
--refine-angle-abs-deg D --refine-angle-abs-deg D
--refine-angle-rel R --refine-angle-rel R
--refine-jacobian-min J
--refine-min-edge-pixels P --refine-min-edge-pixels P
--refine-min-area-pixels2 A --refine-min-area-pixels2 A
``` ```
@@ -171,8 +173,20 @@ prevent selecting a leaf already at or below the requested image-plane
long-edge and area scales. long-edge and area scales.
Triangles whose three vertices disagree between capture and escape are split Triangles whose three vertices disagree between capture and escape are split
independently of the direction-error thresholds, allowing the mesh to follow a independently of the direction-error thresholds, allowing the mesh to follow a
shadow boundary. This first implementation deliberately does not evaluate shadow boundary.
orientation or Jacobian criteria.
Independently of the midpoint geometry test, an all-escaped triangle also
computes the discrete lens Jacobian
`J = Omega_source / Omega_image`. Both signed solid angles use
`2 atan2(dot(a, cross(b,c)), 1 + dot(a,b) + dot(b,c) + dot(c,a))`, with the
ordered camera directions for `Omega_image` and their traced infinity
directions for `Omega_source`. `abs(J) < --refine-jacobian-min` requests a
split near a critical curve. The signed-area calculation retains the sign of
`J`, but sign changes across adjacent triangles are not currently a refinement
trigger; this makes it possible to evaluate the `abs(J)` criterion separately.
The `1e-3` default is deliberately provisional and should be tuned with the
small Schwarzschild refinement diagnostic before being treated as a production
threshold.
For a short Schwarzschild diagnostic that permits at most one actual split For a short Schwarzschild diagnostic that permits at most one actual split
generation, for example: generation, for example:
@@ -181,7 +195,8 @@ generation, for example:
./build/schwarzschild_sky --catalog assets/sky_grid_5deg.csv \ ./build/schwarzschild_sky --catalog assets/sky_grid_5deg.csv \
--width 48 --height 48 --coarse-cell-pixels 24 --fov-deg 40 \ --width 48 --height 48 --coarse-cell-pixels 24 --fov-deg 40 \
--refine-max-level 1 --refine-angle-abs-deg 0.001 \ --refine-max-level 1 --refine-angle-abs-deg 0.001 \
--refine-angle-rel 0.001 --refine-min-edge-pixels 1 \ --refine-angle-rel 0.001 --refine-jacobian-min 0.001 \
--refine-min-edge-pixels 1 \
--refine-min-area-pixels2 1 --draw-mesh \ --refine-min-area-pixels2 1 --draw-mesh \
--output output/imgs/schwarzschild_refinement.png --output output/imgs/schwarzschild_refinement.png
``` ```
+35
View File
@@ -335,6 +335,37 @@ static double direction_angle(const double a[3], const double b[3]) {
return acos(product); return acos(product);
} }
static double spherical_signed_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(dot(a, b_cross_c),
1.0 + dot(a, b) + dot(b, c) + dot(c, a));
}
/* Returns whether the discrete source/image solid-angle ratio is available.
* Captured and degenerate image triangles have no reliable value. */
static int discrete_jacobian(const FrameLensMesh *mesh,
const LensTriangle *triangle, double *value) {
const LensVertex *a = &mesh->vertices[triangle->vertex[0]];
const LensVertex *b = &mesh->vertices[triangle->vertex[1]];
const LensVertex *c = &mesh->vertices[triangle->vertex[2]];
if (a->status != RAY_ENDPOINT_ESCAPED || b->status != RAY_ENDPOINT_ESCAPED ||
c->status != RAY_ENDPOINT_ESCAPED)
return 0;
const double image_area = spherical_signed_area(
a->camera_direction, b->camera_direction, c->camera_direction);
if (!isfinite(image_area) || fabs(image_area) <= 1e-15)
return 0;
const double source_area = spherical_signed_area(
a->n_infinity, b->n_infinity, c->n_infinity);
const double jacobian = source_area / image_area;
if (!isfinite(jacobian))
return 0;
*value = jacobian;
return 1;
}
static int probe_requires_split(const FrameLensMesh *mesh, static int probe_requires_split(const FrameLensMesh *mesh,
const LensTriangle *triangle, const LensTriangle *triangle,
unsigned int side, unsigned int side,
@@ -441,6 +472,10 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
if (allowed[i]) { if (allowed[i]) {
const unsigned int side = longest_side(mesh, triangle); const unsigned int side = longest_side(mesh, triangle);
requested[3 * i + side] = probe_requires_split(mesh, triangle, side, config); requested[3 * i + side] = probe_requires_split(mesh, triangle, side, config);
double jacobian;
if (discrete_jacobian(mesh, triangle, &jacobian) &&
fabs(jacobian) < config->jacobian_minimum)
requested[3 * i + side] = 1;
} }
} }
qsort(edges, edge_count, sizeof *edges, compare_mesh_edge); qsort(edges, edge_count, sizeof *edges, compare_mesh_edge);
+7 -3
View File
@@ -28,11 +28,15 @@ typedef struct {
unsigned int max_level; unsigned int max_level;
double angle_absolute_rad; double angle_absolute_rad;
double angle_relative; double angle_relative;
double jacobian_minimum;
double min_edge_pixels; double min_edge_pixels;
double min_area_pixels2; double min_area_pixels2;
} RefinementConfig; } RefinementConfig;
typedef enum { FRAME_SAMPLE_VERTEX, FRAME_SAMPLE_PROBE } FrameSampleKind; typedef enum {
FRAME_SAMPLE_VERTEX,
FRAME_SAMPLE_PROBE
} FrameSampleKind;
typedef struct { typedef struct {
FrameSampleKind kind; FrameSampleKind kind;
@@ -74,8 +78,8 @@ int frame_lens_mesh_build_coarse(FrameLensMesh *mesh, int width, int height,
int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime, int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
const ObserverState *observer, const ObserverState *observer,
const GeodesicTraceConfig *trace); const GeodesicTraceConfig *trace);
/* Builds exactly one generation of requests. Probe results must be installed /* Builds one generation of vertex/probe ray requests. Results are installed
* only after the caller has completed the generation's tracing sweep. */ * only after the caller has completed the generation's sweep. */
int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh, int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
const RefinementConfig *config); const RefinementConfig *config);
const FrameSample *frame_lens_mesh_samples(const FrameLensMesh *mesh, const FrameSample *frame_lens_mesh_samples(const FrameLensMesh *mesh,
+4
View File
@@ -154,6 +154,7 @@ static int parse_args(int argc, char **argv, Settings *s,
.refinement = {.angle_absolute_rad = .refinement = {.angle_absolute_rad =
1e-3 * 3.14159265358979323846 / 180.0, 1e-3 * 3.14159265358979323846 / 180.0,
.angle_relative = 0.1, .angle_relative = 0.1,
.jacobian_minimum = 1e-3,
.min_edge_pixels = 0.5, .min_edge_pixels = 0.5,
.min_area_pixels2 = 0.25}}; .min_area_pixels2 = 0.25}};
*write_path = NULL; *write_path = NULL;
@@ -181,6 +182,8 @@ static int parse_args(int argc, char **argv, Settings *s,
s->refinement.angle_absolute_rad *= 3.14159265358979323846 / 180.0; s->refinement.angle_absolute_rad *= 3.14159265358979323846 / 180.0;
} else if (!strcmp(argv[i], "--refine-angle-rel") && i + 1 < argc && } else if (!strcmp(argv[i], "--refine-angle-rel") && i + 1 < argc &&
!parse_positive(argv[++i], &s->refinement.angle_relative)) { !parse_positive(argv[++i], &s->refinement.angle_relative)) {
} else if (!strcmp(argv[i], "--refine-jacobian-min") && i + 1 < argc &&
!parse_positive(argv[++i], &s->refinement.jacobian_minimum)) {
} else if (!strcmp(argv[i], "--refine-min-edge-pixels") && i + 1 < argc && } else if (!strcmp(argv[i], "--refine-min-edge-pixels") && i + 1 < argc &&
!parse_positive(argv[++i], &s->refinement.min_edge_pixels)) { !parse_positive(argv[++i], &s->refinement.min_edge_pixels)) {
} else if (!strcmp(argv[i], "--refine-min-area-pixels2") && i + 1 < argc && } else if (!strcmp(argv[i], "--refine-min-area-pixels2") && i + 1 < argc &&
@@ -528,6 +531,7 @@ int main(int argc, char **argv) {
#endif #endif
"[--coarse-cell-pixels N] [--refine-max-level N " "[--coarse-cell-pixels N] [--refine-max-level N "
"--refine-angle-abs-deg D --refine-angle-rel R " "--refine-angle-abs-deg D --refine-angle-rel R "
"--refine-jacobian-min J "
"--refine-min-edge-pixels P --refine-min-area-pixels2 A] " "--refine-min-edge-pixels P --refine-min-area-pixels2 A] "
"[--draw-mesh] [--write-catalog PATH] " "[--draw-mesh] [--write-catalog PATH] "
"[--catalog-load-workers N] " "[--catalog-load-workers N] "
+24 -1
View File
@@ -164,9 +164,10 @@ int main(void) {
* shared diagonal probe must produce one stable midpoint and conforming * shared diagonal probe must produce one stable midpoint and conforming
* children only after its endpoint has been installed. */ * children only after its endpoint has been installed. */
FrameLensMesh adaptive_mesh = {0}; FrameLensMesh adaptive_mesh = {0};
const RefinementConfig refine = {.max_level = 1, RefinementConfig refine = {.max_level = 1,
.angle_absolute_rad = 1e-4, .angle_absolute_rad = 1e-4,
.angle_relative = 1e-4, .angle_relative = 1e-4,
.jacobian_minimum = 1e-3,
.min_edge_pixels = 1.0, .min_edge_pixels = 1.0,
.min_area_pixels2 = 1.0}; .min_area_pixels2 = 1.0};
if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0)) if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0))
@@ -192,6 +193,28 @@ int main(void) {
goto done; goto done;
} }
frame_lens_mesh_destroy(&adaptive_mesh); frame_lens_mesh_destroy(&adaptive_mesh);
/* A zero discrete Jacobian is an independent fold/critical-band trigger:
* the probe below agrees exactly with linear endpoint interpolation, so the
* direction-error criterion cannot request this split. */
if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0))
goto done;
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
adaptive_mesh.vertices[i].traced = 1;
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
adaptive_mesh.vertices[i].n_infinity[0] = 1.0;
}
const RayEndpoint flat_probe = {.n_infinity = {1.0, 0.0, 0.0},
.frequency_ratio = 1.0,
.status = RAY_ENDPOINT_ESCAPED};
if (frame_lens_mesh_prepare_generation(&adaptive_mesh, &refine) != 1 ||
frame_lens_mesh_install_sample(&adaptive_mesh, 0, &flat_probe) ||
frame_lens_mesh_finish_generation(&adaptive_mesh, &refine) != 1 ||
adaptive_mesh.vertex_count != 5 || adaptive_mesh.triangle_count != 4) {
fputs("adaptive discrete-Jacobian split regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
frame_lens_mesh_destroy(&adaptive_mesh);
result = 0; result = 0;
done: done:
frame_lens_mesh_destroy(&mesh); frame_lens_mesh_destroy(&mesh);
+1
View File
@@ -64,6 +64,7 @@ int main(void) {
const RefinementConfig refinement = {.max_level = 1, const RefinementConfig refinement = {.max_level = 1,
.angle_absolute_rad = 1e-5, .angle_absolute_rad = 1e-5,
.angle_relative = 1e-5, .angle_relative = 1e-5,
.jacobian_minimum = 1e-3,
.min_edge_pixels = 1.0, .min_edge_pixels = 1.0,
.min_area_pixels2 = 1.0}; .min_area_pixels2 = 1.0};
if (frame_lens_mesh_build_coarse(&mesh, 48, 48, 24, 40.0) || if (frame_lens_mesh_build_coarse(&mesh, 48, 48, 24, 40.0) ||
+1 -1
View File
@@ -88,7 +88,7 @@ SampleRequest -> RayPool (SoA, inactive / active / terminated)
## Phase D:补全多 pass adaptive mesh 与 out-of-core 准备(未开始) ## Phase D:补全多 pass adaptive mesh 与 out-of-core 准备(未开始)
1. `FrameLensMesh` 采用 append-only stable vertex ID、generation-local request/probe 队列、triangle level 与“缺失 endpoint”状态。每 pass 的 probe 只放在最长 image-plane 边中点;完整 sweep 后才安装 endpoint 并改变拓扑。细分要求同时超过绝对与相对 `n_infinity` 方向误差阈值,并受最大层数、最小长边和最小面积约束;capture/escape 不一致强制细分。首版不计算 orientation 或 Jacobian。 1. `FrameLensMesh` 采用 append-only stable vertex ID、generation-local request/probe 队列、triangle level 与“缺失 endpoint”状态。每 pass 的 probe 只放在最长 image-plane 边中点;完整 sweep 后才安装 endpoint 并改变拓扑。细分保留同时超过绝对与相对 `n_infinity` 方向误差阈值的几何判据,并受最大层数、最小长边和最小面积约束;capture/escape 不一致强制细分。另以三顶点的有向球面面积比 `J = Omega_source / Omega_image` 独立检测临界带:当前仅 `abs(J)` 小于阈值时细分;相邻三角形的 `J` 异号细分暂不启用,留待验证 `abs(J)` 的覆盖能力后再决定。
2. 以迭代 queue(可按 frame/root tile 并行、线程本地 request buffer 后 sort/deduplicate)替代递归 task。每一个 pass 完整执行 Phase C sweep;只有所有 frame 都无新请求才进行 catalog splat。 2. 以迭代 queue(可按 frame/root tile 并行、线程本地 request buffer 后 sort/deduplicate)替代递归 task。每一个 pass 完整执行 Phase C sweep;只有所有 frame 都无新请求才进行 catalog splat。
3. 在 movie 生命周期中及时释放已完成的 RayPool、临时 request 和单帧 HDR;保留最终 mesh/endpoints,或在渲染 PNG 后按明确策略释放,避免视频时无界增长。 3. 在 movie 生命周期中及时释放已完成的 RayPool、临时 request 和单帧 HDR;保留最终 mesh/endpoints,或在渲染 PNG 后按明确策略释放,避免视频时无界增长。
4. 为 nmesh 预留并实现 source-side slab overlap / temporal ghost-slice 契约、可配置 memory budget、slab coverage 日志和线程本地 `MetricWorkspace` ownership。此 phase 不重采样为 Cartesian grid,也不假定相邻时间 slice 的 AMR tree 相同。 4. 为 nmesh 预留并实现 source-side slab overlap / temporal ghost-slice 契约、可配置 memory budget、slab coverage 日志和线程本地 `MetricWorkspace` ownership。此 phase 不重采样为 Cartesian grid,也不假定相邻时间 slice 的 AMR tree 相同。