Scene and mesh (PyTorch)

rayd.torch Mesh, Ray, Scene, MultiDeviceOptions, and Camera - construction, updates, ray, edge, visibility, reflection, and diffraction queries on CUDA tensors.

The PyTorch backend takes and returns CUDA tensors. Results are frozen dataclasses with a batch dimension; see Records.

import rayd.torch as rt

Unless noted, every tensor must be a contiguous CUDA tensor on the scene's device: positions and directions are float32 with shape (N, 3), indices are int32, and masks are torch.bool with shape (N,). active=None means all lanes. When any continuous input requires a gradient, the query records an autograd node.

Mesh#

Mesh(
    vertices: Tensor,
    faces: Tensor,
    uv: Tensor | None = None,
    face_uv: Tensor | None = None,
    use_face_normals: bool = False,
    edges_enabled: bool = True,
    to_world_left: Tensor | None = None,
    to_world_right: Tensor | None = None,
)
verticesTensorrequired

(V, 3) float32 vertex positions. May require a gradient.

facesTensorrequired

(F, 3) int32 vertex indices.

uvTensor | Nonedefault None

Optional per-vertex texture coordinates.

face_uvTensor | Nonedefault None

Optional per-face UV indices.

use_face_normalsbooldefault False

Shade with face normals.

edges_enabledbooldefault True

Include this mesh's edges in edge queries.

to_world_leftTensor | Nonedefault None

Optional (4, 4) float32 transform applied on the left (column vectors).

to_world_rightTensor | Nonedefault None

Optional (4, 4) float32 transform applied on the right.

Ray#

Ray(o: Tensor, d: Tensor, tmax: Tensor | None = None)

A frozen batch of rays. o and d are (N, 3) float32; directions need not be normalized. tmax is an optional (N,) tensor; None means unbounded.

RayFlags#

RayFlags.Geometric (1), RayFlags.ShadingN (2), RayFlags.UV (4), and RayFlags.All (7). Same meaning as in Dr.Jit.

Scene#

Scene(
    trace_backend: str = "auto",
    edge_bvh_backend: str = "auto",
    *,
    devices: Sequence[int | str | torch.device] | None = None,
    options: MultiDeviceOptions | None = None,
)
trace_backendstrdefault 'auto'

"auto", "optix", or "cuda".

edge_bvh_backendstrdefault 'auto'

"auto", "optix", or "cuda".

devicesSequence | Nonedefault None

Replicate the scene on these CUDA devices and split each batch across them. None uses the device of the input tensors.

optionsMultiDeviceOptions | Nonedefault None

Tuning for a multi-device scene.

Properties after build(): trace_backend, edge_bvh_backend (resolved names), num_meshes, num_geometries, version, and, for a multi-device scene, device_weights.

Building and updating#

MethodDescription
add_mesh(mesh, dynamic=False) -> intadds a mesh, returns its id
add_instance(geometry_id, transform, dynamic=True) -> intinstance of a static mesh with a (4, 4) transform; not available with devices
build() -> Nonebuilds acceleration structures
update_mesh_vertices(mesh_id, positions) -> Nonequeues new (V, 3) positions for a dynamic mesh
set_instance_transform(instance_id, transform) -> Nonequeues a new transform for a dynamic instance
set_edge_mask(mask) -> Nonequeues a (E,) bool mask over scene-global edges
edge_mask() -> Tensorcurrent edge mask
sync() -> Noneapplies queued updates
has_pending_updates() -> bool, is_ready() -> boolstate
global_geometry() -> SceneGlobalGeometryall world-space triangles with ids

intersect#

intersect(ray: Ray, active: Tensor | None = None, flags: RayFlags = RayFlags.All) -> Intersection

Closest hit per ray. Returns an Intersection with (N,) and (N, 3) tensors; a miss has t = inf and ids of -1.

nearest_edge, nearest_edges#

nearest_edge(point: Tensor | Ray) -> NearestPointEdge | NearestRayEdge
nearest_edges(point: Tensor, k: int, active: Tensor | None = None) -> NearestEdgesTopK

Closest edge to each point (N, 3) or ray, and the k closest (at most 16) as (N, k) tensors.

Visibility#

visible(start: Tensor, end: Tensor, active: Tensor | None = None) -> Tensor
visible_pair(start, end_a, end_b, ignore_prim_ids: Tensor | None = None, active=None) -> SegmentPairVisibility
visible_edge(source, edge_position, edge_direction, edge_t_min, edge_t_max,
             sample_fractions: Iterable[float] = (0.0, 0.25, 0.5, 0.75, 1.0), active=None) -> AxialEdgeVisibility
visible_chain(points: Tensor, chain_length: Tensor, ignore_prim_per_segment: Tensor | None = None, active=None) -> SegmentChainVisibility

visible returns a (N,) bool tensor directly. visible_chain takes points as (N, P, 3). Semantics match the Dr.Jit visibility queries.

Reflection#

