Skip to content

ray

triwarp.ray

Ray and point-in-mesh queries on wp.Mesh.

intersects_location

intersects_location(
    mesh: Mesh,
    ray_origins: array[vec3],
    ray_directions: array[vec3],
    *,
    max_t: float | None = None,
) -> tuple[
    wp.array[wp.vec3],
    wp.array[wp.int32],
    wp.array[wp.int32],
]

Return world-space locations where rays hit the mesh surface (first hit per ray).

Uses wp.mesh_query_ray on the mesh BVH. Ray directions are unitized before querying. Returns only rays that hit within max_t as sparse (m,) arrays, carrying the same set of hits as the dense output of intersects_first.

Row order is unspecified. Each hitting ray claims its output slot from an atomic counter, so rows arrive in the order the rays completed rather than in ray order, and two runs over the same input may order them differently. Sort by index_ray (or read intersects_first instead) when a stable order matters.

Parameters:

Name Type Description Default
mesh Mesh

Triangle mesh with a built BVH (wp.Mesh).

required
ray_origins array[vec3]

(n,) ray origin positions as wp.vec3.

required
ray_directions array[vec3]

(n,) ray direction vectors as wp.vec3 (need not be unit length).

required
max_t float | None

Optional maximum parametric distance along each normalized ray. When None, derived from the combined mesh-and-origin AABB diagonal.

None

Returns:

Type Description
locations

(m,) intersection points.

index_ray

(m,) index of the ray that produced each hit.

index_tri

(m,) face indices for each hit.

Raises:

Type Description
ValueError

If ray_origins and ray_directions do not have the same shape.

RuntimeError

If mesh, ray_origins and ray_directions are not all on one device.

Source code in triwarp/ray.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def intersects_location(
    mesh: wp.Mesh,
    ray_origins: wp.array[wp.vec3],
    ray_directions: wp.array[wp.vec3],
    *,
    max_t: float | None = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32], wp.array[wp.int32]]:
    """
    Return world-space locations where rays hit the mesh surface (first hit per ray).

    Uses ``wp.mesh_query_ray`` on the mesh BVH. Ray directions are unitized before
    querying. Returns only rays that hit within ``max_t`` as sparse ``(m,)`` arrays,
    carrying the same set of hits as the dense output of
    [`intersects_first`][triwarp.ray.intersects_first].

    **Row order is unspecified.** Each hitting ray claims its output slot from an atomic
    counter, so rows arrive in the order the rays completed rather than in ray order, and two
    runs over the same input may order them differently. Sort by ``index_ray`` (or read
    [`intersects_first`][triwarp.ray.intersects_first] instead) when a stable order matters.

    Parameters
    ----------
    mesh
        Triangle mesh with a built BVH (``wp.Mesh``).
    ray_origins
        ``(n,)`` ray origin positions as ``wp.vec3``.
    ray_directions
        ``(n,)`` ray direction vectors as ``wp.vec3`` (need not be unit length).
    max_t
        Optional maximum parametric distance along each normalized ray. When
        ``None``, derived from the combined mesh-and-origin AABB diagonal.

    Returns
    -------
    locations
        ``(m,)`` intersection points.
    index_ray
        ``(m,)`` index of the ray that produced each hit.
    index_tri
        ``(m,)`` face indices for each hit.

    Raises
    ------
    ValueError
        If ``ray_origins`` and ``ray_directions`` do not have the same shape.
    RuntimeError
        If ``mesh``, ``ray_origins`` and ``ray_directions`` are not all on one device.
    """
    require_same_device(mesh=mesh, ray_origins=ray_origins, ray_directions=ray_directions)
    _validate_ray_inputs(ray_origins, ray_directions)
    n = ray_origins.shape[0]
    device = ray_origins.device
    if n == 0:
        empty_int = wp.empty(0, dtype=wp.int32, device=device)
        return wp.empty(0, dtype=wp.vec3, device=device), empty_int, empty_int

    if max_t is None:
        max_t = enclosing_diagonal(mesh.points, ray_origins)

    index_ray = wp.empty(n, dtype=wp.int32, device=device)
    index_tri = wp.empty(n, dtype=wp.int32, device=device)
    locations = wp.empty(n, dtype=wp.vec3, device=device)
    counter = wp.zeros(1, dtype=wp.int32, device=device)
    wp.launch(
        kernel_ray.first_hit_append,
        dim=n,
        inputs=[
            wp.uint64(mesh.id),
            ray_origins,
            ray_directions,
            wp.float32(max_t),
            index_ray,
            index_tri,
            locations,
            counter,
        ],
        device=device,
    )
    _, (locations, index_ray, index_tri) = tw.array.trim_to_count(
        counter, locations, index_ray, index_tri
    )
    return locations, index_ray, index_tri

