Skip to content

Add intersect_ray function that uses BVH accelerated raycasting, Remove SensorRaycast - #2971

Merged
eric-heiden merged 16 commits into
newton-physics:mainfrom
StafaH:mh/intersect_ray
Jun 2, 2026
Merged

Add intersect_ray function that uses BVH accelerated raycasting, Remove SensorRaycast#2971
eric-heiden merged 16 commits into
newton-physics:mainfrom
StafaH:mh/intersect_ray

Conversation

@StafaH

@StafaH StafaH commented May 28, 2026

Copy link
Copy Markdown
Member

Add intersect_ray, heightfield mesh raycast, remove SensorRaycast

Description

newton.intersect_ray() — new public raycast API

Adds newton.intersect_ray(), a functional helper for casting batched rays against all shapes in a model across multiple worlds. It uses the shape BVH for broad-phase culling and dispatches to per-shape intersection routines (primitives, meshes, and heightfields).

newton.intersect_ray(
    model,           # Model — must have a built BVH (build_bvh_shape)
    ray_origins,     # wp.array2d[wp.vec3]  shape [world_count, ray_count]
    ray_directions,  # wp.array2d[wp.vec3]  shape [world_count, ray_count]
    out_dist,        # wp.array2d[float]    output hit distances (-1 = miss)
    out_shape_id,    # wp.array2d[wp.int32] output hit shape indices
    out_normal,      # wp.array2d[wp.vec3]  output surface normals at hit
)

This is the recommended building block for custom raycast sensors, lidar simulations, and picking — replacing ad-hoc kernel launches against internal kernel symbols.

Heightfield raycast via wp.Mesh

Previously GeoType.HFIELD shapes used a per-thread 2D DDA grid traversal in the raycast kernel, requiring three extra arrays (shape_heightfield_index, heightfield_data, heightfield_elevations) threaded through every raycast kernel signature and a factory pattern (_make_raycast_funcs) to generate two compiled variants.

Now ModelBuilder.finalize() builds a wp.Mesh from each heightfield (same CCW triangulation as the collision path) and stores its ID in model.shape_source_ptr, exactly like MESH and CONVEX_MESH shapes. ray_intersect_geom handles HFIELD in the same branch as meshes.

Benefits:

  • Raycast kernel signatures shrink by 3 parameters; no more two-variant factory
  • BVH-accelerated ray-triangle intersection replaces per-thread grid walking
  • Same Heightfield object reuses one wp.Mesh across instances; per-instance scale is applied at query time
  • model.heightfield_meshes keeps the wp.Mesh objects alive; heightfield_data/heightfield_elevations are unchanged (still used by collision kernels)

Remove SensorRaycast

SensorRaycast was deprecated in 1.2 in favour of SensorTiledCamera. This PR removes it along with the sensor_raycast_kernel, sensor_raycast_particles_kernel, and sensor_raycast_kernel_no_hfield that existed solely to support it. ray_intersect_particle_sphere is retained — still used by warp_raytrace.

Migration: use SensorTiledCamera with SensorTiledCamera.utils.compute_pinhole_camera_rays() and create_depth_image_output().

Closes #2465.

Checklist

  • New or existing tests cover these changes
  • The documentation is up to date with these changes
  • CHANGELOG.md has been updated (if user-facing change)

Test plan

# Raycast tests (28 tests — all heightfield cases now use wp.Mesh path)
uv run --extra dev -m newton.tests -k test_raycast

# Full suite — no regressions
uv run --extra dev -m newton.tests

New feature / API change

import newton
import numpy as np
import warp as wp

builder = newton.ModelBuilder()
builder.begin_world()
body = builder.add_body(xform=wp.transform(wp.vec3(0.0, 0.0, 0.0), wp.quat_identity()))
builder.add_shape_sphere(body=body, radius=0.5)

elevation = np.random.rand(32, 32).astype(np.float32) * 2.0
hf = newton.Heightfield(data=elevation, nrow=32, ncol=32, hx=5.0, hy=5.0)
builder.add_shape_heightfield(heightfield=hf)
builder.end_world()

model = builder.finalize()
state = model.state()
newton.geometry.build_bvh_shape(model, state)

