7183e6bd3f97beb47dafba48263d0b6c4c0eaddc
[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, defaults to the width in pixels of the screen
211 var width: Float = display.width.to_f is lazy
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
225 self.height = height
226 self.width = height * display.aspect_ratio
227 end
228
229 # Convert the position `x, y` on screen, to UI coordinates
230 fun camera_to_ui(x, y: Numeric): Point[Float]
231 do
232 # FIXME this kind of method should use something like a canvas
233 # instead of being hard coded on the display.
234
235 var wx = x.to_f * width / display.width.to_f - position.x
236 var wy = y.to_f * height / display.height.to_f - position.y
237 return new Point[Float](wx, -wy)
238 end
239
240 # Center of the screen, from the point of view of the camera, at z = 0
241 var center: IPoint3d[Float] = new CameraAnchor(self, 0.5, -0.5)
242
243 # Center of the top of the screen, at z = 0
244 var top: IPoint3d[Float] = new CameraAnchor(self, 0.5, 0.0)
245
246 # Center of the bottom of the screen, at z = 0
247 var bottom: IPoint3d[Float] = new CameraAnchor(self, 0.5, -1.0)
248
249 # Center of the left border of the screen, at z = 0
250 var left: IPoint3d[Float] = new CameraAnchor(self, 0.0, -1.0)
251
252 # Center of the right border of the screen, at z = 0
253 var right: IPoint3d[Float] = new CameraAnchor(self, 1.0, -1.0)
254
255 # Top left corner of the screen, at z = 0
256 var top_left: IPoint3d[Float] = new CameraAnchor(self, 0.0, 0.0)
257
258 # Top right corner of the screen, at z = 0
259 var top_right: IPoint3d[Float] = new CameraAnchor(self, 1.0, 0.0)
260
261 # Bottom left corner of the screen, at z = 0
262 var bottom_left: IPoint3d[Float] = new CameraAnchor(self, 0.0, -1.0)
263
264 # Bottom right corner of the screen, at z = 0
265 var bottom_right: IPoint3d[Float] = new CameraAnchor(self, 1.0, -1.0)
266
267 redef fun mvp_matrix
268 do
269 var view = new Matrix.identity(4)
270
271 # Translate the world away from the camera
272 view.translate(-position.x, -position.y, -position.z)
273
274 # Use a projection matrix with a depth
275 var projection = new Matrix.orthogonal(0.0, width, -height, 0.0, near, far)
276
277 return view * projection
278 end
279 end
280
281 # Immutable relative anchors for reference points on `camera`
282 private class CameraAnchor
283 super IPoint3d[Float]
284
285 # Reference camera
286 var camera: UICamera
287
288 # Reference position, the top left of the screen
289 var ref: Point3d[Float] = camera.position is lazy
290
291 # X position as proportion of the screen width
292 var relative_x: Float
293
294 # Y position as proportion of the screen height
295 var relative_y: Float
296
297 redef fun x do return ref.x + relative_x*camera.width
298 redef fun y do return ref.y + relative_y*camera.height
299 redef fun z do return ref.z
300
301 redef fun offset(x, y, z) do return new OffsetPoint3d(self, x.to_f, y.to_f, z.to_f)
302 end
303
304 # Position relative to another point or usually a `CameraAnchor`
305 private class OffsetPoint3d
306 super Point3d[Float]
307
308 autoinit ref, offset_x, offset_y, offset_z
309
310 # Reference point to which the offsets are applied
311 var ref: IPoint3d[Float]
312
313 # Difference on the X axis
314 var offset_x: Float
315
316 # Difference on the X axis
317 var offset_y: Float
318
319 # Difference on the X axis
320 var offset_z: Float
321
322 redef fun x do return ref.x + offset_x
323 redef fun y do return ref.y + offset_y
324 redef fun z do return ref.z + offset_z
325
326 redef fun x=(value) do if value != null then offset_x += value - x
327 redef fun y=(value) do if value != null then offset_y += value - y
328 redef fun z=(value) do if value != null then offset_z += value - z
329
330 redef fun offset(x, y, z) do return new OffsetPoint3d(self, x.to_f, y.to_f, z.to_f)
331 end