intersects_first

intersects_first(
    mesh: Mesh,
    ray_origins: array[vec3],
    ray_directions: array[vec3],
    *,
    max_t: float | None = None,
) -> wp.array[wp.int32]

Find the index of the first triangle each ray hits.

Uses wp.mesh_query_ray on the mesh BVH. Ray directions are unitized before querying. The search distance along each ray defaults to the diagonal of the axis-aligned bounding box enclosing mesh vertices and ray origins.

Parameters:

Name Type Description Default
mesh Mesh

Triangle mesh with a built BVH (wp.Mesh).

required
ray_origins array[vec3]

(n,) ray origin positions as wp.vec3.

required
ray_directions array[vec3]

(n,) ray direction vectors as wp.vec3 (need not be unit length).

required
max_t float | None

Optional maximum parametric distance along each normalized ray. When None, derived from the combined mesh-and-origin AABB diagonal.

None

Returns:

Type Description
array[int32]

(n,) face indices; -1 when a ray misses within max_t.

Raises:

Type Description
ValueError

If ray_origins and ray_directions do not have the same shape.

RuntimeError

If mesh, ray_origins and ray_directions are not all on one device.

Source code in triwarp/ray.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def intersects_first(
    mesh: wp.Mesh,
    ray_origins: wp.array[wp.vec3],
    ray_directions: wp.array[wp.vec3],
    *,
    max_t: float | None = None,
) -> wp.array[wp.int32]:
    """
    Find the index of the first triangle each ray hits.

    Uses ``wp.mesh_query_ray`` on the mesh BVH. Ray directions are unitized before
    querying. The search distance along each ray defaults to the diagonal of the
    axis-aligned bounding box enclosing mesh vertices and ray origins.

    Parameters
    ----------
    mesh
        Triangle mesh with a built BVH (``wp.Mesh``).
    ray_origins
        ``(n,)`` ray origin positions as ``wp.vec3``.
    ray_directions
        ``(n,)`` ray direction vectors as ``wp.vec3`` (need not be unit length).
    max_t
        Optional maximum parametric distance along each normalized ray. When
        ``None``, derived from the combined mesh-and-origin AABB diagonal.

    Returns
    -------
    wp.array[wp.int32]
        ``(n,)`` face indices; ``-1`` when a ray misses within ``max_t``.

    Raises
    ------
    ValueError
        If ``ray_origins`` and ``ray_directions`` do not have the same shape.
    RuntimeError
        If ``mesh``, ``ray_origins`` and ``ray_directions`` are not all on one device.
    """
    require_same_device(mesh=mesh, ray_origins=ray_origins, ray_directions=ray_directions)
    _validate_ray_inputs(ray_origins, ray_directions)
    n = ray_origins.shape[0]
    if n == 0:
        return wp.empty(0, dtype=wp.int32, device=ray_origins.device)
    if max_t is None:
        max_t = enclosing_diagonal(mesh.points, ray_origins)

    out_triangle_index = wp.empty(n, dtype=wp.int32, device=ray_origins.device)
    # ``first_hit`` returns ``(face, location)`` and ``wp.map`` wants one ``out=`` per returned
    # value, so the location is written and dropped. Kept rather than given a face-only
    # ``@wp.func`` of its own: that would generate a second ``map_*`` module to save one
    # allocation and an (n,) vec3 store, well under a percent of a call this size.
    locations_scratch = wp.empty(n, dtype=wp.vec3, device=ray_origins.device)
    wp.map(
        kernel_ray.first_hit,
        wp.uint64(mesh.id),
        ray_origins,
        ray_directions,
        wp.float32(max_t),
        out=[out_triangle_index, locations_scratch],
    )
    return out_triangle_index

