Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@

### Removed

- Remove `SensorRaycast` (deprecated in 1.2); use `SensorTiledCamera` with `SensorTiledCamera.utils.compute_pinhole_camera_rays()` and `create_depth_image_output()` instead
- Remove `SensorContact.net_force` (deprecated in 1.1.0); use `SensorContact.total_force` and `SensorContact.force_matrix` instead
- Remove `include_total` parameter from `SensorContact` (deprecated in 1.1.0); use `measure_total` instead
- Remove `SensorContact.sensing_objs` (deprecated in 1.1.0); use `SensorContact.sensing_obj_idx` and `SensorContact.sensing_obj_type` instead
Expand Down
1 change: 1 addition & 0 deletions docs/api/newton_sensors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ newton.sensors
SensorContact
SensorFrameTransform
SensorIMU
SensorRaycast
SensorTiledCamera
4 changes: 3 additions & 1 deletion docs/concepts/sensors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,16 @@ Examples::
Available Sensors
-----------------

Newton provides four sensor types. See the
Newton provides five sensor types. See the
:doc:`API reference <../api/newton_sensors>` for constructor arguments,
attributes, and usage examples.

* :class:`~newton.sensors.SensorContact` -- contact forces between bodies or shapes, including friction decomposition,
with optional per-counterpart breakdown.
* :class:`~newton.sensors.SensorFrameTransform` -- relative transforms of shapes/sites with respect to reference sites.
* :class:`~newton.sensors.SensorIMU` -- linear acceleration and angular velocity at site frames.
* :class:`~newton.sensors.SensorRaycast` -- *(deprecated)* ray-based depth images from a virtual camera; use
:class:`~newton.sensors.SensorTiledCamera` instead.
* :class:`~newton.sensors.SensorTiledCamera` -- raytraced color and depth rendering across multiple worlds.

Extended Attributes
Expand Down
237 changes: 237 additions & 0 deletions newton/_src/geometry/raycast.py
Original file line number Diff line number Diff line change
Expand Up @@ -982,3 +982,240 @@ def _intersect_ray_kernel(
],
device=model.device,
)


@wp.func
def ray_for_pixel(
camera_position: wp.vec3,
camera_direction: wp.vec3,
camera_up: wp.vec3,
camera_right: wp.vec3,
fov_scale: float,
camera_aspect_ratio: float,
resolution: wp.vec2,
pixel_x: int,
pixel_y: int,
):
"""Generate a ray for a given pixel in a perspective camera.

Args:
camera_position: Camera position in world space
camera_direction: Camera forward direction (normalized)
camera_up: Camera up direction (normalized)
camera_right: Camera right direction (normalized)
fov_scale: Scale factor for field of view, ``tan(fov_radians/2)``
camera_aspect_ratio: Width/height aspect ratio
resolution: Image resolution as (width, height)
pixel_x: Pixel x coordinate (0 to width-1)
pixel_y: Pixel y coordinate (0 to height-1)

Returns:
Tuple of (ray_origin, ray_direction) in world space, direction normalized.
"""
width = resolution[0]
height = resolution[1]

# Convert to normalized coordinates [-1, 1] with (0,0) at center
ndc_x = (2.0 * float(pixel_x) + 1.0) / width - 1.0
ndc_y = 1.0 - (2.0 * float(pixel_y) + 1.0) / height # Flip Y axis

# Apply field of view and aspect ratio
cam_x = ndc_x * fov_scale * camera_aspect_ratio
cam_y = ndc_y * fov_scale
cam_z = 1.0 # Forward is negative Z in camera space (camera_direction already looks at -Z)

ray_dir_camera = wp.vec3(cam_x, cam_y, cam_z)

# Transform ray direction from camera to world space
ray_direction_world = (
camera_right * ray_dir_camera[0] + camera_up * ray_dir_camera[1] + camera_direction * ray_dir_camera[2]
)
ray_direction_world = wp.normalize(ray_direction_world)

return camera_position, ray_direction_world


