Skip to content

Commit db81a21

Browse files
authored
[MISC] Add stub code for mouse interaction (#1321)
1 parent 60c1935 commit db81a21

File tree

8 files changed

+336
-13
lines changed

8 files changed

+336
-13
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from dataclasses import dataclass
2+
3+
from .vec3 import Vec3
4+
5+
6+
EPSILON = 1e-6
7+
8+
9+
class Ray:
10+
origin: Vec3
11+
direction: Vec3
12+
13+
def __init__(self, origin: Vec3, direction: Vec3):
14+
self.origin = origin
15+
self.direction = direction.normalized()
16+
17+
def __repr__(self) -> str:
18+
return f"Ray(origin={self.origin}, direction={self.direction})"
19+
20+
21+
@dataclass
22+
class RayHit:
23+
is_hit: bool
24+
distance: float
25+
normal: Vec3
26+
position: Vec3
27+
object_idx: int
28+
29+
30+
class Plane:
31+
normal: Vec3
32+
distance: float # distance from plane to origin along normal
33+
34+
def __init__(self, normal: Vec3, point: Vec3):
35+
self.normal = normal
36+
self.distance = -normal.dot(point)
37+
38+
def raycast(self, ray: Ray) -> RayHit:
39+
dot = ray.direction.dot(self.normal)
40+
dist = ray.origin.dot(self.normal) + self.distance
41+
42+
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+
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import numpy as np
2+
from numpy.typing import NDArray
3+
4+
5+
class Vec3:
6+
"""
7+
Use this wrapper around np.array if you want to ensure adherence to float32 arithmethic
8+
with runtime checks, and avoid hidden and costly conversions between float32 and float64.
9+
10+
This also makes vector dimensionality explicit for linting and static analysis.
11+
"""
12+
v: NDArray[np.float32]
13+
14+
def __init__(self, v: NDArray[np.float32]):
15+
assert v.shape == (3,), f"Vec3 must be initialized with a 3-element array, got {v.shape}"
16+
assert v.dtype == np.float32, f"Vec3 must be initialized with a float32 array, got {v.dtype}"
17+
self.v = v
18+
19+
def __add__(self, other: 'Vec3') -> 'Vec3':
20+
return Vec3(self.v + other.v)
21+
22+
def __sub__(self, other: 'Vec3') -> 'Vec3':
23+
return Vec3(self.v - other.v)
24+
25+
def __mul__(self, other: float) -> 'Vec3':
26+
return Vec3(self.v * np.float32(other))
27+
28+
def __rmul__(self, other: float) -> 'Vec3':
29+
return Vec3(self.v * np.float32(other))
30+
31+
def dot(self, other: 'Vec3') -> float:
32+
return np.dot(self.v, other.v).item()
33+
34+
def cross(self, other: 'Vec3') -> 'Vec3':
35+
return Vec3(np.cross(self.v, other.v))
36+
37+
def normalized(self) -> 'Vec3':
38+
return Vec3(self.v / (np.linalg.norm(self.v) + 1e-24))
39+
40+
def copy(self) -> 'Vec3':
41+
return Vec3(self.v.copy())
42+
43+
def __repr__(self) -> str:
44+
return f"Vec3({self.v[0]}, {self.v[1]}, {self.v[2]})"
45+
46+
47+
@classmethod
48+
def from_xyz(cls, x: float, y: float, z: float) -> 'Vec3':
49+
return cls(np.array([x, y, z], dtype=np.float32))
50+
51+
@classmethod
52+
def from_int32(cls, v: NDArray[np.int32]) -> 'Vec3':
53+
assert v.shape == (3,), f"Vec3 must be initialized with a 3-element array, got {v.shape}"
54+
assert v.dtype == np.int32, f"from_int32 must be initialized with a int32 array, got {v.dtype}"
55+
return cls.from_xyz(*v)
56+
57+
@classmethod
58+
def from_int64(cls, v: NDArray[np.int64]) -> 'Vec3':
59+
assert v.shape == (3,), f"Vec3 must be initialized with a 3-element array, got {v.shape}"
60+
assert v.dtype == np.int64, f"from_int64 must be initialized with a int64 array, got {v.dtype}"
61+
return cls.from_xyz(*v)
62+
63+
@classmethod
64+
def from_float64(cls, v: NDArray[np.float64]) -> 'Vec3':
65+
assert v.shape == (3,), f"Vec3 must be initialized with a 3-element array, got {v.shape}"
66+
assert v.dtype == np.float64, f"from_float64 must be initialized with a float64 array, got {v.dtype}"
67+
return cls.from_xyz(*v)
68+
69+
70+
@classmethod
71+
def zero(cls):
72+
return cls(np.array([0, 0, 0], dtype=np.float32))
73+
74+
@classmethod
75+
def one(cls):
76+
return cls(np.array([1, 1, 1], dtype=np.float32))
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from typing import TYPE_CHECKING
2+
from typing_extensions import override
3+
4+
import numpy as np
5+
6+
from pyglet.event import EVENT_HANDLE_STATE
7+
8+
from .ray import Plane, Ray, RayHit
9+
from .vec3 import Vec3
10+
from .viewer_interaction_base import ViewerInteractionBase
11+
12+
if TYPE_CHECKING:
13+
from genesis.engine.scene import Scene
14+
from genesis.ext.pyrender.node import Node
15+
16+
17+
class ViewerInteraction(ViewerInteractionBase):
18+
"""Functionalities to be implemented:
19+
- mouse picking
20+
- mouse dragging
21+
"""
22+
23+
camera: 'Node'
24+
scene: 'Scene'
25+
viewport_size: tuple[int, int]
26+
camera_yfov: float
27+
28+
tan_half_fov: float
29+
prev_mouse_pos: tuple[int, int]
30+
31+
def __init__(self,
32+
camera: 'Node',
33+
scene: 'Scene',
34+
viewport_size: tuple[int, int],
35+
camera_yfov: float,
36+
log_events: bool = False,
37+
camera_fov: float = 60.0,
38+
):
39+
super().__init__(log_events)
40+
self.camera = camera
41+
self.scene = scene
42+
self.viewport_size = viewport_size
43+
self.camera_yfov = camera_yfov
44+
45+
self.tan_half_fov = np.tan(0.5 * self.camera_yfov)
46+
self.prev_mouse_pos = tuple(np.array(viewport_size) / 2)
47+
48+
@override
49+
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> EVENT_HANDLE_STATE:
50+
super().on_mouse_motion(x, y, dx, dy)
51+
self.prev_mouse_pos = (x, y)
52+
53+
@override
54+
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> EVENT_HANDLE_STATE:
55+
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
56+
self.prev_mouse_pos = (x, y)
57+
58+
@override
59+
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> EVENT_HANDLE_STATE:
60+
super().on_mouse_press(x, y, button, modifiers)
61+
mouse_ray = self.screen_position_to_ray(x, y)
62+
# print(f"mouse_ray: {mouse_ray}")
63+
64+
@override
65+
def on_draw(self) -> None:
66+
super().on_draw()
67+
if self.scene._visualizer is not None and self.scene._visualizer.viewer_lock is not None:
68+
self.scene.clear_debug_objects()
69+
70+
ray_hit = self._raycast_against_ground(self.screen_position_to_ray(*self.prev_mouse_pos))
71+
if ray_hit.is_hit:
72+
self.scene.draw_debug_sphere(ray_hit.position.v, 0.01, (0, 1, 0, 1))
73+
self._draw_arrow(ray_hit.position, ray_hit.normal, (0, 1, 0, 1))
74+
75+
def screen_position_to_ray(self, x: float, y: float) -> Ray:
76+
# convert screen position to ray
77+
if True:
78+
x = x - 0.5 * self.viewport_size[0]
79+
y = y - 0.5 * self.viewport_size[1]
80+
x = 2.0 * x / self.viewport_size[1] * self.tan_half_fov
81+
y = 2.0 * y / self.viewport_size[1] * self.tan_half_fov
82+
else:
83+
# alternative way
84+
projection_matrix = self.camera.camera.get_projection_matrix(*self.viewport_size)
85+
x = x - 0.5 * self.viewport_size[0]
86+
y = y - 0.5 * self.viewport_size[1]
87+
x = 2.0 * x / self.viewport_size[0] / projection_matrix[0, 0]
88+
y = 2.0 * y / self.viewport_size[1] / projection_matrix[1, 1]
89+
90+
# Note: ignoring pixel aspect ratio
91+
92+
mtx = self.camera.matrix
93+
position = Vec3.from_float64(mtx[:3, 3])
94+
forward = Vec3.from_float64(-mtx[:3, 2])
95+
right = Vec3.from_float64(mtx[:3, 0])
96+
up = Vec3.from_float64(mtx[:3, 1])
97+
98+
direction = forward + right * x + up * y
99+
return Ray(position, direction)
100+
101+
def get_camera_ray(self) -> Ray:
102+
mtx = self.camera.matrix
103+
position = Vec3.from_float64(mtx[:3, 3])
104+
forward = Vec3.from_float64(-mtx[:3, 2])
105+
return Ray(position, forward)
106+
107+
def _raycast_against_ground(self, ray: Ray) -> RayHit:
108+
ground_plane = Plane(Vec3.from_xyz(0, 0, 1), Vec3.zero())
109+
return ground_plane.raycast(ray)
110+
111+
def _draw_arrow(
112+
self, pos: Vec3, dir: Vec3, color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
113+
) -> None:
114+
self.scene.draw_debug_arrow(pos.v, dir.v, color=color) # Only draws arrowhead -- bug?
115+
self.scene.draw_debug_line(pos.v, pos.v + dir.v, color=color)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from pyglet.event import EVENT_HANDLE_STATE
2+
3+
import genesis as gs
4+
5+
# Note: Viewer window is based on pyglet.window.Window, mouse events are defined in pyglet.window.BaseWindow
6+
7+
class ViewerInteractionBase():
8+
"""Base class for handling pyglet.window.Window events.
9+
"""
10+
11+
log_events: bool
12+
13+
def __init__(self, log_events: bool = False):
14+
self.log_events = log_events
15+
16+
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> EVENT_HANDLE_STATE:
17+
if self.log_events:
18+
gs.logger.info(f"Mouse moved to {x}, {y}")
19+
20+
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> EVENT_HANDLE_STATE:
21+
if self.log_events:
22+
gs.logger.info(f"Mouse dragged to {x}, {y}")
23+
24+
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> EVENT_HANDLE_STATE:
25+
if self.log_events:
26+
gs.logger.info(f"Mouse buttons {button} pressed at {x}, {y}")
27+
28+
def on_mouse_release(self, x: int, y: int, button: int, modifiers: int) -> EVENT_HANDLE_STATE:
29+
if self.log_events:
30+
gs.logger.info(f"Mouse buttons {button} released at {x}, {y}")
31+
32+
def on_key_press(self, symbol: int, modifiers: int) -> EVENT_HANDLE_STATE:
33+
if self.log_events:
34+
gs.logger.info(f"Key pressed: {chr(symbol)}")
35+
36+
def on_key_release(self, symbol: int, modifiers: int) -> EVENT_HANDLE_STATE:
37+
if self.log_events:
38+
gs.logger.info(f"Key released: {chr(symbol)}")
39+
40+
def on_draw(self) -> None:
41+
pass

0 commit comments

Comments
 (0)