intersects_any

intersects_any(
    mesh: Mesh,
    ray_origins: array[vec3],
    ray_directions: array[vec3],
    *,
    max_t: float | None = None,
) -> wp.array[wp.bool]

Check whether each ray hits the mesh surface.

Uses wp.mesh_query_ray_anyhit on the mesh BVH. Ray directions are unitized before querying. The search distance along each ray defaults to the diagonal of the axis-aligned bounding box enclosing mesh vertices and ray origins.

Parameters:

Name Type Description Default
mesh Mesh

Triangle mesh with a built BVH (wp.Mesh).

required
ray_origins array[vec3]

(n,) ray origin positions as wp.vec3.

required
ray_directions array[vec3]

(n,) ray direction vectors as wp.vec3 (need not be unit length).

required
max_t float | None

Optional maximum parametric distance along each normalized ray. When None, derived from the combined mesh-and-origin AABB diagonal.

None

Returns:

Type Description
array[bool]

(n,) hit flags; True when a ray hits within max_t.

Raises:

Type Description
ValueError

If ray_origins and ray_directions do not have the same shape.

RuntimeError

If mesh, ray_origins and ray_directions are not all on one device.

Source code in triwarp/ray.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def intersects_any(
    mesh: wp.Mesh,
    ray_origins: wp.array[wp.vec3],
    ray_directions: wp.array[wp.vec3],
    *,
    max_t: float | None = None,
) -> wp.array[wp.bool]:
    """
    Check whether each ray hits the mesh surface.

    Uses ``wp.mesh_query_ray_anyhit`` on the mesh BVH. Ray directions are unitized
    before querying. The search distance along each ray defaults to the diagonal of
    the axis-aligned bounding box enclosing mesh vertices and ray origins.

    Parameters
    ----------
    mesh
        Triangle mesh with a built BVH (``wp.Mesh``).
    ray_origins
        ``(n,)`` ray origin positions as ``wp.vec3``.
    ray_directions
        ``(n,)`` ray direction vectors as ``wp.vec3`` (need not be unit length).
    max_t
        Optional maximum parametric distance along each normalized ray. When
        ``None``, derived from the combined mesh-and-origin AABB diagonal.

    Returns
    -------
    wp.array[wp.bool]
        ``(n,)`` hit flags; ``True`` when a ray hits within ``max_t``.

    Raises
    ------
    ValueError
        If ``ray_origins`` and ``ray_directions`` do not have the same shape.
    RuntimeError
        If ``mesh``, ``ray_origins`` and ``ray_directions`` are not all on one device.
    """
    require_same_device(mesh=mesh, ray_origins=ray_origins, ray_directions=ray_directions)
    _validate_ray_inputs(ray_origins, ray_directions)
    n = ray_origins.shape[0]
    if n == 0:
        return wp.empty(0, dtype=wp.bool, device=ray_origins.device)
    if max_t is None:
        max_t = enclosing_diagonal(mesh.points, ray_origins)

    out_hit = wp.empty(n, dtype=wp.bool, device=ray_origins.device)
    wp.map(
        kernel_ray.any_hit,
        wp.uint64(mesh.id),
        ray_origins,
        ray_directions,
        wp.float32(max_t),
        out=out_hit,
    )
    return out_hit

longest_ray

longest_ray(
    mesh: Mesh,
    ray_origins: array[vec3],
    ray_directions: array[vec3],
    *,
    max_t: float | None = None,
    planar_tol: float = TOLERANCE_PLANAR,
) -> wp.array[wp.float32]

Find the length of the longest unobstructed ray segment along each direction.

Uses iterative wp.mesh_query_ray on the mesh BVH. Ray directions are unitized before querying. For each ray, returns the distance to the first mesh intersection strictly beyond planar_tol (to ignore degenerate on-surface hits), or inf when no such intersection exists within max_t.

Equivalent to trimesh.proximity.longest_ray.

Parameters:

Name Type Description Default
mesh Mesh

Triangle mesh with a built BVH (wp.Mesh).

required
ray_origins array[vec3]

(n,) ray origin positions as wp.vec3.

required
ray_directions array[vec3]

(n,) ray direction vectors as wp.vec3 (need not be unit length).

required
max_t float | None

