gamnit: enable dynamic window resizing on desktop
[nit.git] / lib / gamnit / cameras.nit
1 # This file is part of NIT (http://www.nitlanguage.org).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Camera services producing Model-View-Projection matrices
16 module cameras
17
18 import geometry
19 import matrix::projection
20
21 import display
22
23 # A camera with a point of view on the world
24 abstract class Camera
25
26 # TODO make this a physical object in the world
27
28 # The host `GamnitDisplay`
29 var display: GamnitDisplay
30
31 # Position of this camera in world space
32 var position = new Point3d[Float](0.0, 0.0, 0.0)
33
34 # The Model-View-Projection matrix created by this camera
35 #
36 # This method should only be called by the display at the moment
37 # of drawing to the screen.
38 fun mvp_matrix: Matrix is abstract
39 end
40
41 # Simple camera with perspective oriented with Euler angles (`pitch, yaw, roll`)
42 class EulerCamera
43 super Camera
44
45 # Rotation around the X axis (looking down or up)
46 var pitch = 0.0 is writable
47
48 # Rotation around the Y axis (looking left or right)
49 var yaw = 0.0 is writable
50
51 # Rotation around the Z axis
52 var roll = 0.0 is writable
53
54 # Field of view in radians on the vertical axis of the screen
55 #
56 # Default at `0.8`
57 var field_of_view_y = 0.8 is writable
58
59 # Clipping wall near the camera, in world dimensions
60 #
61 # Default at `0.01`.
62 var near = 0.01 is writable
63
64 # Clipping wall the farthest of the camera, in world dimensions
65 #
66 # Default at `10000.0` but this one should be adapted to each context.
67 var far = 10000.0 is writable
68
69 # Look around sensitivity, used by `turn`
70 var sensitivity = 0.005 is writable
71
72 # Apply a mouse movement (or similar) to the camera
73 #
74 # `dx` and `dy` are relative mouse movements in pixels.
75 fun turn(dx, dy: Float)
76 do
77 # Moving on x, turn around the y axis
78 yaw -= dx*sensitivity
79 pitch -= dy*sensitivity
80
81 # Protect rotation around then x axis for not falling on your back
82 pitch = pitch.min(pi/2.0)
83 pitch = pitch.max(-pi/2.0)
84 end
85
86 # Move the camera considering the current orientation
87 fun move(dx, dy, dz: Float)
88 do
89 # +dz move forward
90 position.x -= yaw.sin*dz
91 position.z -= yaw.cos*dz
92
93 # +dx strafe to the right
94 position.x += yaw.cos*dx
95 position.z -= yaw.sin*dx
96
97 # +dz move towards the sky
98 position.y += dy
99 end
100
101 # Aim the camera at `x, y, z`
102 fun look_at(x, y, z: Float)
103 do
104 var dx = position.x
105 var dy = position.y
106 var dz = position.z
107
108 yaw = atan2(dx, dz)
109 pitch = atan2(-dy, dz)
110 end
111
112 # Rotation matrix produced by the current rotation of the camera
113 protected fun rotation_matrix: Matrix
114 do
115 var view = new Matrix.identity(4)
116
117 # Rotate the camera, first by looking left or right, then up or down
118 view.rotate(yaw, 0.0, 1.0, 0.0)
119 view.rotate(pitch, 1.0, 0.0, 0.0)
120 view.rotate(roll, 0.0, 0.0, 1.0)
121
122 return view
123 end
124
125 redef fun mvp_matrix
126 do
127 var view = new Matrix.identity(4)
128
129 # Translate the world away from the camera
130 view.translate(-position.x, -position.y, -position.z)
131
132 # Rotate the camera, first by looking left or right, then up or down
133 view = view * rotation_matrix
134
135 # Use a projection matrix with a depth
136 var projection = new Matrix.perspective(field_of_view_y,
137 display.aspect_ratio, near, far)
138
139 return view * projection
140 end
141
142 # Reset the camera position so that `height` world units are visible on the y axis at z=0
143 #
144 # By default, `height` is set to `display.height`.
145 #
146 # After the reset, the camera sits on the Z axis and rotation values are reset to 0.
147 # The X axis is horizontal on the screen and the Y axis is vertical.
148 # Higher values on the Z axis are closer to the camera.
149 fun reset_height(height: nullable Float)
150 do
151 if height == null then height = display.height.to_f
152
153 var opp = height / 2.0
154 var angle = field_of_view_y / 2.0
155 var adj = opp / angle.tan
156
157 position.x = 0.0
158 position.y = 0.0
159 position.z = adj
160
161 pitch = 0.0
162 yaw = 0.0
163 roll = 0.0
164 end
165
166 # Convert the position `x, y` on screen, to world coordinates on the plane at `target_z`
167 #
168 # `target_z` defaults to `0.0` and specifies the Z coordinates of the plane
169 # on which to project the screen position `x, y`.
170 #
171 # This method assumes that the camera is looking along the Z axis towards higher values.
172 # Using it in a different orientation can be useful, but won't result in valid
173 # world coordinates.
174 fun camera_to_world(x, y: Numeric, target_z: nullable Float): Point[Float]
175 do
176 # TODO, this method could be tweaked to support projecting the 2D point,
177 # on the near plane (x,y) onto a given distance no matter to orientation
178 # of the camera.
179
180 target_z = target_z or else 0.0
181
182 # Convert from pixel units / window resolution to
183 # units on the near clipping wall to
184 # units on the target wall at Z = 0
185 var near_height = (field_of_view_y/2.0).tan * near
186 var cross_screen_to_near = near_height / (display.height.to_f/2.0)
187 var cross_near_to_target = (position.z - target_z) / near
188 var mod = cross_screen_to_near * cross_near_to_target
189
190 var wx = position.x + (x.to_f-display.width.to_f/2.0) * mod
191 var wy = position.y - (y.to_f-display.height.to_f/2.0) * mod
192 return new Point[Float](wx, wy)
193 end
194 end
195
196 # Orthogonal camera to draw UI objects with services to work with screens of different sizes
197 #
198 # X axis: left to right of the screen, from `position.x` to `position.x + width`
199 # Y axis: top to bottom of the screen, from `position.y` to `position.y + height`
200 # Z axis: far to near the camera (usually when values are higher), from `far` to `near`
201 class UICamera
202 super Camera
203
204 # Clipping wall near the camera, defaults to 100.0
205 var near = 100.0 is writable
206
207 # Clipping wall the farthest of the camera, defaults to -100.0
208 var far: Float = -100.0 is writable
209
210 # Width in world units, calculated from `height` and the screen aspect ratio
211 fun width: Float do return height * display.aspect_ratio
212
213 # Height in world units, defaults to 1080.0
214 #
215 # Set this value using `reset_height`.
216 var height = 1080.0
217
218 # Reset the camera position so that `height` world units are visible on the Y axis
219 #
220 # This can be used to set standardized UI units independently from the screen resolution.
221 fun reset_height(height: nullable Float)
222 do
223 if height == null then height = display.height.to_f
224 self.height = height
225 end
226
227 # Convert the position `x, y` on screen, to UI coordinates
228 fun camera_to_ui(x, y: Numeric): Point[Float]
229 do
230 # FIXME this kind of method should use something like a canvas
231 # instead of being hard coded on the display.
232
233 var wx = x.to_f * width / display.width.to_f - position.x
234 var wy = y.to_f * height / display.height.to_f - position.y
235 return new Point[Float](wx, -wy)
236 end
237
238 # Center of the screen, from the point of view of the camera, at z = 0
239 var center: IPoint3d[Float] = new CameraAnchor(self, 0.5, -0.5)
240
241 # Center of the top of the screen, at z = 0
242 var top: IPoint3d[Float] = new CameraAnchor(self, 0.5, 0.0)
243
244 # Center of the bottom of the screen, at z = 0
245 var bottom: IPoint3d[Float] = new CameraAnchor(self, 0.5, -1.0)
246
247 # Center of the left border of the screen, at z = 0
248 var left: IPoint3d[Float] = new CameraAnchor(self, 0.0, -1.0)
249
250 # Center of the right border of the screen, at z = 0
251 var right: IPoint3d[Float] = new CameraAnchor(self, 1.0, -1.0)
252
253 # Top left corner of the screen, at z = 0
254 var top_left: IPoint3d[Float] = new CameraAnchor(self, 0.0, 0.0)
255
256 # Top right corner of the screen, at z = 0
257 var top_right: IPoint3d[Float] = new CameraAnchor(self, 1.0, 0.0)
258
259 # Bottom left corner of the screen, at z = 0
260 var bottom_left: IPoint3d[Float] = new CameraAnchor(self, 0.0, -1.0)
261
262 # Bottom right corner of the screen, at z = 0
263 var bottom_right: IPoint3d[Float] = new CameraAnchor(self, 1.0, -1.0)
264
265 redef fun mvp_matrix
266 do
267 var view = new Matrix.identity(4)
268
269 # Translate the world away from the camera
270 view.translate(-position.x, -position.y, -position.z)
271
272 # Use a projection matrix with a depth
273 var projection = new Matrix.orthogonal(0.0, width, -height, 0.0, near, far)
274
275 return view * projection
276 end
277 end
278
279 # Immutable relative anchors for reference points on `camera`
280 private class CameraAnchor
281 super IPoint3d[Float]
282
283 # Reference camera
284 var camera: UICamera
285
286 # Reference position, the top left of the screen
287 var ref: Point3d[Float] = camera.position is lazy
288
289 # X position as proportion of the screen width
290 var relative_x: Float
291
292 # Y position as proportion of the screen height
293 var relative_y: Float
294
295 redef fun x do return ref.x + relative_x*camera.width
296 redef fun y do return ref.y + relative_y*camera.height
297 redef fun z do return ref.z
298
299 redef fun offset(x, y, z) do return new OffsetPoint3d(self, x.to_f, y.to_f, z.to_f)
300 end
301
302 # Position relative to another point or usually a `CameraAnchor`
303 private class OffsetPoint3d
304 super Point3d[Float]
305
306 autoinit ref, offset_x, offset_y, offset_z
307
308 # Reference point to which the offsets are applied
309 var ref: IPoint3d[Float]
310
311 # Difference on the X axis
312 var offset_x: Float
313
314 # Difference on the X axis
315 var offset_y: Float
316
317 # Difference on the X axis
318 var offset_z: Float
319
320 redef fun x do return ref.x + offset_x
321 redef fun y do return ref.y + offset_y
322 redef fun z do return ref.z + offset_z
323
324 redef fun x=(value) do if value != null then offset_x += value - x
325 redef fun y=(value) do if value != null then offset_y += value - y
326 redef fun z=(value) do if value != null then offset_z += value - z
327
328 redef fun offset(x, y, z) do return new OffsetPoint3d(self, x.to_f, y.to_f, z.to_f)
329 end