10
11
12
13
14
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
97
98
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
160
161
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 | class FirstPersonCamera:
"""A class representing a first-person camera.
This class provides functionality for a first-person camera, including movement,
rotation, and projection matrix calculation.
Attributes:
eye (Vec3): The position of the camera.
look (Vec3): The point the camera is looking at.
world_up (Vec3): The world's up vector.
front (Vec3): The front direction vector of the camera.
up (Vec3): The up direction vector of the camera.
right (Vec3): The right direction vector of the camera.
yaw (float): The yaw angle of the camera.
pitch (float): The pitch angle of the camera.
speed (float): The movement speed of the camera.
sensitivity (float): The mouse sensitivity.
zoom (float): The zoom level of the camera.
near (float): The near clipping plane.
far (float): The far clipping plane.
aspect (float): The aspect ratio.
fov (float): The field of view.
persp_mode (PerspMode): The target graphics API clip-space convention.
projection (Mat4): The projection matrix.
view (Mat4): The view matrix.
"""
def __init__(
self,
eye: Vec3,
look: Vec3,
up: Vec3,
fov: float,
persp_mode: PerspMode = PerspMode.OpenGL,
) -> None:
"""Initialize the FirstPersonCamera.
Args:
eye (Vec3): The position of the camera.
look (Vec3): The point the camera is looking at.
up (Vec3): The world's up vector.
fov (float): The field of view.
persp_mode (PerspMode): The target graphics API clip-space convention.
"""
self.eye: Vec3 = eye
self.look: Vec3 = look
self.world_up: Vec3 = up
self.front: Vec3 = Vec3()
self.up: Vec3 = Vec3()
self.right: Vec3 = Vec3()
self.yaw: float = -90.0
self.pitch: float = 0.0
self.speed: float = 2.5
self.sensitivity: float = 0.1
self.zoom: float = 45.0
self.near: float = 0.1
self.far: float = 100.0
self.aspect: float = 1.2
self.fov: float = fov
self.persp_mode: PerspMode = persp_mode
self._update_camera_vectors()
self._projection: Mat4 = self.set_projection(
self.fov, self.aspect, self.near, self.far, persp_mode
)
self._view: Mat4 = look_at(self.eye, self.eye + self.front, self.up)
def __str__(self) -> str:
"""Pretty representation showing eye, look, up, and fov."""
return f"Camera {self.eye} {self.look} {self.world_up} {self.fov}"
def __repr__(self) -> str:
"""Representation showing eye, look, up, and fov."""
return f"Camera {self.eye} {self.look} {self.world_up} {self.fov}"
@property
def projection(self) -> Mat4:
"""The camera's projection matrix."""
return self._projection
@property
def view(self) -> Mat4:
"""The camera's view matrix."""
return self._view
def process_mouse_movement(
self, diffx: float, diffy: float, _constrain_pitch: bool = True
) -> None:
"""Process mouse movement to update the camera's direction vectors.
Args:
diffx (float): The difference in the x-coordinate of the mouse movement.
diffy (float): The difference in the y-coordinate of the mouse movement.
_constrain_pitch (bool, optional): Whether to constrain the pitch angle. Defaults to True.
"""
diffx *= self.sensitivity
diffy *= self.sensitivity
self.yaw += diffx
self.pitch += diffy
# Make sure that when pitch is out of bounds, screen doesn't get flipped
if _constrain_pitch:
if self.pitch > 89.0:
self.pitch = 89.0
if self.pitch < -89.0:
self.pitch = -89.0
self._update_camera_vectors()
def _update_camera_vectors(self) -> None:
"""Update the camera's direction vectors based on the current yaw and pitch angles."""
pitch = math.radians(self.pitch)
yaw = math.radians(self.yaw)
self.front.x = math.cos(yaw) * math.cos(pitch)
self.front.y = math.sin(pitch)
self.front.z = math.sin(yaw) * math.cos(pitch)
self.front = self.front.normalized()
# Also re-calculate the Right and Up vector
self.right = self.front.cross(self.world_up)
self.up = self.right.cross(self.front)
# normalize as fast movement can cause issues
self.right = self.right.normalized()
self.front = self.front.normalized()
self._view = look_at(self.eye, self.eye + self.front, self.up)
def set_projection(
self,
fov: float,
aspect: float,
near: float,
far: float,
persp_mode: PerspMode = PerspMode.OpenGL,
) -> Mat4:
"""Set the projection matrix for the camera.
Args:
fov (float): The field of view.
aspect (float): The aspect ratio.
near (float): The near clipping plane.
far (float): The far clipping plane.
persp_mode (PerspMode): The target graphics API clip-space convention.
Returns:
Mat4: The projection matrix.
"""
return perspective(fov, aspect, near, far, persp_mode)
def move(self, x: float, y: float, delta: float) -> None:
"""Move the camera based on input directions.
Args:
x (float): The movement in the x-direction.
y (float): The movement in the y-direction.
delta (float): The amount to move the camera.
"""
velocity = self.speed * delta
self.eye += self.front * velocity * x
self.eye += self.right * velocity * y
self._update_camera_vectors()
def get_vp(self) -> Mat4:
"""Get the view-projection matrix.
Returns:
Mat4: The view-projection matrix.
"""
return self._projection @ self._view
def process_mouse_scroll(self, y_offset: float) -> None:
"""Process mouse scroll events.
Args:
y_offset (float): The scroll offset.
"""
if self.zoom >= 1.0 and self.zoom <= 45.0:
self.zoom -= y_offset
if self.zoom <= 1.0:
self.zoom = 1.0
if self.zoom >= 45.0:
self.zoom = 45.0
self._projection = perspective(
self.zoom, self.aspect, self.near, self.far, self.persp_mode
)
|