Optional maximum parametric distance along each normalized ray. When None, derived from the combined mesh-and-origin AABB diagonal.

None
planar_tol float

Ignore intersections closer than this distance from the ray origin.

TOLERANCE_PLANAR

Returns:

Type Description
array[float32]

(n,) unobstructed ray lengths; inf when a ray misses within max_t.

Raises:

Type Description
ValueError

If ray_origins and ray_directions do not have the same shape.

RuntimeError

If mesh, ray_origins and ray_directions are not all on one device.

Source code in triwarp/ray.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
def longest_ray(
    mesh: wp.Mesh,
    ray_origins: wp.array[wp.vec3],
    ray_directions: wp.array[wp.vec3],
    *,
    max_t: float | None = None,
    planar_tol: float = TOLERANCE_PLANAR,
) -> wp.array[wp.float32]:
    """
    Find the length of the longest unobstructed ray segment along each direction.

    Uses iterative ``wp.mesh_query_ray`` on the mesh BVH. Ray directions are
    unitized before querying. For each ray, returns the distance to the first mesh
    intersection strictly beyond ``planar_tol`` (to ignore degenerate on-surface
    hits), or ``inf`` when no such intersection exists within ``max_t``.

    Equivalent to [`trimesh.proximity.longest_ray`][].

    Parameters
    ----------
    mesh
        Triangle mesh with a built BVH (``wp.Mesh``).
    ray_origins
        ``(n,)`` ray origin positions as ``wp.vec3``.
    ray_directions
        ``(n,)`` ray direction vectors as ``wp.vec3`` (need not be unit length).
    max_t
        Optional maximum parametric distance along each normalized ray. When
        ``None``, derived from the combined mesh-and-origin AABB diagonal.
    planar_tol
        Ignore intersections closer than this distance from the ray origin.

    Returns
    -------
    wp.array[wp.float32]
        ``(n,)`` unobstructed ray lengths; ``inf`` when a ray misses within ``max_t``.

    Raises
    ------
    ValueError
        If ``ray_origins`` and ``ray_directions`` do not have the same shape.
    RuntimeError
        If ``mesh``, ``ray_origins`` and ``ray_directions`` are not all on one device.
    """
    require_same_device(mesh=mesh, ray_origins=ray_origins, ray_directions=ray_directions)
    _validate_ray_inputs(ray_origins, ray_directions)
    n = ray_origins.shape[0]
    if n == 0:
        return wp.empty(0, dtype=wp.float32, device=ray_origins.device)
    if max_t is None:
        max_t = enclosing_diagonal(mesh.points, ray_origins)

    out_distances = wp.empty(n, dtype=wp.float32, device=ray_origins.device)
    wp.map(
        kernel_ray.longest_ray_distance,
        wp.uint64(mesh.id),
        ray_origins,
        ray_directions,
        wp.float32(max_t),
        wp.float32(planar_tol),
        out=out_distances,
    )
    return out_distances

contains_points

contains_points(
    mesh: Mesh,
    points: array[vec3],
    *,
    n_sample: int = 5,
    perturbation_scale: float = 0.1,
) -> wp.array[wp.bool]

Test whether query points lie inside a closed mesh (ray parity sign).

Uses wp.mesh_query_point_sign_parity on the mesh BVH, sharing the same parity path as signed_distance_on_mesh. Points outside the mesh axis-aligned bounding box are rejected without a parity test.

Triwarp mesh queries use Warp's SDF sign convention (outside positive, inside negative). A point is classified as inside when its signed distance would be negative; behavior on the on-surface tolerance band is undefined. Boolean results still match trimesh.Trimesh.contains.

Non-watertight meshes

Ray parity has no principled answer on an open or holed surface. For those, use the generalized winding-number sign instead:

from triwarp.kernels import array as kernel_array

signed = tw.proximity.signed_distance_on_mesh(v, f, pts, sign_mode="winding")
inside = wp.empty(signed.shape, dtype=wp.bool, device=signed.device)
wp.map(kernel_array.less, signed, wp.float32(0.0), out=inside)

That mode is deliberately not offered here. It needs a wp.Mesh built with support_winding_number=True, and Warp neither records that flag on the mesh object nor errors when it is missing — it silently falls back to ray parity. Since this function takes a caller-supplied wp.Mesh, triwarp cannot verify the flag, so the option would be able to quietly return the parity answer. signed_distance_on_mesh builds its own mesh and therefore can guarantee it.