# Cast two rays in one call: one toward the sphere, one toward the terrain.
origins = wp.array([[[- 2.0, 0.0, 0.0], [0.0, 0.0, 8.0]]],  dtype=wp.vec3)
directions = wp.array([[[1.0, 0.0, 0.0], [0.0, 0.0, -1.0]]], dtype=wp.vec3)

out_dist     = wp.empty(shape=(1, 2), dtype=float)
out_shape_id = wp.empty(shape=(1, 2), dtype=wp.int32)
out_normal   = wp.empty(shape=(1, 2), dtype=wp.vec3)

newton.intersect_ray(model, origins, directions, out_dist, out_shape_id, out_normal)

print(out_dist.numpy())     # [[1.5, <terrain_dist>]]
print(out_shape_id.numpy()) # [[0, 1]]
print(out_normal.numpy())   # [[-1, 0, 0], <terrain_normal>]

Summary by CodeRabbit

  • New Features
    • Added intersect_ray() API for custom raycast queries.
  • Improvements
    • Heightfield raycasting now uses mesh-based BVH; heightfields are converted to retained meshes during model finalization.
  • Documentation
    • API docs updated to include intersect_ray and remove SensorRaycast entries.
  • Deprecations / Removals
    • SensorRaycast removed from public exports; use SensorTiledCamera for pinhole ray/depth output.
  • Tests
    • Raycast tests updated for new interfaces; legacy SensorRaycast tests removed.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Builds per-heightfield Warp meshes in ModelBuilder.finalize(), unifies ray intersection via a mesh_id-based dispatcher and BVH kernel, exposes newton.intersect_ray(), removes SensorRaycast from exports/docs, and updates callers and tests to the new interfaces.

Changes

Heightfield Raycasting and Ray API Refactor

Layer / File(s) Summary
Heightfield-to-Mesh Conversion Foundation
newton/_src/sim/builder.py, newton/_src/sim/model.py
Converts Heightfield sources into finalized wp.Mesh instances during ModelBuilder.finalize() and stores them in Model.heightfield_meshes.
Ray Geometry Intersection Dispatcher Refactor
newton/_src/geometry/raycast.py
Adds ray_intersect_geom() dispatcher, updates raycast_kernel to derive mesh_id from shape_source_ptr for mesh-like shapes, introduces BVH _intersect_ray_kernel, and adds public intersect_ray() wrapper.
Picking & Sensor API Updates
newton/_src/viewer/picking.py, newton/sensors.py, docs/concepts/sensors.rst
Updates Picking.pick() to call the unified raycast.raycast_kernel without heightfield-array args; removes SensorRaycast from public exports and documentation.
Raytrace Warp HFIELD Handling
newton/_src/sensors/warp_raytrace/raytrace.py
Adds HFIELD branches that invoke mesh-based intersection helpers for closest-hit, depth-only, and first-hit flows.
Raycast Kernel & Test Signature Updates
newton/_src/geometry/raycast.py, newton/tests/test_raycast.py
Removes heightfield-array parameters from the raycast kernel and kernel_test_geom() signature; updates primitive/mesh test call sites.
Heightfield Test Refactoring
newton/tests/test_raycast.py
Adds _hfield_mesh() helper to build wp.Mesh from height grids and updates HFIELD tests to pass mesh.id.
New intersect_ray Public API & Tests
newton/_src/geometry/__init__.py, newton/__init__.py, newton/geometry.py, newton/tests/test_raycast.py
Re-exports intersect_ray and adds test_intersect_ray() to validate distances, shape IDs, and normals returned by newton.intersect_ray().
Documentation and Changelog
CHANGELOG.md, docs/api/newton*.rst
Documents the new intersect_ray() API, records HFIELD BVH transition, and notes removal of SensorRaycast from docs.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant intersect_ray
  participant _intersect_ray_kernel
  participant BVH
  participant ray_intersect_geom

  Client->>intersect_ray: call(model, ray_origins, ray_dirs, out_*)
  intersect_ray->>_intersect_ray_kernel: launch BVH kernel with model BVH & shape arrays
  _intersect_ray_kernel->>BVH: query BVH nodes for candidate triangles/shape_ids
  BVH-->>_intersect_ray_kernel: candidate mesh_id + tri info
  _intersect_ray_kernel->>ray_intersect_geom: call(mesh_id, geom transform, ray)
  ray_intersect_geom-->>_intersect_ray_kernel: (t_hit, normal)
  _intersect_ray_kernel-->>intersect_ray: write out_dist, out_shape_id, out_normal
  intersect_ray-->>Client: outputs populated in provided buffers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • eric-heiden
  • adenzler-nvidia
  • nvtw
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the two main changes: adding a new intersect_ray function with BVH acceleration and removing the deprecated SensorRaycast, matching the core objectives documented in the PR description.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
newton/_src/sim/builder.py (1)

