Skip to content

Commit 32451aa

Browse files
committed
Lazy majority vote and mesh closure validation
- point_in_mesh: cast 1 ray first, only fall back to 3-ray majority vote if deduplication removed near-duplicate t-values (indicating an edge/vertex hit). 3x faster for the common case (no edge hits). - init_mesh: validate mesh closure by building an edge-to-face-count hash table. Every edge must be shared by exactly 2 faces. Sets is_closed=false and prints a warning for open/non-manifold meshes. point_in_mesh returns false for all points on open meshes. - Add 2 new tests: open mesh detection, closed mesh detection (13 tests total, all passing).
1 parent bdae298 commit 32451aa

2 files changed

Lines changed: 139 additions & 19 deletions

File tree

utils/geom.c

Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2427,8 +2427,11 @@ static int remove_duplicate_intersections(double *slist, int count, double tol)
24272427
#define MESH_MAX_INTERSECTIONS 256
24282428

24292429
/* Count ray-mesh intersections using BVH traversal.
2430-
Collects all intersection t-values, deduplicates, and returns the count. */
2431-
static int count_ray_mesh_intersections(const mesh *m, vector3 origin, vector3 dir) {
2430+
Collects all intersection t-values, deduplicates, and returns the count.
2431+
If had_degenerate is non-NULL, sets it to 1 if any near-duplicate
2432+
t-values were found (indicating a potential edge/vertex hit). */
2433+
static int count_ray_mesh_intersections_ex(const mesh *m, vector3 origin, vector3 dir,
2434+
int *had_degenerate) {
24322435
double slist[MESH_MAX_INTERSECTIONS];
24332436
int count = mesh_ray_all_intersections(m, origin, dir, slist, MESH_MAX_INTERSECTIONS);
24342437

@@ -2440,32 +2443,46 @@ static int count_ray_mesh_intersections(const mesh *m, vector3 origin, vector3 d
24402443

24412444
if (nforward > 1) {
24422445
qsort(slist, nforward, sizeof(double), mesh_dcmp);
2443-
nforward = remove_duplicate_intersections(slist, nforward, 1e-10);
2446+
int deduped = remove_duplicate_intersections(slist, nforward, 1e-10);
2447+
if (had_degenerate) *had_degenerate = (deduped != nforward);
2448+
nforward = deduped;
2449+
} else {
2450+
if (had_degenerate) *had_degenerate = 0;
24442451
}
24452452

24462453
return nforward;
24472454
}
24482455

2456+
static int count_ray_mesh_intersections(const mesh *m, vector3 origin, vector3 dir) {
2457+
return count_ray_mesh_intersections_ex(m, origin, dir, NULL);
2458+
}
2459+
24492460
/***************************************************************/
24502461
/* Core mesh geometric operations */
24512462
/***************************************************************/
24522463

2464+
static const vector3 mesh_ray_dirs[3] = {
2465+
{0.57735026918962576, 0.57735026918962576, 0.57735026918962576}, /* (1,1,1)/√3 */
2466+
{0.80178372573727319, 0.53452248382484879, 0.26726124191242440}, /* (3,2,1)/√14 */
2467+
{0.12309149097933272, 0.49236596391733088, 0.86164043685532904}, /* (1,4,7)/√66 */
2468+
};
2469+
24532470
static boolean point_in_mesh(const mesh *m, vector3 p) {
24542471
if (!m->is_closed) return 0;
24552472

2456-
/* Cast rays along multiple directions and take majority vote.
2457-
A single ray can give wrong parity when it passes through a mesh
2458-
edge or vertex, causing deduplication to miss or double-count
2459-
a crossing. Three irrational directions make it extremely unlikely
2460-
that more than one ray hits a degenerate case at the same point. */
2461-
static const vector3 dirs[3] = {
2462-
{0.57735026918962576, 0.57735026918962576, 0.57735026918962576}, /* (1,1,1)/√3 */
2463-
{0.80178372573727319, 0.53452248382484879, 0.26726124191242440}, /* (3,2,1)/√14 */
2464-
{0.12309149097933272, 0.49236596391733088, 0.86164043685532904}, /* (1,4,7)/√66 */
2465-
};
2466-
int votes = 0;
2467-
for (int d = 0; d < 3; d++) {
2468-
int crossings = count_ray_mesh_intersections(m, p, dirs[d]);
2473+
/* Fast path: cast one ray. If no degenerate edge/vertex hits were
2474+
detected during deduplication, trust the result immediately.
2475+
Only fall back to 3-ray majority vote when a degenerate case
2476+
is detected (near-duplicate t-values removed). */
2477+
int had_degenerate;
2478+
int crossings = count_ray_mesh_intersections_ex(m, p, mesh_ray_dirs[0], &had_degenerate);
2479+
if (!had_degenerate)
2480+
return (crossings % 2) == 1;
2481+
2482+
/* Degenerate hit detected — cast two more rays and take majority vote. */
2483+
int votes = (crossings % 2 == 1) ? 1 : 0;
2484+
for (int d = 1; d < 3; d++) {
2485+
crossings = count_ray_mesh_intersections(m, p, mesh_ray_dirs[d]);
24692486
if (crossings % 2 == 1) votes++;
24702487
}
24712488
return votes >= 2;
@@ -2593,9 +2610,65 @@ static void init_mesh(geometric_object *o) {
25932610
m->centroid = vector3_plus(m->centroid, m->vertices.items[i]);
25942611
m->centroid = vector3_scale(1.0 / nv, m->centroid);
25952612

2596-
/* Check if mesh is closed (every edge shared by exactly 2 faces)
2597-
and ensure consistent outward-facing normals. */
2598-
m->is_closed = 1;
2613+
/* Check if mesh is closed: every edge must be shared by exactly 2 faces.
2614+
An edge is identified by a sorted pair of vertex indices. We use a
2615+
hash table mapping edge → face count. */
2616+
{
2617+
/* Hash table size: next power of 2 above 3*nf (each face has 3 edges). */
2618+
int htsize = 1;
2619+
while (htsize < 4 * nf) htsize <<= 1;
2620+
int htmask = htsize - 1;
2621+
2622+
/* Each bucket stores (v_lo, v_hi, count). Use open addressing. */
2623+
int *ht_vlo = (int *)calloc(htsize, sizeof(int));
2624+
int *ht_vhi = (int *)calloc(htsize, sizeof(int));
2625+
int *ht_cnt = (int *)calloc(htsize, sizeof(int));
2626+
/* Mark empty slots with -1. */
2627+
for (int i = 0; i < htsize; i++) ht_vlo[i] = -1;
2628+
2629+
m->is_closed = 1;
2630+
for (int f = 0; f < nf && m->is_closed; f++) {
2631+
for (int e = 0; e < 3; e++) {
2632+
int va = m->face_indices[3 * f + e];
2633+
int vb = m->face_indices[3 * f + (e + 1) % 3];
2634+
int vlo = (va < vb) ? va : vb;
2635+
int vhi = (va < vb) ? vb : va;
2636+
unsigned int h = (unsigned int)(vlo * 73856093u ^ vhi * 19349663u) & htmask;
2637+
/* Linear probing. */
2638+
while (1) {
2639+
if (ht_vlo[h] == -1) {
2640+
/* Empty slot: insert new edge. */
2641+
ht_vlo[h] = vlo;
2642+
ht_vhi[h] = vhi;
2643+
ht_cnt[h] = 1;
2644+
break;
2645+
} else if (ht_vlo[h] == vlo && ht_vhi[h] == vhi) {
2646+
/* Found existing edge. */
2647+
ht_cnt[h]++;
2648+
if (ht_cnt[h] > 2) m->is_closed = 0; /* non-manifold edge */
2649+
break;
2650+
}
2651+
h = (h + 1) & htmask;
2652+
}
2653+
}
2654+
}
2655+
/* Check that all edges have exactly 2 faces. */
2656+
if (m->is_closed) {
2657+
for (int i = 0; i < htsize; i++) {
2658+
if (ht_vlo[i] != -1 && ht_cnt[i] != 2) {
2659+
m->is_closed = 0;
2660+
break;
2661+
}
2662+
}
2663+
}
2664+
free(ht_vlo);
2665+
free(ht_vhi);
2666+
free(ht_cnt);
2667+
2668+
if (!m->is_closed)
2669+
ctl_printf("WARNING: mesh is not closed (not all edges shared by exactly 2 faces).\n"
2670+
" point_in_mesh results may be incorrect.\n");
2671+
}
25992672

26002673
/* Use signed volume to check/fix normal orientation. */
26012674
double signed_vol = 0;

utils/test-mesh.c

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,51 @@ static void test_mesh_with_center(void) {
357357
printf("done\n");
358358
}
359359

360+
/************************************************************************/
361+
/* Test: open mesh detection */
362+
/************************************************************************/
363+
static void test_open_mesh(void) {
364+
printf("test_open_mesh... ");
365+
/* A tetrahedron with one face duplicated and one face missing — not closed.
366+
4 faces total (passes the >= 4 check) but has boundary edges. */
367+
vector3 verts[4] = {
368+
{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}
369+
};
370+
int tris[4 * 3] = {
371+
0, 1, 2,
372+
0, 1, 3,
373+
0, 2, 3,
374+
0, 2, 3 /* duplicate of face 2 instead of the missing face (1,2,3) */
375+
};
376+
geometric_object open = make_mesh(NULL, verts, 4, tris, 4);
377+
mesh *m = open.subclass.mesh_data;
378+
ASSERT_TRUE("open mesh detected as not closed", !m->is_closed);
379+
380+
/* point_in_mesh should return false for open meshes. */
381+
vector3 p = {0.1, 0.1, 0.1};
382+
ASSERT_TRUE("open mesh: point_in returns false", !point_in_fixed_pobjectp(p, &open));
383+
384+
geometric_object_destroy(open);
385+
printf("done\n");
386+
}
387+
388+
/************************************************************************/
389+
/* Test: closed mesh correctly detected */
390+
/************************************************************************/
391+
static void test_closed_detection(void) {
392+
printf("test_closed_detection... ");
393+
geometric_object cube = make_cube_mesh(NULL);
394+
mesh *m = cube.subclass.mesh_data;
395+
ASSERT_TRUE("cube mesh detected as closed", m->is_closed);
396+
geometric_object_destroy(cube);
397+
398+
geometric_object tetra = make_tetra_mesh(NULL);
399+
m = tetra.subclass.mesh_data;
400+
ASSERT_TRUE("tetra mesh detected as closed", m->is_closed);
401+
geometric_object_destroy(tetra);
402+
printf("done\n");
403+
}
404+
360405
/************************************************************************/
361406
int main(void) {
362407
geom_initialize();
@@ -372,6 +417,8 @@ int main(void) {
372417
test_display_info();
373418
test_tetra_point_in();
374419
test_mesh_with_center();
420+
test_open_mesh();
421+
test_closed_detection();
375422

376423
printf("\n%d test failures\n", test_failures);
377424
return test_failures > 0 ? 1 : 0;

0 commit comments

Comments
 (0)