Fix BoT shots with origins inside mesh bounds

Commit 7376385fd2 backed the HLBVH traversal origin by twice the
soltab bounding radius to preserve entrance triangles.  Commit
1d75918492 replaced that conservative distance with max(0, -r_min),
assuming r_min reaches the solid boundary.  During rt_shootray,
however, r_min can describe the current spatial partition cell.  A
ray starting inside a BoT can enter that cell after entering the mesh.
BVH culling then drops the entrance triangles, and segment assembly
discards the exit hit without an entrance.  In an overlapping scene,
ARS and native BoT differ because the ARS RPP test refreshes r_min.

For origins inside the BoT BVH root box, move the traversal origin
just beyond the box's entry face.  This retains the entrance facets
without the excess traversal from a bounding radius backout.  Rays
originating outside the box need no backout.  Share the calculation
between scalar and vector shots, while keeping triangle intersections
relative to the original ray origin.

Five deterministic, single processor Generic_Twin.g renders per view
had identical pixels with both backout methods.  The box method cut
shot time by 3.5% from inside the flight deck console, 24.5% from
inside a main strut, and 5.9% from inside a tire.  An exterior view
differed by about 0.2% from the radius method.  Avoiding unnecessary BVH
traversal more than offsets the extra box backout arithmetic for
these interior views.

Test direct ARS and BoT shots whose origins are inside the mesh,
including vector shots, and an overlapping composite scene that
reproduces the failure through rt_shootray without setting r_min
manually.
This commit is contained in:
Clifford Yapp committed 2026-09-17 21:20:03 +00:00
1 parent bcfb8669c1
commit de09372b73
3 files changed
+383 -25

No files matched your search

