Skip to content

Commit 8844cf1

Browse files
gasnicaYilingQiao
authored andcommitted
[MISC] Add raycasting against box-entities in the scene (Genesis-Embodied-AI#1346)
* Add raycasting against Aabb and Oobb. * Add helper classes: Quat, Pose, Aabb, Color * Highlight hit object * Update ViewerInteraction to run this logic every frame * Add partial information on member-field types to RigidGeom, and RigidLink
1 parent 1ae1ef8 commit 8844cf1

File tree

7 files changed

+339
-37
lines changed

7 files changed

+339
-37
lines changed

genesis/engine/entities/rigid_entity/rigid_geom.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import pickle as pkl
3+
from typing import TYPE_CHECKING
34

45
import igl
56
import numpy as np
@@ -14,6 +15,12 @@
1415
from genesis.repr_base import RBC
1516
from genesis.utils.misc import tensor_to_array
1617

18+
if TYPE_CHECKING:
19+
from genesis.engine.materials.rigid import Rigid as RigidMaterial
20+
21+
from .rigid_entity import RigidEntity
22+
from .rigid_link import RigidLink
23+
1724

1825
@ti.data_oriented
1926
class RigidGeom(RBC):
@@ -42,9 +49,9 @@ def __init__(
4249
center_init=None,
4350
data=None,
4451
):
45-
self._link = link
46-
self._entity = link.entity
47-
self._material = link.entity.material
52+
self._link: "RigidLink" = link
53+
self._entity: "RigidEntity" = link.entity
54+
self._material: "RigidMaterial" = link.entity.material
4855
self._solver = link.entity.solver
4956
self._mesh = mesh
5057

genesis/engine/entities/rigid_entity/rigid_link.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from typing import TYPE_CHECKING
2+
13
import numpy as np
4+
from numpy.typing import ArrayLike
25
import taichi as ti
36
import torch
47

@@ -9,6 +12,9 @@
912

1013
from .rigid_geom import RigidGeom, RigidVisGeom
1114

15+
if TYPE_CHECKING:
16+
from .rigid_entity import RigidEntity
17+
1218

1319
@ti.data_oriented
1420
class RigidLink(RBC):
@@ -43,8 +49,8 @@ def __init__(
4349
invweight,
4450
visualize_contact,
4551
):
46-
self._name = name
47-
self._entity = entity
52+
self._name: str = name
53+
self._entity: "RigidEntity" = entity
4854
self._solver = entity.solver
4955
self._entity_idx_in_solver = entity.idx
5056

@@ -68,16 +74,18 @@ def __init__(
6874
self._vvert_start = vvert_start
6975
self._vface_start = vface_start
7076

71-
self._pos = pos
72-
self._quat = quat
73-
self._inertial_pos = inertial_pos
74-
self._inertial_quat = inertial_quat
77+
# Link position & rotation at creation time:
78+
self._pos: ArrayLike = pos
79+
self._quat: ArrayLike = quat
80+
# Link's center-of-mass position & principal axes frame rotation at creation time:
81+
self._inertial_pos: ArrayLike = inertial_pos
82+
self._inertial_quat: ArrayLike = inertial_quat
7583
self._inertial_mass = inertial_mass
7684
self._inertial_i = inertial_i
7785

7886
self._visualize_contact = visualize_contact
7987

80-
self._geoms = gs.List()
88+
self._geoms: list[RigidGeom] = gs.List()
8189
self._vgeoms = gs.List()
8290

8391
def _build(self):
@@ -99,7 +107,7 @@ def _build(self):
99107
is_fixed = False
100108
if self._root_idx is None:
101109
self._root_idx = gs.np_int(link.idx)
102-
self.is_fixed = gs.np_int(is_fixed)
110+
self.is_fixed: np.int32 = gs.np_int(is_fixed) # note: type inconsistent with is_built, is_free, is_leaf: bool
103111

104112
# inertial_mass and inertia_i
105113
if self._inertial_mass is None:
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import numpy as np
2+
from numpy.typing import NDArray
3+
4+
from .ray import Ray, RayHit, EPSILON
5+
from .vec3 import Pose, Vec3
6+
7+
class AABB:
8+
v: NDArray[np.float32]
9+
10+
def __init__(self, v: NDArray[np.float32]):
11+
assert v.shape == (2, 3,), f"Aabb must be initialized with a (2,3)-element array, got {v.shape}"
12+
assert v.dtype == np.float32, f"Aabb must be initialized with a float32 array, got {v.dtype}"
13+
self.v = v
14+
15+
@property
16+
def min(self) -> Vec3:
17+
return Vec3(self.v[0])
18+
19+
@property
20+
def max(self) -> Vec3:
21+
return Vec3(self.v[1])
22+
23+
def expand(self, padding: float) -> None:
24+
self.v[0] -= padding
25+
self.v[1] += padding
26+
27+
def raycast(self, ray: Ray) -> RayHit:
28+
"""
29+
Standard AABB slab implementation. Early-exits and returns no-hit for rays withing the XY, XZ, or YZ planes.
30+
Ignores hits for rays originating inside the AABB.
31+
"""
32+
if (np.abs(ray.direction.v) < EPSILON).any():
33+
# unhandled ray case: early-exit
34+
return RayHit.no_hit()
35+
36+
tmin = (self.v[0] - ray.origin.v) / ray.direction.v
37+
tmax = (self.v[1] - ray.origin.v) / ray.direction.v
38+
mmin = np.minimum(tmin, tmax)
39+
mmax = np.maximum(tmin, tmax)
40+
min_idx = np.argmax(mmin)
41+
max_idx = np.argmin(mmax)
42+
tnear = mmin[min_idx]
43+
tfar = mmax[max_idx]
44+
45+
# Drop hits coming from inside
46+
if tfar < tnear or tnear < 0: # tfar < 0
47+
return RayHit.no_hit()
48+
49+
# Calculate enter point and normal
50+
enter = tnear # if 0 <= tnear else tfar
51+
normal = Vec3.zero()
52+
normal.v[min_idx] = -np.sign(ray.direction.v[min_idx])
53+
54+
hit_pos = ray.origin + ray.direction * enter
55+
return RayHit(enter, hit_pos, normal)
56+
57+
def raycast_oobb(self, pose: Pose, ray: Ray) -> RayHit:
58+
inv_pose = pose.get_inverse()
59+
origin2 = inv_pose.transform_point(ray.origin)
60+
direction2 = inv_pose.transform_direction(ray.direction)
61+
ray2 = Ray(origin2, direction2)
62+
ray_hit = self.raycast(ray2)
63+
if ray_hit.is_hit:
64+
ray_hit.position = pose.transform_point(ray_hit.position)
65+
ray_hit.normal = pose.transform_direction(ray_hit.normal)
66+
return ray_hit
67+
68+
def __repr__(self) -> str:
69+
return f"Min({self.min.x}, {self.min.y}, {self.min.z}) Max({self.max.x}, {self.max.y}, {self.max.z})"
70+
71+
@classmethod
72+
def from_min_max(cls, min: Vec3, max: Vec3) -> 'AABB':
73+
bounds = np.stack((min.v, max.v))
74+
return cls(bounds)
75+
76+
@classmethod
77+
def from_center_and_size(cls, center: Vec3, size: Vec3) -> 'AABB':
78+
min = center - 0.5 * size
79+
max = center + 0.5 * size
80+
bounds = np.stack((min.v, max.v))
81+
return cls(bounds)

genesis/ext/pyrender/interaction/ray.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55

66
EPSILON = 1e-6
7+
EPSILON2 = EPSILON * EPSILON
78

89

910
class Ray:
@@ -20,11 +21,17 @@ def __repr__(self) -> str:
2021

2122
@dataclass
2223
class RayHit:
23-
is_hit: bool
2424
distance: float
25-
normal: Vec3
2625
position: Vec3
27-
object_idx: int
26+
normal: Vec3
27+
28+
@property
29+
def is_hit(self) -> bool:
30+
return 0 <= self.distance
31+
32+
@classmethod
33+
def no_hit(cls) -> 'RayHit':
34+
return RayHit(-1.0, Vec3.zero(), Vec3.zero())
2835

2936

3037
class Plane:
@@ -40,15 +47,7 @@ def raycast(self, ray: Ray) -> RayHit:
4047
dist = ray.origin.dot(self.normal) + self.distance
4148

4249
if -EPSILON < dot or dist < EPSILON:
43-
return RayHit(is_hit=False, distance=0, normal=Vec3.zero(), position=Vec3.zero(), object_idx=-1)
44-
45-
dist_along_ray = dist / -dot
46-
47-
return RayHit(
48-
is_hit=True,
49-
distance=dist_along_ray,
50-
normal=self.normal,
51-
position=ray.origin + ray.direction * dist_along_ray,
52-
object_idx=0
53-
)
54-
50+
return RayHit.no_hit()
51+
else:
52+
dist_along_ray = dist / -dot
53+
return RayHit(dist_along_ray, ray.origin + ray.direction * dist_along_ray, self.normal)

genesis/ext/pyrender/interaction/vec3.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
from dataclasses import dataclass
2+
from typing import Union
3+
14
import numpy as np
25
from numpy.typing import NDArray
36

7+
# If not needing runtime checks, we can just use annotated types:
8+
# Vec3 = Annotated[npt.NDArray[np.float32], (3,)]
9+
# Aabb = Annotated[npt.NDArray[np.float32], (2, 3)]
10+
411

512
class Vec3:
613
"""
@@ -43,6 +50,17 @@ def copy(self) -> 'Vec3':
4350
def __repr__(self) -> str:
4451
return f"Vec3({self.v[0]}, {self.v[1]}, {self.v[2]})"
4552

53+
@property
54+
def x(self) -> float:
55+
return self.v[0]
56+
57+
@property
58+
def y(self) -> float:
59+
return self.v[1]
60+
61+
@property
62+
def z(self) -> float:
63+
return self.v[2]
4664

4765
@classmethod
4866
def from_xyz(cls, x: float, y: float, z: float) -> 'Vec3':
@@ -66,6 +84,11 @@ def from_float64(cls, v: NDArray[np.float64]) -> 'Vec3':
6684
assert v.dtype == np.float64, f"from_float64 must be initialized with a float64 array, got {v.dtype}"
6785
return cls.from_xyz(*v)
6886

87+
@classmethod
88+
def from_any_array(cls, v: np.ndarray) -> 'Vec3':
89+
assert v.shape == (3,), f"Vec3 must be initialized with a 3-element array, got {v.shape}"
90+
return cls.from_xyz(*v)
91+
6992

7093
@classmethod
7194
def zero(cls):
@@ -74,3 +97,126 @@ def zero(cls):
7497
@classmethod
7598
def one(cls):
7699
return cls(np.array([1, 1, 1], dtype=np.float32))
100+
101+
102+
class Quat:
103+
v: NDArray[np.float32]
104+
def __init__(self, v: NDArray[np.float32]):
105+
assert v.shape == (4,), f"Quat must be initialized with a 4-element array, got {v.shape}"
106+
assert v.dtype == np.float32, f"Quat must be initialized with a float32 array, got {v.dtype}"
107+
self.v = v
108+
109+
def get_inverse(self) -> 'Quat':
110+
quat_inv = self.v.copy()
111+
quat_inv[1:] *= -1
112+
return Quat(quat_inv)
113+
114+
def __mul__(self, other: Union['Quat', Vec3]) -> Union['Quat', Vec3]:
115+
if isinstance(other, Quat):
116+
# Quaternion * Quaternion
117+
w1, x1, y1, z1 = self.w, self.x, self.y, self.z
118+
w2, x2, y2, z2 = other.w, other.x, other.y, other.z
119+
return Quat.from_wxyz(
120+
w1*w2 - x1*x2 - y1*y2 - z1*z2,
121+
w1*x2 + x1*w2 + y1*z2 - z1*y2,
122+
w1*y2 - x1*z2 + y1*w2 + z1*x2,
123+
w1*z2 + x1*y2 - y1*x2 + z1*w2
124+
)
125+
elif isinstance(other, Vec3): # (other, np.ndarray) and other.shape == (3,):
126+
# Quaternion * Vector3 -> rotate vector
127+
v_quat = Quat.from_wxyz(0, *other.v)
128+
result = self * v_quat * self.get_inverse()
129+
return Vec3(result.v[1:])
130+
else:
131+
return NotImplemented
132+
133+
def copy(self) -> 'Quat':
134+
return Quat(self.v.copy())
135+
136+
def __repr__(self) -> str:
137+
return f"Quat({self.v[0]}, {self.v[1]}, {self.v[2]}, {self.v[3]})"
138+
139+
@property
140+
def w(self) -> float:
141+
return self.v[0]
142+
143+
@property
144+
def x(self) -> float:
145+
return self.v[1]
146+
147+
@property
148+
def y(self) -> float:
149+
return self.v[2]
150+
151+
@property
152+
def z(self) -> float:
153+
return self.v[3]
154+
155+
156+
@classmethod
157+
def from_wxyz(cls, w: float, x: float, y: float, z: float) -> 'Quat':
158+
return cls(np.array([w, x, y, z], dtype=np.float32))
159+
160+
@classmethod
161+
def from_any_array(cls, v: np.ndarray) -> 'Quat':
162+
assert v.shape == (4,), f"Quat must be initialized with a 4-element array, got {v.shape}"
163+
return cls.from_wxyz(*v)
164+
165+
166+
@dataclass
167+
class Pose:
168+
pos: Vec3
169+
rot: Quat
170+
171+
# todo: consider using a single np.array with views
172+
173+
def transform_point(self, point: Vec3) -> Vec3:
174+
return self.pos + self.rot * point
175+
176+
def inverse_transform_point(self, point: Vec3) -> Vec3:
177+
return self.rot.get_inverse() * (point - self.pos)
178+
179+
def transform_direction(self, direction: Vec3) -> Vec3:
180+
return self.rot * direction
181+
182+
def inverse_transform_direction(self, direction: Vec3) -> Vec3:
183+
return self.rot.get_inverse() * direction
184+
185+
def get_inverse(self) -> 'Pose':
186+
inv_rot = self.rot.get_inverse()
187+
# inv_pos = -1.0 * (inv_rot * self.pos)
188+
# faster -- avoid repeated quat inversion:
189+
pos_quat = Quat.from_wxyz(0, *self.pos.v)
190+
inv_pos = inv_rot * pos_quat * self.rot
191+
inv_pos = Vec3(-inv_pos.v[1:])
192+
return Pose(inv_pos, inv_rot)
193+
194+
195+
@dataclass
196+
class Color:
197+
r: float
198+
g: float
199+
b: float
200+
a: float
201+
202+
def tuple(self) -> tuple[float, float, float, float]:
203+
return (self.r, self.g, self.b, self.a)
204+
205+
def with_alpha(self, alpha: float) -> 'Color':
206+
return Color(self.r, self.g, self.b, alpha)
207+
208+
@classmethod
209+
def red(cls) -> 'Color':
210+
return cls(1.0, 0.0, 0.0, 1.0)
211+
212+
@classmethod
213+
def green(cls) -> 'Color':
214+
return cls(0.0, 1.0, 0.0, 1.0)
215+
216+
@classmethod
217+
def blue(cls) -> 'Color':
218+
return cls(0.0, 0.0, 1.0, 1.0)
219+
220+
@classmethod
221+
def yellow(cls) -> 'Color':
222+
return cls(1.0, 1.0, 0.0, 1.0)

0 commit comments

Comments
 (0)