Parameters:

Name Type Description Default
mesh Mesh

Triangle mesh with a built BVH (wp.Mesh).

required
points array[vec3]

(n,) query positions as wp.vec3.

required
n_sample int

Number of perturbed rays for parity voting (higher is more robust).

5
perturbation_scale float

Uniform perturbation scale applied to the base ray direction.

0.1

Returns:

Type Description
array[bool]

(n,) flags; True when the point is classified as inside the mesh.

Raises:

Type Description
RuntimeError

If mesh and points are not all on one device.

See Also

signed_distance_on_mesh

Source code in triwarp/ray.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
def contains_points(
    mesh: wp.Mesh, points: wp.array[wp.vec3], *, n_sample: int = 5, perturbation_scale: float = 0.1
) -> wp.array[wp.bool]:
    """
    Test whether query points lie inside a closed mesh (ray parity sign).

    Uses ``wp.mesh_query_point_sign_parity`` on the mesh BVH, sharing the same
    parity path as
    [`signed_distance_on_mesh`][triwarp.proximity.signed_distance_on_mesh]. Points
    outside the mesh axis-aligned bounding box are rejected without a parity test.

    Triwarp mesh queries use **Warp's SDF sign convention** (outside positive,
    inside negative). A point is classified as inside when its signed distance
    would be negative; behavior on the on-surface tolerance band is undefined.
    Boolean results still match [`trimesh.Trimesh.contains`][].

    !!! note "Non-watertight meshes"

        Ray parity has no principled answer on an open or holed surface. For those, use the
        generalized winding-number sign instead:

        ```python
        from triwarp.kernels import array as kernel_array

        signed = tw.proximity.signed_distance_on_mesh(v, f, pts, sign_mode="winding")
        inside = wp.empty(signed.shape, dtype=wp.bool, device=signed.device)
        wp.map(kernel_array.less, signed, wp.float32(0.0), out=inside)
        ```

        That mode is deliberately **not** offered here. It needs a ``wp.Mesh`` built with
        ``support_winding_number=True``, and Warp neither records that flag on the mesh object nor
        errors when it is missing — it silently falls back to ray parity. Since this function takes
        a caller-supplied ``wp.Mesh``, triwarp cannot verify the flag, so the option would be able
        to quietly return the parity answer.
        [`signed_distance_on_mesh`][triwarp.proximity.signed_distance_on_mesh] builds its own mesh
        and therefore can guarantee it.

    Parameters
    ----------
    mesh
        Triangle mesh with a built BVH (``wp.Mesh``).
    points
        ``(n,)`` query positions as ``wp.vec3``.
    n_sample
        Number of perturbed rays for parity voting (higher is more robust).
    perturbation_scale
        Uniform perturbation scale applied to the base ray direction.

    Returns
    -------
    wp.array[wp.bool]
        ``(n,)`` flags; ``True`` when the point is classified as inside the mesh.

    Raises
    ------
    RuntimeError
        If ``mesh`` and ``points`` are not all on one device.

    See Also
    --------
    [`signed_distance_on_mesh`][triwarp.proximity.signed_distance_on_mesh]
    """
    require_same_device(mesh=mesh, points=points)
    n = points.shape[0]
    if n == 0:
        return wp.empty(0, dtype=wp.bool, device=points.device)
    # One reduction, not two: ``enclosing_diagonal(mesh.points)`` would recompute exactly these
    # corners, and the parity kernel needs both them and the diagonal, so compute the AABB once
    # and derive the diagonal from it directly.
    mesh_min, mesh_max = aabb(mesh.points)
    max_dist = float(wp.length(mesh_max - mesh_min))
    out_contains = wp.empty(n, dtype=wp.bool, device=points.device)
    wp.launch(
        kernel_proximity.contains_points_sign_parity,
        dim=n,
        inputs=[
            mesh.id,
            points,
            wp.float32(max_dist),
            wp.int32(n_sample),
            wp.float32(perturbation_scale),
            mesh_min,
            mesh_max,
            out_contains,
        ],
        device=points.device,
    )
    return out_contains