+42 -25
View File
@@ -727,8 +727,46 @@ bot_makesegs_specific(hit_da *hits,
struct rt_piecestate *psp);
/* r_min can describe a spatial partition cell that begins inside this
* solid. Start BVH traversal just before the ray enters the BoT's own
* root box so entrance triangles behind the ray origin remain visible. */
static inline void
bot_backed_origin(point_t b_pt, const struct xray *rp, const struct bvh_flat_node *root,
const vect_t inv_dir, const struct soltab *stp)
{
fastf_t backout = MAX_FASTF;
int axis;
for (axis = X; axis <= Z; axis++) {
if (rp->r_pt[axis] < root->bounds[axis] ||
rp->r_pt[axis] > root->bounds[axis + 3]) {
VMOVE(b_pt, rp->r_pt);
return;
}
}
for (axis = X; axis <= Z; axis++) {
fastf_t distance;
if (rp->r_dir[axis] > 0.0)
distance = (rp->r_pt[axis] - root->bounds[axis]) * inv_dir[axis];
else if (rp->r_dir[axis] < 0.0)
distance = (root->bounds[axis + 3] - rp->r_pt[axis]) * -inv_dir[axis];
else
continue;
backout = FMIN(backout, distance);
}
if (backout < MAX_FASTF) {
fastf_t padding = stp->st_rtip ? stp->st_rtip->rti_tol.dist : BN_TOL_DIST;
VJOIN1(b_pt, rp->r_pt, -(backout + padding), rp->r_dir);
} else {
VMOVE(b_pt, rp->r_pt);
}
}
void
bot_shot_hlbvh_flat(struct bvh_flat_node *root, struct xray* rp, triangle_s *tris, size_t ntris, hit_da* hits, fastf_t toldist)
bot_shot_hlbvh_flat(struct bvh_flat_node *root, struct xray* rp, triangle_s *tris, size_t ntris, hit_da* hits, fastf_t toldist, const struct soltab *stp)
{
struct bvh_flat_node *stack_node[HLBVH_STACK_SIZE];
unsigned char stack_child_index[HLBVH_STACK_SIZE];
@@ -744,25 +782,8 @@ bot_shot_hlbvh_flat(struct bvh_flat_node *root, struct xray* rp, triangle_s *tri
inverse_r_dir[1] = RAYDIR_INV(rp->r_dir[1]);
inverse_r_dir[2] = RAYDIR_INV(rp->r_dir[2]);
// Because we are doing solid shotlining, we need to intersect all
// triangles on the line of the ray, even when the r_pt is inside the mesh.
// This means we DON'T want to cull hlbvh leaves behind our r_pt when the
// r_pt is inside the mesh. To avoid this, we back our r_pt up to the
// bounding sphere if needed. This incurs a slight
// performance penalty when r_pt is past the mesh but still within the
// bounding sphere diameter distance, since backing up will cause false
// bbox intersect matches in those cases. This is necessary to ensure we
// get the intersections needed for solid segment creation - the subsequent
// full intersection solves will still produce the correct results.
//
// NOTE: We DO, however, need to use the real rp->r_pt when doing the
// actual intersection solve math so our segments end up in the right
// place.
fastf_t backout = FMAX(0.0, -rp->r_min);
point_t b_pt;
b_pt[X] = rp->r_pt[X] - backout * rp->r_dir[X];
b_pt[Y] = rp->r_pt[Y] - backout * rp->r_dir[Y];
b_pt[Z] = rp->r_pt[Z] - backout * rp->r_dir[Z];
bot_backed_origin(b_pt, rp, root, inverse_r_dir, stp);
while (stack_ind >= 0) {
if (UNLIKELY(stack_ind >= HLBVH_STACK_SIZE)) {
@@ -924,7 +945,7 @@ rt_bot_shot_specific(struct bot_specific *bot, struct soltab *stp, struct xray *
toldist = (DBL_EPSILON * stp->st_aradius * 10);
}
bot_shot_hlbvh_flat(sps->root, rp, sps->tris, bot->bot_ntri, &hits_per_cpu, toldist);
bot_shot_hlbvh_flat(sps->root, rp, sps->tris, bot->bot_ntri, &hits_per_cpu, toldist, stp);
if (hits_per_cpu.count == 0) {
return 0;
@@ -1026,14 +1047,10 @@ bot_vshot_packet(struct soltab *stp, struct bot_specific *bot,
/* Per-ray setup: inverse dir, backed-up origin, reset hit list. */
for (k = 0; k < m; k++) {
fastf_t backout;
inv_dir[k][0] = bot_vshot_rdinv(rp[k]->r_dir[0]);
inv_dir[k][1] = bot_vshot_rdinv(rp[k]->r_dir[1]);
inv_dir[k][2] = bot_vshot_rdinv(rp[k]->r_dir[2]);
backout = FMAX(0.0, -rp[k]->r_min);
b_pt[k][X] = rp[k]->r_pt[X] - backout * rp[k]->r_dir[X];
b_pt[k][Y] = rp[k]->r_pt[Y] - backout * rp[k]->r_dir[Y];
b_pt[k][Z] = rp[k]->r_pt[Z] - backout * rp[k]->r_dir[Z];
bot_backed_origin(b_pt[k], rp[k], sps->root, inv_dir[k], stp);
vhits[k].count = 0;
segp[k].seg_stp = (struct soltab *)0;
}
+4
View File
@@ -150,6 +150,9 @@ brlcad_addexec(rt_bot_split
target_include_directories(rt_bot_split PRIVATE ${BRLCAD_SOURCE_DIR}/src/libbg)
brlcad_add_test(NAME rt_bot_split COMMAND rt_bot_split)
brlcad_addexec(rt_ars_inside ars_inside.c "librt;libwdb;libbu;${M_LIBRARY}" TEST)
brlcad_add_test(NAME rt_ars_inside COMMAND rt_ars_inside)
# boolweave testing
brlcad_addexec(rt_boolweave rt_boolweave.c "${RT_TEST_LIBS}" TEST)
brlcad_add_test(NAME rt_boolweave COMMAND rt_boolweave)
@@ -202,6 +205,7 @@ brlcad_add_test(NAME rt_metaball_adjust COMMAND rt_metaball_adjust)
set(
distcheck_files
CMakeLists.txt
ars_inside.c
arb_intersect.g
binary_attribute.c
binunif.c
+337
View File
@@ -0,0 +1,337 @@
/* A R S _ I N S I D E . C
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
#include "common.h"
#include <math.h>
#include <string.h>
#include "bu/app.h"
#include "bu/log.h"
#include "bu/malloc.h"
#include "raytrace.h"
#include "rt/geom.h"
#include "wdb.h"
#define RING_SIDES 8
#define RING_POINTS (RING_SIDES + 1)
static int
check_shot(const struct rt_functab *ftp, struct soltab *stp, struct application *ap, struct xray *ray, const char *label)
{
struct seg seghead;
struct seg *segp;
struct resource *resp = ap->a_resource;
int nhits;
int failed = 0;
BU_LIST_INIT(&seghead.l);
nhits = ftp->ft_shot(stp, ray, ap, &seghead);
if (nhits <= 0 || BU_LIST_IS_EMPTY(&seghead.l)) {
bu_log("%s: inside-origin ray missed\n", label);
failed = 1;
} else {
segp = BU_LIST_FIRST(seg, &seghead.l);
if (!NEAR_EQUAL(segp->seg_in.hit_dist, -9.0, SMALL_FASTF) ||
!NEAR_EQUAL(segp->seg_out.hit_dist, 7.0, SMALL_FASTF)) {
bu_log("%s: interval %g..%g, expected -9..7\n", label,
segp->seg_in.hit_dist, segp->seg_out.hit_dist);
failed = 1;
}
}
while (BU_LIST_WHILE(segp, seg, &seghead.l)) {
BU_LIST_DEQUEUE(&segp->l);
RT_FREE_SEG(segp, resp);
}
if (ftp->ft_vshot) {
struct soltab *stps[1] = {stp};
struct xray *rays[1] = {ray};
struct seg vseg = {0};
ftp->ft_vshot(stps, rays, &vseg, 1, ap);
if (vseg.seg_stp != stp ||
!NEAR_EQUAL(vseg.seg_in.hit_dist, -9.0, SMALL_FASTF) ||
!NEAR_EQUAL(vseg.seg_out.hit_dist, 7.0, SMALL_FASTF)) {
bu_log("%s: vector shot missed or returned wrong interval\n", label);
failed = 1;
}
}
return failed;
}
struct scene_result {
int subject_hits;
fastf_t out_dist;
};
static int
scene_hit(struct application *ap, struct partition *part_head,
struct seg *UNUSED(finished_segs))
{
struct scene_result *result = (struct scene_result *)ap->a_uptr;
struct partition *part;
for (part = part_head->pt_forw; part != part_head; part = part->pt_forw) {
if (strstr(part->pt_regionp->reg_name, "subject.r")) {
result->subject_hits++;
result->out_dist = part->pt_outhit->hit_dist;
}
}
return result->subject_hits > 0;
}
static int
scene_miss(struct application *UNUSED(ap))
{
return 0;
}
/* The support sphere makes the cut tree start the ray's first cell
* inside the subject, while the subject itself spans the entire cell. */
static int
check_scene(int subject_id, const fastf_t *const curve_data[3],
fastf_t *vertices, int *faces, const char *file)
{
struct db_i *dbip = NULL;
struct rt_wdb *wdbp = NULL;
struct rt_i *rtip = NULL;
struct resource resp = RT_RESOURCE_INIT_ZERO;
struct wmember members;
struct application ap;
struct scene_result result = {0};
point_t support_center;
int failed = 0;
if (file) {
wdbp = wdb_fopen(file);
if (wdbp)
dbip = wdbp->dbip;
} else {
dbip = db_create_inmem();
if (dbip)
wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_INMEM);
}
if (!wdbp) {
if (dbip)
db_close(dbip);
return 1;
}
if (subject_id == ID_ARS) {
fastf_t **curves = (fastf_t **)bu_malloc(3 * sizeof(fastf_t *), "ARS curves");
size_t i;
for (i = 0; i < 3; i++) {
curves[i] = (fastf_t *)bu_malloc(RING_POINTS * ELEMENTS_PER_POINT *
sizeof(fastf_t), "ARS curve");
memcpy(curves[i], curve_data[i],
RING_POINTS * ELEMENTS_PER_POINT * sizeof(fastf_t));
}
failed += mk_ars(wdbp, "subject.s", 3, RING_POINTS, curves) != 0;
} else {
failed += mk_bot(wdbp, "subject.s", RT_BOT_SOLID, RT_BOT_CCW, 0,
RING_SIDES + 2, RING_SIDES * 2, vertices, faces,
NULL, NULL) != 0;
}
BU_LIST_INIT(&members.l);
if (!mk_addmember("subject.s", &members.l, NULL, WMOP_UNION))
failed++;
failed += mk_lcomb(wdbp, "subject.r", &members, 1, NULL, NULL, NULL, 0) != 0;
VSET(support_center, 1.5, 0, 4);
failed += mk_sph(wdbp, "support.s", support_center, 1.0) != 0;
BU_LIST_INIT(&members.l);
if (!mk_addmember("support.s", &members.l, NULL, WMOP_UNION))
failed++;
failed += mk_lcomb(wdbp, "support.r", &members, 1, NULL, NULL, NULL, 0) != 0;
if (failed)
goto done;
rtip = rt_i_create(dbip);
if (!rtip || rt_gettree(rtip, "subject.r") ||
rt_gettree(rtip, "support.r")) {
failed = 1;
goto done;
}
rt_prep(rtip);
rt_init_resource(&resp, 0, rtip);
RT_APPLICATION_INIT(&ap);
ap.a_rt_i = rtip;
ap.a_resource = &resp;
ap.a_hit = scene_hit;
ap.a_miss = scene_miss;
ap.a_uptr = &result;
VSET(ap.a_ray.r_pt, 3, 0, 4);
VSET(ap.a_ray.r_dir, 1, 0, 0);
rt_shootray(&ap);
if (result.subject_hits != 1 ||
!NEAR_EQUAL(result.out_dist, 5.0, rtip->rti_tol.dist)) {
bu_log("%s scene: %d subject partitions, exit %.17g; expected one exit at 5\n",
subject_id == ID_ARS ? "ARS" : "BoT",
result.subject_hits, result.out_dist);
failed = 1;
}
done:
if (rtip)
rt_i_destroy(rtip);
wdb_close(wdbp);
return failed;
}
int
main(int argc, char **argv)
{
fastf_t curve_data[3][RING_POINTS * ELEMENTS_PER_POINT] = {{0}};
fastf_t *curves[3] = {curve_data[0], curve_data[1], curve_data[2]};
fastf_t bot_vertices[(RING_SIDES + 2) * ELEMENTS_PER_POINT] = {0};
int bot_faces[RING_SIDES * 2 * 3];
struct rt_bot_internal bot = {0};
struct rt_ars_internal ars = {0};
struct rt_db_internal intern;
struct rt_db_internal bot_intern;
struct soltab st = {0};
struct soltab bot_st = {0};
struct rt_i *rtip;
struct resource *resp = &rt_uniresource;
struct application ap;
struct xray ray = {0};
vect_t invdir;
size_t i;
int failed = 0;
(void)argc;
bu_setprogname(argv[0]);
for (i = 0; i < RING_POINTS; i++) {
fastf_t angle = 2.0 * M_PI * (i % RING_SIDES) / RING_SIDES;
VSET(&curve_data[1][i * ELEMENTS_PER_POINT],
10.0 * cos(angle), 10.0 * sin(angle), 5.0);
if (i < RING_SIDES)
VMOVE(&bot_vertices[(i + 1) * ELEMENTS_PER_POINT],
&curve_data[1][i * ELEMENTS_PER_POINT]);
curve_data[2][i * ELEMENTS_PER_POINT + Z] = 10.0;
}
bot_vertices[(RING_SIDES + 1) * ELEMENTS_PER_POINT + Z] = 10.0;
for (i = 0; i < RING_SIDES; i++) {
int current = (int)i + 1;
int next = (int)((i + 1) % RING_SIDES) + 1;
int *bottom = &bot_faces[i * 6];
int *top = &bottom[3];
bottom[0] = 0;
bottom[1] = next;
bottom[2] = current;
top[0] = current;
top[1] = next;
top[2] = RING_SIDES + 1;
}
bot.magic = RT_BOT_INTERNAL_MAGIC;
bot.mode = RT_BOT_SOLID;
bot.orientation = RT_BOT_CCW;
bot.num_vertices = RING_SIDES + 2;
bot.vertices = bot_vertices;
bot.num_faces = RING_SIDES * 2;
bot.faces = bot_faces;
ars.magic = RT_ARS_INTERNAL_MAGIC;
ars.ncurves = 3;
ars.pts_per_curve = RING_POINTS;
ars.curves = curves;
RT_DB_INTERNAL_INIT(&intern);
intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
intern.idb_type = ID_ARS;
intern.idb_meth = &OBJ[ID_ARS];
intern.idb_ptr = &ars;
RT_DB_INTERNAL_INIT(&bot_intern);
bot_intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
bot_intern.idb_type = ID_BOT;
bot_intern.idb_meth = &OBJ[ID_BOT];
bot_intern.idb_ptr = &bot;
rtip = rt_dirbuild_inmem(NULL, 0, NULL, 0);
if (!rtip)
return 1;
rt_init_resource(resp, 0, rtip);
st.l.magic = RT_SOLTAB_MAGIC;
st.l2.magic = RT_SOLTAB2_MAGIC;
st.st_rtip = rtip;
st.st_meth = &OBJ[ID_ARS];
st.st_id = ID_ARS;
if (OBJ[ID_ARS].ft_prep(&st, &intern, rtip)) {
bu_log("ARS prep failed\n");
return 1;
}
bot_st.l.magic = RT_SOLTAB_MAGIC;
bot_st.l2.magic = RT_SOLTAB2_MAGIC;
bot_st.st_rtip = rtip;
bot_st.st_meth = &OBJ[ID_BOT];
bot_st.st_id = ID_BOT;
if (OBJ[ID_BOT].ft_prep(&bot_st, &bot_intern, rtip)) {
bu_log("BoT prep failed\n");
return 1;
}
RT_APPLICATION_INIT(&ap);
ap.a_rt_i = rtip;
ap.a_resource = resp;
ray.magic = RT_RAY_MAGIC;
VSET(ray.r_pt, 1, 0, 4);
VSET(ray.r_dir, 1, 0, 0);
ray.r_min = -0.05; /* the current spatial cell starts inside the ARS */
failed += check_shot(&OBJ[ID_ARS], &st, &ap, &ray, "ARS cell bound");
failed += check_shot(&OBJ[ID_BOT], &bot_st, &ap, &ray, "BoT cell bound");
VSET(invdir, 1, INFINITY, INFINITY);
if (!rt_in_rpp(&ray, invdir, st.st_min, st.st_max)) {
bu_log("ARS bounding box missed the test ray\n");
failed++;
} else {
failed += check_shot(&OBJ[ID_ARS], &st, &ap, &ray, "ARS solid bound");
}
ray.r_min = -0.05;
if (!rt_in_rpp(&ray, invdir, bot_st.st_min, bot_st.st_max)) {
bu_log("BoT bounding box missed the test ray\n");
failed++;
} else {
failed += check_shot(&OBJ[ID_BOT], &bot_st, &ap, &ray, "BoT solid bound");
}
OBJ[ID_ARS].ft_free(&st);
OBJ[ID_BOT].ft_free(&bot_st);
rt_i_destroy(rtip);
if (argc != 1 && argc != 3) {
bu_log("Usage: %s [ars_scene.g bot_scene.g]\n", argv[0]);
return 1;
}
{
const fastf_t *scene_curves[3] = {curve_data[0], curve_data[1], curve_data[2]};
failed += check_scene(ID_ARS, scene_curves, bot_vertices, bot_faces,
argc == 3 ? argv[1] : NULL);
failed += check_scene(ID_BOT, scene_curves, bot_vertices, bot_faces,
argc == 3 ? argv[2] : NULL);
}
return failed ? 1 : 0;
}