trace_reflections(ray: Ray, max_bounces: int, active: Tensor | None = None) -> ReflectionChain
trace_refl_epc(source: Tensor, receiver: Tensor, max_bounces: int, options: ReflEpcOptions, active=None) -> ReflEpc
trace_refl_epc_field(source: Tensor, receiver: Tensor, max_bounces: int, active=None) -> ReflEpcField
accumulate_reflections(ray: Ray, tx_position: Tensor, grid: AccumGrid, material: ReflMaterial, max_bounces: int,
                       options: AccumOptions | None = None, active=None, tx_polarization: Tensor | None = None) -> AccumResult

trace_reflections returns (N, max_bounces) tensors and supports gradients. trace_refl_epc_field and accumulate_reflections are forward-only: they raise RuntimeError when an input requires a gradient or carries a forward-AD tangent.

Diffraction#

accum_dfr_direct(*, states: DfrStates, grid: DfrGrid, material: DfrMaterial | None = None, active=None,
                 wavelength: float = 1.0, direct_samples: int = 0, keller_samples: int = 0, suffix_samples: int = 0,
                 seed: int = 0, lane_offset: int = 0, lane_count: int = -1) -> DfrAccum
accum_dfr(initial_states=None, recursive_states=None, grid=None, material=None, active=None, recursive_active=None,
          wavelength=1.0, direct_samples=0, keller_samples=0, suffix_samples=0, seed=0, max_order=2,
          lane_offset=0, lane_count=-1, **kwargs) -> DfrAccum
accum_dfr_coherent_direct(*, states: DfrStates, grid: DfrGrid, material=None, active=None, wavelength=1.0,
                          select_diffraction_point=True, prefilter_visibility=True, lane_offset=0, lane_count=-1) -> DfrCoherentAccum
trace_dfr_paths(*, tx_positions: Tensor, rx_positions: Tensor, states: DfrStates, material: DfrMaterial | None = None,
                active: Tensor, max_paths: int | None = None, wavelength: float = 1.0, layout: DfrPathLayout | None = None) -> DfrPaths

material=None uses default materials. lane_offset and lane_count (-1 = all) select a window of the Monte Carlo sample space; disjoint windows sum to the full result. trace_dfr_paths requires active and is forward-only; layout chooses DfrPathLayout.Compact or SourceLane rows.

acc = scene.accum_dfr_direct(states=states, grid=rt.DfrGrid(resolution0=4, resolution1=4),
                             material=rt.DfrMaterial.default(1, device="cuda"),
                             wavelength=1.0, direct_samples=4096, seed=1)

Multi-device#

calibrate_devices(*, operation: str | None = None, rays: int = 1 << 20, max_bounces: int = 0,
                  probe=None, repeats: int = 3, warm_up: int = 1, refine: bool = True) -> DeviceCalibration
device_weights_for(operation: str) -> tuple[float, ...] | None

calibrate_devices times a probe (intersect, or trace_reflections when max_bounces is set) on each device and stores the best split for that operation. To time another call, pass both operation and probe(scene, device). Raises RuntimeError on a scene with fewer than two devices.

MultiDeviceOptions#

MultiDeviceOptions(
    weights=None, operation_weights=None, require_peer_access=True, require_homogeneous_devices=True,
    warm_up=True, chunk_rays=None, offload=None, tape_memory_budget_bytes=None, pipeline=True,
    pipeline_chunks_per_device=4, min_rays_per_device=262144, min_lanes_per_device=262144,
)
FieldMeaning
weightsshard split per device as a ratio; equal by default; 0 leaves a device idle
operation_weightssplit for an operation family ("intersect") or exact shape ("trace_reflections:4")
require_peer_accessreject devices without bidirectional CUDA peer access
require_homogeneous_devicesreject a mix of GPU models
warm_uppre-link OptiX pipelines on each device during build()
chunk_raysrows per launch for the chunked executor
offloadoffload(chunk_start_row, chunk_result) hook; per-ray results are streamed and the call returns None
tape_memory_budget_bytesmemory budget that sizes the chunks
pipeline, pipeline_chunks_per_deviceoverlap scatter, compute, and gather
min_rays_per_device, min_lanes_per_devicesmaller remote shards run on the first device instead

See Select devices and use several GPUs.

Camera#

Camera(width: int, height: int, fov_x: float)

A camera-space pinhole helper at the origin looking along +z. fov_x is the horizontal field of view in degrees, in (0, 180). Property aspect.

MethodDescription
sample_ray(sample: Tensor) -> Rayrays through (N, 2) image samples in [0, 1]; (0.5, 0.5) gives d = (0, 0, 1)
sample_to_world(sample, depth=1.0) -> Tensorcamera-space point at depth
world_to_sample(point) -> Tensorimage sample of a camera-space point

The camera has no pose; transform the rays yourself.

Module functions#

backend_capabilities() -> dict returns the flat feature map (including torch_compile and multi_device_replicated). api_manifest() -> dict returns the API and derivative manifest; see Differentiation.