Skip to content

Commit 2aaed07

Browse files
authored
Merge pull request #54 from makepath/feature/object-picking
Add object picking via OptiX single-ray raycasts
2 parents 24f5e6f + d1ca7f5 commit 2aaed07

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

rtxpy/engine.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,17 @@ def fn(v):
631631
_add_overlay(v, 'aspect', data.data)
632632
self._submit(fn)
633633

634+
# ------------------------------------------------------------------
635+
# Picking
636+
# ------------------------------------------------------------------
637+
638+
def pick(self, screen_x, screen_y):
639+
"""Pick geometry at screen coordinates. Returns hit info dict."""
640+
def fn(v):
641+
origin, direction = v._screen_to_ray(screen_x, screen_y)
642+
return v.rtx.pick(origin, direction)
643+
return self._submit(fn)
644+
634645
# ------------------------------------------------------------------
635646
# Layer management
636647
# ------------------------------------------------------------------
@@ -1487,6 +1498,32 @@ def _get_right(self):
14871498
right = np.cross(world_up, front)
14881499
return right / (np.linalg.norm(right) + 1e-8)
14891500

1501+
def _screen_to_ray(self, screen_x, screen_y):
1502+
"""Convert screen pixel coordinates to a world-space ray.
1503+
1504+
Returns (origin, direction) as numpy float32 arrays of shape (3,).
1505+
"""
1506+
front = self._get_front()
1507+
world_up = np.array([0, 0, 1], dtype=np.float32)
1508+
right = np.cross(world_up, front)
1509+
rn = np.linalg.norm(right)
1510+
if rn > 1e-8:
1511+
right /= rn
1512+
else:
1513+
right = np.array([1, 0, 0], dtype=np.float32)
1514+
cam_up = np.cross(front, right)
1515+
1516+
fov_scale = np.tan(np.radians(self.fov) / 2.0)
1517+
aspect = self.render_width / max(1, self.render_height)
1518+
1519+
# Window coords → NDC (-1..1)
1520+
nx = 2.0 * screen_x / max(1, self.width) - 1.0
1521+
ny = 1.0 - 2.0 * screen_y / max(1, self.height)
1522+
1523+
direction = front + nx * fov_scale * aspect * right + ny * fov_scale * cam_up
1524+
direction = direction / (np.linalg.norm(direction) + 1e-30)
1525+
return self.position.copy(), direction.astype(np.float32)
1526+
14901527
def _get_look_at(self):
14911528
"""Get the current look-at point."""
14921529
return self.position + self._get_front() * 1000.0
@@ -5029,6 +5066,18 @@ def _handle_mouse_press(self, button, xpos, ypos):
50295066
self._mouse_last_x = xpos
50305067
self._mouse_last_y = ypos
50315068

5069+
elif button == 1: # right click — object picking
5070+
origin, direction = self._screen_to_ray(xpos, ypos)
5071+
result = self.rtx.pick(origin, direction)
5072+
if result['hit']:
5073+
gid = result['geometry_id'] or '?'
5074+
px, py, pz = result['position']
5075+
print(f"Pick: geometry='{gid}' pos=({px:.1f}, {py:.1f}, {pz:.1f}) "
5076+
f"t={result['t']:.1f} prim={result['primitive_id']} "
5077+
f"instance={result['instance_id']}")
5078+
else:
5079+
print("Pick: no geometry hit")
5080+
50325081
def _handle_mouse_release(self, button):
50335082
"""End drag on button release."""
50345083
self._mouse_dragging = False
@@ -5327,6 +5376,7 @@ def _render_help_text(self):
53275376
("GEOMETRY", [
53285377
("N", "Cycle geometry layer"),
53295378
("P", "Prev geometry in group"),
5379+
("Right-Click", "Pick geometry"),
53305380
]),
53315381
("OBSERVERS", [
53325382
("1-8", "Select / create observer"),

rtxpy/rtx.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,6 +1885,60 @@ def trace(self, rays, hits, numRays: int, primitive_ids=None, instance_ids=None,
18851885
return _trace_rays(self._geom_state, rays, hits, numRays, primitive_ids, instance_ids,
18861886
ray_flags=ray_flags)
18871887

1888+
def pick(self, origin, direction) -> dict:
1889+
"""Fire a single ray and return hit info.
1890+
1891+
Parameters
1892+
----------
1893+
origin : array-like
1894+
Ray origin (x, y, z).
1895+
direction : array-like
1896+
Ray direction (dx, dy, dz), will be normalized.
1897+
1898+
Returns
1899+
-------
1900+
dict
1901+
Keys: 'hit' (bool), 'geometry_id' (str or None),
1902+
't' (float), 'normal' (tuple), 'position' (tuple),
1903+
'primitive_id' (int), 'instance_id' (int).
1904+
"""
1905+
o = np.asarray(origin, dtype=np.float32)
1906+
d = np.asarray(direction, dtype=np.float32)
1907+
d = d / (np.linalg.norm(d) + 1e-30)
1908+
1909+
rays = cupy.array([o[0], o[1], o[2], 0.001,
1910+
d[0], d[1], d[2], 1e10], dtype=cupy.float32)
1911+
hits = cupy.zeros(4, dtype=cupy.float32)
1912+
prim_ids = cupy.full(1, -1, dtype=cupy.int32)
1913+
inst_ids = cupy.full(1, -1, dtype=cupy.int32)
1914+
1915+
self.trace(rays, hits, 1, primitive_ids=prim_ids, instance_ids=inst_ids)
1916+
1917+
t = float(hits[0])
1918+
if t > 0:
1919+
iid = int(inst_ids[0])
1920+
geom_list = self.list_geometries()
1921+
geom_id = geom_list[iid] if 0 <= iid < len(geom_list) else None
1922+
pos = o + d * t
1923+
return {
1924+
'hit': True,
1925+
'geometry_id': geom_id,
1926+
't': t,
1927+
'normal': (float(hits[1]), float(hits[2]), float(hits[3])),
1928+
'position': (float(pos[0]), float(pos[1]), float(pos[2])),
1929+
'primitive_id': int(prim_ids[0]),
1930+
'instance_id': iid,
1931+
}
1932+
return {
1933+
'hit': False,
1934+
'geometry_id': None,
1935+
't': -1.0,
1936+
'normal': (0.0, 0.0, 0.0),
1937+
'position': (0.0, 0.0, 0.0),
1938+
'primitive_id': -1,
1939+
'instance_id': -1,
1940+
}
1941+
18881942
# -------------------------------------------------------------------------
18891943
# Multi-GAS API
18901944
# -------------------------------------------------------------------------

0 commit comments

Comments
 (0)