Test visibility

Check whether segments, segment pairs, multi-segment chains, and points along an edge are unobstructed, and ignore chosen primitives.

Visibility queries answer "is this segment clear?" for whole batches. They are the building block for line-of-sight tests, path validation in RF tracing, and shadow rays. The results are masks and carry no gradient.

The examples use a large floor at z = 0 (global faces 0 and 1) and a small blocker at z = 1 (global faces 2 and 3).

import rayd.drjit as rd
from drjit.cuda import Array3f, Array3i, Float, Int
 
def quad(z, s=1.0):
    return rd.Mesh(Array3f([-s, s, s, -s], [-s, -s, s, s], [z, z, z, z]),
                   Array3i([0, 0], [1, 2], [2, 3]))
 
scene = rd.Scene()
scene.add_mesh(quad(0.0))         # floor
scene.add_mesh(quad(1.0, 0.25))   # blocker, |x|, |y| <= 0.25
scene.build()

One segment per lane#

visible(start, end) tests the open segment between each pair of points. Hits at the endpoints themselves do not count, so points that lie on a surface are fine.

start = Array3f([0.0, 0.0], [0.0, 0.0], [2.0, 2.0])
end = Array3f([0.0, 0.9], [0.0, 0.9], [0.5, 0.5])
print(scene.visible(start, end).visible)   # [False, True]

In Dr.Jit the result is a SegmentVisibility with visible and ray_count. In Torch, Scene.visible(start, end) returns the bool tensor directly.

Ignore primitives#

Path validation often needs to skip the surfaces a path reflects from. ignore_prim_ids lists global primitive ids to skip, K per segment, so its width is a multiple of the segment count. Use -1 to pad unused slots.

ignore = Int([2, 3,  2, 3])                 # K = 2 ids for each of the 2 segments
print(scene.visible(start, end, ignore_prim_ids=ignore).visible)   # [True, True]

Two segments from one start#

visible_pair(start, end_a, end_b) tests two segments that share a start point, for example a reflection point seen from both the transmitter and the receiver.

end_b = Array3f([0.9, 0.0], [0.9, 0.0], [0.5, 0.5])
pair = scene.visible_pair(start, end, end_b)
print(pair.visible_a, pair.visible_b)       # [False, True] [True, False]

A chain of segments#

visible_chain(points, chain_length) tests polylines, such as a full multi-bounce path. Each chain has up to P points, stored flat and padded to the longest chain; chain_length gives the actual number of points in each chain. A chain of n points has n - 1 segments.

# chain 0: 2 points (one segment, through the blocker), padded to 3
# chain 1: 3 points (two clear segments)
pts = Array3f([0.0, 0.0, 0.9,  0.9, 0.9, 0.9],
              [0.0, 0.0, 0.9,  0.9, 0.9, 0.9],
              [2.0, 0.5, 2.0,  2.0, 0.5, 0.5])
ch = scene.visible_chain(pts, Int([2, 3]))
print(ch.all_visible)             # [False, True]
print(ch.first_blocked_segment)   # [0, -1]
print(ch.first_blocked_prim)      # [3, -1]: global face 3 blocked chain 0
print(ch.max_segments)            # 2

ignore_prim_per_segment works like ignore_prim_ids, with a width that is a multiple of chain_count * max_segments. In Torch, points is an (N, P, 3) tensor.

Points along an edge#

visible_edge(src, edge_pos, edge_dir, edge_t_min, edge_t_max, sample_fractions=[0, 0.25, 0.5, 0.75, 1]) tests whether a source sees any of several sample points on an edge segment edge_pos + t * edge_dir, with t taken at the given fractions of [edge_t_min, edge_t_max]. It is meant for culling diffraction edges.

src = Array3f([0.0], [0.0], [2.0])
ev = scene.visible_edge(src, Array3f([-1.0], [0.0], [0.5]), Array3f([1.0], [0.0], [0.0]),
                        Float([0.0]), Float([2.0]))
print(ev.any_visible, ev.state_count)        # [True] 1