Frame: gate fold refinement by Jacobian

This commit is contained in:
wyj committed 2026-08-29 01:17:47 -04:00
1 parent 4c575fa8b5
commit b95c6579bd
4 files changed
+65 -31

No files matched your search

+8 -7
View File
@@ -180,13 +180,14 @@ 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.
directions for `Omega_source`. A J-driven split requires **both** a shared
image edge whose incident triangles have opposite nonzero signs of `J` and
`min(abs(J_left), abs(J_right)) < --refine-jacobian-min`. It then requests
that shared edge on both leaves. Thus `|J|` bounds the fold selection instead
of widening it as a standalone critical-curve band. A negative sign is
physical parity and is retained. 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
generation, for example:
+37 -12
View File
@@ -344,9 +344,11 @@ static double spherical_signed_area(const double a[3], const double b[3],
}
/* Returns whether the discrete source/image solid-angle ratio is available.
* Captured and degenerate image triangles have no reliable value. */
* A zero output parity is a valid, critical (zero-Jacobian) result; captured
* and degenerate image triangles have no reliable parity. */
static int discrete_jacobian(const FrameLensMesh *mesh,
const LensTriangle *triangle, double *value) {
const LensTriangle *triangle, double *value,
signed char *parity) {
const LensVertex *a = &mesh->vertices[triangle->vertex[0]];
const LensVertex *b = &mesh->vertices[triangle->vertex[1]];
const LensVertex *c = &mesh->vertices[triangle->vertex[2]];
@@ -363,6 +365,7 @@ static int discrete_jacobian(const FrameLensMesh *mesh,
if (!isfinite(jacobian))
return 0;
*value = jacobian;
*parity = jacobian > 0.0 ? 1 : jacobian < 0.0 ? -1 : 0;
return 1;
}
@@ -456,8 +459,11 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
MeshEdge *edges = calloc(edge_count, sizeof *edges);
unsigned char *requested = calloc(edge_count, sizeof *requested);
unsigned char *allowed = calloc(mesh->triangle_count, sizeof *allowed);
if (edges == NULL || requested == NULL || allowed == NULL) {
free(edges); free(requested); free(allowed);
signed char *parity = calloc(mesh->triangle_count, sizeof *parity);
double *jacobians = calloc(mesh->triangle_count, sizeof *jacobians);
if (edges == NULL || requested == NULL || allowed == NULL || parity == NULL ||
jacobians == NULL) {
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
return -1;
}
for (size_t i = 0; i < mesh->triangle_count; ++i) {
@@ -472,13 +478,32 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
if (allowed[i]) {
const unsigned int side = longest_side(mesh, triangle);
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;
(void)discrete_jacobian(mesh, triangle, &jacobians[i], &parity[i]);
}
}
qsort(edges, edge_count, sizeof *edges, compare_mesh_edge);
/* A fold is selected only when its adjacent discrete parities disagree and
* at least one of those leaves is close enough to the critical curve. */
for (size_t first = 0; first < edge_count;) {
size_t last = first + 1;
while (last < edge_count && edges[last].a == edges[first].a &&
edges[last].b == edges[first].b)
++last;
if (last - first == 2) {
const size_t left = edges[first].triangle;
const size_t right = edges[first + 1].triangle;
if (parity[left] != 0 && parity[right] != 0 &&
parity[left] != parity[right] &&
fmin(fabs(jacobians[left]), fabs(jacobians[right])) <
config->jacobian_minimum) {
if (allowed[left] && allowed[right]) {
requested[3 * left + edges[first].side] = 1;
requested[3 * right + edges[first + 1].side] = 1;
}
}
}
first = last;
}
/* A requested interior edge is split by both incident leaves, preserving a
* conforming mesh. If either side has reached its geometric limit, reject
* the whole edge instead of introducing a T-junction. */
@@ -542,7 +567,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
if (split_edges == 0) {
mesh->sample_count = 0;
mesh->samples_include_probes = 0;
free(edges); free(requested); free(allowed);
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
return 0;
}
/* Allocate a single stable midpoint vertex for each requested edge group. */
@@ -559,7 +584,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
first = last;
}
if (ensure_vertices(mesh, mesh->vertex_count + midpoint_count)) {
free(edges); free(requested); free(allowed);
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
return -1;
}
size_t next_vertex = mesh->vertex_count;
@@ -586,7 +611,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
const size_t old_count = mesh->triangle_count;
LensTriangle *children = calloc(old_count * 4, sizeof *children);
if (children == NULL) {
free(edges); free(requested); free(allowed);
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
return -1;
}
size_t child_count = 0;
@@ -634,7 +659,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
mesh->vertex_count = next_vertex;
mesh->sample_count = 0;
mesh->samples_include_probes = 0;
free(edges); free(requested); free(allowed);
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
return (int)midpoint_count;
}
+19 -11
View File
@@ -193,24 +193,32 @@ int main(void) {
goto done;
}
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};
/* Opposite nonzero discrete-Jacobian signs on the two sides of the shared
* diagonal require a sufficiently small magnitude before requesting it. */
refine.jacobian_minimum = 10.0;
refine.angle_absolute_rad = 3.14159265358979323846;
refine.angle_relative = 1e6;
if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0))
goto done;
const double source_directions[4][3] = {
{1.0, 0.0, 0.0},
{sqrt(0.99), 0.0, 0.1},
{sqrt(0.99), 0.0, 0.1},
{sqrt(0.98), 0.1, 0.1}};
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;
memcpy(adaptive_mesh.vertices[i].n_infinity, source_directions[i],
sizeof source_directions[i]);
}
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);
fputs("adaptive fold-parity split regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
+1 -1
View File
@@ -88,7 +88,7 @@ SampleRequest -> RayPool (SoA, inactive / active / terminated)
## 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 不一致强制细分。另以三顶点的有向球面面积比 `J = Omega_source / Omega_image` 独立检测临界带:当前仅 `abs(J)` 小于阈值时细分;相邻三角形的 `J` 异号细分暂不启用,留待验证 `abs(J)` 的覆盖能力后再决定。
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` 检测 fold:仅当共享边两侧非零 `J` 异号,且 `min(abs(J_left), abs(J_right))` 小于阈值时,细分该共享边的两叶;负 parity 本身保留而不剔除。
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 后按明确策略释放,避免视频时无界增长。
4. 为 nmesh 预留并实现 source-side slab overlap / temporal ghost-slice 契约、可配置 memory budget、slab coverage 日志和线程本地 `MetricWorkspace` ownership。此 phase 不重采样为 Cartesian grid,也不假定相邻时间 slice 的 AMR tree 相同。