100-121: ⚡ Quick win

Vectorize the heightfield mesh build path.

This helper now runs during finalize() for every unique Heightfield, so the nested Python loops will add noticeable setup latency on large terrains. NumPy broadcasting can build the same vertex/index buffers in bulk and keep the BVH fast path from paying Python overhead up front.

♻️ Proposed refactor
-    verts = np.empty((nrow * ncol, 3), dtype=np.float32)
-    for r in range(nrow):
-        for c in range(ncol):
-            idx = r * ncol + c
-            verts[idx, 0] = -hf.hx + c * dx
-            verts[idx, 1] = -hf.hy + r * dy
-            verts[idx, 2] = hf.min_z + float(hf.data[r, c]) * z_range
-
-    num_cells = (nrow - 1) * (ncol - 1)
-    indices = np.empty(num_cells * 6, dtype=np.int32)
-    i = 0
-    for r in range(nrow - 1):
-        for c in range(ncol - 1):
-            v00 = r * ncol + c
-            v10 = r * ncol + (c + 1)
-            v01 = (r + 1) * ncol + c
-            v11 = (r + 1) * ncol + (c + 1)
-            # Triangle 0: (p00, p10, p11) — CCW from above
-            indices[i : i + 3] = [v00, v10, v11]
-            # Triangle 1: (p00, p11, p01) — CCW from above
-            indices[i + 3 : i + 6] = [v00, v11, v01]
-            i += 6
+    xs = np.linspace(-hf.hx, hf.hx, ncol, dtype=np.float32)
+    ys = np.linspace(-hf.hy, hf.hy, nrow, dtype=np.float32)
+    grid_x, grid_y = np.meshgrid(xs, ys, indexing="xy")
+    grid_z = hf.min_z + np.asarray(hf.data, dtype=np.float32) * z_range
+    verts = np.stack((grid_x, grid_y, grid_z), axis=-1).reshape(-1, 3)
+
+    cell_rows = np.arange(nrow - 1, dtype=np.int32)[:, None]
+    cell_cols = np.arange(ncol - 1, dtype=np.int32)[None, :]
+    v00 = (cell_rows * ncol + cell_cols).ravel()
+    v10 = v00 + 1
+    v01 = v00 + ncol
+    v11 = v01 + 1
+    indices = np.stack(
+        (
+            v00,
+            v10,
+            v11,
+            v00,
+            v11,
+            v01,
+        ),
+        axis=-1,
+    ).reshape(-1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/_src/sim/builder.py` around lines 100 - 121, The nested loops building
verts and indices (in finalize()/heightfield mesh path) should be replaced with
NumPy vectorized operations: use np.arange and np.meshgrid (or broadcasting)
over r and c to produce flattened row/col arrays, compute verts[:,0],
verts[:,1], and verts[:,2] in bulk from hx/hy/dx/dy and hf.data.ravel() instead
of per-element assignment, and build the triangle index arrays by computing the
four corner indices v00/v10/v01/v11 as vectorized arrays for all cells and then
stacking the two triangle index patterns ([v00,v10,v11] and [v00,v11,v01]) into
the flat indices buffer; keep dtype and shapes the same (verts, indices) and
ensure ordering matches the existing CCW winding.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 22: Update the Changed entry for GeoType.HFIELD to include explicit
migration guidance: tell callers that the raycast kernel no longer accepts
shape_heightfield_index, heightfield_data, or heightfield_elevations and that
direct callers of geometry.raycast_kernel should stop passing those arrays and
instead use the mesh-backed raycast path built during ModelBuilder.finalize()
(via wp.Mesh BVH) by routing through model.shape_source_ptr; also note that
Model still retains the old heightfield arrays for collision kernels that still
rely on them.

In `@newton/_src/geometry/raycast.py`:
- Around line 903-942: The intersect_ray wrapper currently dereferences
model.bvh_shapes, model.bvh_shapes_group_roots and
model.bvh_shape_world_transforms unconditionally; add a guard at the top of
intersect_ray that checks these fields are not None and raises a clear exception
(e.g., ValueError) instructing the caller to call build_bvh_shape() or
refit_bvh_shape() first if they are missing, and update the function docstring
to remove the nonexistent `state` parameter and document the prerequisite that
the shape BVH/world transforms must be built via build_bvh_shape() or
refit_bvh_shape() before calling intersect_ray.

---

Nitpick comments:
In `@newton/_src/sim/builder.py`:
- Around line 100-121: The nested loops building verts and indices (in
finalize()/heightfield mesh path) should be replaced with NumPy vectorized
operations: use np.arange and np.meshgrid (or broadcasting) over r and c to
produce flattened row/col arrays, compute verts[:,0], verts[:,1], and verts[:,2]
in bulk from hx/hy/dx/dy and hf.data.ravel() instead of per-element assignment,
and build the triangle index arrays by computing the four corner indices
v00/v10/v01/v11 as vectorized arrays for all cells and then stacking the two
triangle index patterns ([v00,v10,v11] and [v00,v11,v01]) into the flat indices
buffer; keep dtype and shapes the same (verts, indices) and ensure ordering
matches the existing CCW winding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 247852f1-2833-4cf7-a7ae-8e83a047d7ff

📥 Commits

Reviewing files that changed from the base of the PR and between b976718 and 9af1cb0.

📒 Files selected for processing (16)
  • CHANGELOG.md
  • docs/api/newton.rst
  • docs/api/newton_geometry.rst
  • docs/api/newton_sensors.rst
  • docs/concepts/sensors.rst
  • newton/__init__.py
  • newton/_src/geometry/__init__.py
  • newton/_src/geometry/raycast.py
  • newton/_src/sensors/sensor_raycast.py
  • newton/_src/sim/builder.py
  • newton/_src/sim/model.py
  • newton/_src/viewer/picking.py
  • newton/geometry.py
  • newton/sensors.py
  • newton/tests/test_raycast.py
  • newton/tests/test_sensor_raycast.py
💤 Files with no reviewable changes (5)
  • docs/api/newton_sensors.rst
  • docs/concepts/sensors.rst
  • newton/tests/test_sensor_raycast.py
  • newton/_src/sensors/sensor_raycast.py
  • newton/sensors.py

Comment thread CHANGELOG.md
Comment thread newton/_src/geometry/raycast.py
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 5 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
newton/_src/sim/builder.py 76.92% 3 Missing ⚠️
newton/_src/geometry/raycast.py 84.61% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread newton/_src/sim/builder.py Outdated
@StafaH
StafaH marked this pull request as ready for review June 1, 2026 07:05
@StafaH
StafaH requested a review from a team as a code owner June 1, 2026 07:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
newton/_src/geometry/raycast.py (1)

903-942: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing prerequisite guard and stale docstring parameter.

The past review comment about this issue is still valid:

  1. Stale docstring: Line 915 references a state parameter that doesn't exist in the function signature.

  2. Missing None guard: The function dereferences model.bvh_shapes.id, model.bvh_shapes_group_roots, model.bvh_shape_enabled, and model.bvh_shape_world_transforms without checking if they are None. If build_bvh_shape() was not called, these will crash with an unhelpful AttributeError or TypeError.

Suggested fix
 def intersect_ray(
     model,
     ray_origins: wp.array2d[wp.vec3],
     ray_directions: wp.array2d[wp.vec3],
     out_dist: wp.array2d[float],
     out_shape_id: wp.array2d[wp.int32],
     out_normal: wp.array2d[wp.vec3],
 ) -> None:
     """Intersect rays with model shapes for all worlds.

+    Requires a built shape BVH. Call :func:`build_bvh_shape` before this
+    function and :func:`refit_bvh_shape` after transforms change.
+
     Args:
         model: Model containing the shapes to query.
-        state: State containing current body transforms.
         ray_origins: Ray origins in world space [m].
         ray_directions: Ray directions in world space. Values
             must be normalized and nonzero.
         out_dist: Output array of hit distances.
         out_shape_id: Output array of hit shape indices.
         out_normal: Output array of hit normals.
     """
+    if (
+        model.bvh_shapes is None
+        or model.bvh_shapes_group_roots is None
+        or model.bvh_shape_enabled is None
+        or model.bvh_shape_world_transforms is None
+    ):
+        raise RuntimeError(
+            "intersect_ray() requires a built shape BVH. "
+            "Call build_bvh_shape(model, state) first."
+        )

     wp.launch(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/_src/geometry/raycast.py` around lines 903 - 942, The intersect_ray
docstring and precondition checks need tightening: remove the stale reference to
a non-existent state parameter from intersect_ray's docstring, and add a guard
before dereferencing model.bvh_shapes.id / model.bvh_shapes_group_roots /
model.bvh_shape_enabled / model.bvh_shape_world_transforms to detect when the
BVH has not been built; if any of those are None, raise a clear ValueError (or
similar) instructing the caller to run build_bvh_shape() on the model (mention
intersect_ray and the bvh fields in the message) instead of letting
AttributeError/TypeError occur.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@newton/_src/geometry/raycast.py`:
- Around line 903-942: The intersect_ray docstring and precondition checks need
tightening: remove the stale reference to a non-existent state parameter from
intersect_ray's docstring, and add a guard before dereferencing
model.bvh_shapes.id / model.bvh_shapes_group_roots / model.bvh_shape_enabled /
model.bvh_shape_world_transforms to detect when the BVH has not been built; if
any of those are None, raise a clear ValueError (or similar) instructing the
caller to run build_bvh_shape() on the model (mention intersect_ray and the bvh
fields in the message) instead of letting AttributeError/TypeError occur.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 7c334449-58fa-46bc-ac3e-817fce422871

📥 Commits

Reviewing files that changed from the base of the PR and between 9aac064 and 117c88b.

📒 Files selected for processing (1)
  • newton/_src/geometry/raycast.py

@daniela-hase

daniela-hase commented Jun 1, 2026

Copy link
Copy Markdown
Member

This looks all good to me! :D

Only question I'd have, do we still really need that raycast kernel? Because it really doesn't do anything that the render kernel doesn't do as well, does it?

It is only used for the viewer-picking as far as I can tell, right?

While I personally would probably be in favor of using the render-kernel for this (assuming the performance would be the same), I don't have a super uber strong opinion on this and can see reasons for keeping it separated as well.

@mmacklin @eric-heiden do you guys have an opinion?

daniela-hase
daniela-hase previously approved these changes Jun 1, 2026

@daniela-hase daniela-hase left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, but lets wait for @mmacklin / @eric-heiden to comment on my above before we merge please :)

Comment thread newton/_src/geometry/raycast.py Outdated
Comment thread newton/_src/geometry/raycast.py Outdated
Comment thread newton/__init__.py
Comment thread CHANGELOG.md Outdated
Comment thread newton/_src/geometry/raycast.py Outdated
Comment thread newton/_src/sim/model.py
Comment thread docs/concepts/sensors.rst
Comment thread newton/_src/sim/builder.py
eric-heiden
eric-heiden previously approved these changes Jun 2, 2026
@eric-heiden
eric-heiden enabled auto-merge June 2, 2026 18:32
@eric-heiden eric-heiden added this to the 1.3 Release milestone Jun 2, 2026
@eric-heiden
eric-heiden added this pull request to the merge queue Jun 2, 2026
Merged via the queue into newton-physics:main with commit 6a04ab7 Jun 2, 2026
25 checks passed
jkuzmeski pushed a commit to jkuzmeski/newton that referenced this pull request Jun 2, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jun 3, 2026
3 tasks
@StafaH
StafaH deleted the mh/intersect_ray branch July 9, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[REQ] BVH accelerated raycast sensor

5 participants