@wp.kernel
def sensor_raycast_kernel(
# Model
body_q: wp.array[wp.transform],
shape_body: wp.array[int],
shape_transform: wp.array[wp.transform],
geom_type: wp.array[int],
geom_size: wp.array[wp.vec3],
shape_source_ptr: wp.array[wp.uint64],
# Camera parameters
camera_position: wp.vec3,
camera_direction: wp.vec3,
camera_up: wp.vec3,
camera_right: wp.vec3,
fov_scale: float,
camera_aspect_ratio: float,
resolution: wp.vec2,
# Output (per-pixel results)
hit_distances: wp.array2d[float],
):
"""Raycast sensor kernel that casts rays for each pixel in an image.

Each thread processes one pixel-shape pair, generating a ray and recording the
closest intersection per pixel. HFIELD shapes are raycast via their ``wp.Mesh``
id from ``shape_source_ptr`` (built during :meth:`~newton.ModelBuilder.finalize`).

Args:
body_q: Array of body transforms
shape_body: Maps shape index to body index
shape_transform: Array of local shape transforms
geom_type: Array of geometry types for each geometry
geom_size: Array of sizes for each geometry
shape_source_ptr: Array of mesh ids for MESH, CONVEX_MESH, and HFIELD geometries
camera_position: Camera position in world space
camera_direction: Camera forward direction (normalized)
camera_up: Camera up direction (normalized)
camera_right: Camera right direction (normalized)
fov_scale: Scale factor for field of view, computed as tan(fov_radians/2) where fov_radians is the vertical field of view angle in radians
camera_aspect_ratio: Width/height aspect ratio
resolution: Image resolution as (width, height)
hit_distances: Output array of hit distances per pixel
"""
pixel_x, pixel_y, shape_idx = wp.tid()

# Skip if out of bounds
if pixel_x >= resolution[0] or pixel_y >= resolution[1]:
return

# Generate ray for this pixel
ray_origin, ray_direction = ray_for_pixel(
camera_position,
camera_direction,
camera_up,
camera_right,
fov_scale,
camera_aspect_ratio,
resolution,
pixel_x,
pixel_y,
)

# compute shape transform
b = shape_body[shape_idx]

X_wb = wp.transform_identity()
if b >= 0:
X_wb = body_q[b]

X_bs = shape_transform[shape_idx]

geom_to_world = wp.mul(X_wb, X_bs)

geomtype = geom_type[shape_idx]

# Get mesh ID for mesh-like geometries
if geomtype == GeoType.MESH or geomtype == GeoType.CONVEX_MESH or geomtype == GeoType.HFIELD:
mesh_id = shape_source_ptr[shape_idx]
else:
mesh_id = wp.uint64(0)

t, _normal = ray_intersect_geom(
geom_to_world,
geom_size[shape_idx],
geomtype,
ray_origin,
ray_direction,
mesh_id,
)

if t >= 0.0:
wp.atomic_min(hit_distances, pixel_y, pixel_x, t)


@wp.kernel
def sensor_raycast_particles_kernel(
grid: wp.uint64,
particle_positions: wp.array[wp.vec3],
particle_radius: wp.array[float],
search_radius: float,
march_step: float,
max_steps: wp.int32,
camera_position: wp.vec3,
camera_direction: wp.vec3,
camera_up: wp.vec3,
camera_right: wp.vec3,
fov_scale: float,
camera_aspect_ratio: float,
resolution: wp.vec2,
max_distance: float,
hit_distances: wp.array2d[float],
):
"""March rays against particles stored in a hash grid and record the nearest hit if found before max_distance.

Args:
grid: The hash grid containing the particles.
particle_positions: Array of particle positions.
particle_radius: Array of particle radii.
search_radius: The radius around each sample point to search for nearby particles.
march_step: The step size for ray marching.
max_steps: Maximum number of ray-march iterations allowed for a pixel.
camera_position: Camera position in world space.
camera_direction: Camera forward direction (normalized); rays travel along this vector.
camera_up: Camera up direction (normalized).
camera_right: Camera right direction (normalized).
fov_scale: Scale factor for field of view, computed as tan(fov_radians/2) where fov_radians is the vertical field of view angle in radians.
camera_aspect_ratio: Width/height aspect ratio.
resolution: Image resolution as (width, height).
max_distance: Maximum distance to march along the ray.
hit_distances: Output array of hit distances per pixel.
"""
pixel_x, pixel_y = wp.tid()

if pixel_x >= resolution[0] or pixel_y >= resolution[1]:
return

ray_origin, ray_direction = ray_for_pixel(
camera_position,
camera_direction,
camera_up,
camera_right,
fov_scale,
camera_aspect_ratio,
resolution,
pixel_x,
pixel_y,
)

best = hit_distances[pixel_y, pixel_x]
if best < 0.0:
best = max_distance

search_radius_local = search_radius
step = march_step

s = wp.int32(0)
t = float(0.0)

while s < max_steps and t <= max_distance and t <= best:
sample_pos = ray_origin + ray_direction * t

query = wp.hash_grid_query(grid, sample_pos, search_radius_local)
candidate = int(0)

while wp.hash_grid_query_next(query, candidate):
# Intersect ray with particle sphere
radius = particle_radius[candidate]
if radius <= 0.0:
continue

center = particle_positions[candidate]
t_hit, _normal = ray_intersect_particle_sphere(ray_origin, ray_direction, center, radius)

if t_hit < 0.0:
continue

if t_hit > max_distance:
continue

if t_hit < best:
hit_distances[pixel_y, pixel_x] = t_hit
best = t_hit

s += 1
t += step
Loading
Loading