lib/gamnit: intro `EulerCamera::reset_to_fit`
[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) is writable
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 # Rotation matrix produced by the current rotation of the camera
102 protected fun rotation_matrix: Matrix
103 do
104 var view = new Matrix.identity(4)
105
106 # Rotate the camera, first by looking left or right, then up or down
107 view.rotate(yaw, 0.0, 1.0, 0.0)
108 view.rotate(pitch, 1.0, 0.0, 0.0)
109 view.rotate(roll, 0.0, 0.0, 1.0)
110
111 return view
112 end
113
114 redef fun mvp_matrix
115 do
116 var view = new Matrix.identity(4)
117
118 # Translate the world away from the camera
119 view.translate(-position.x/2.0, -position.y/2.0, -position.z/2.0)
120
121 # Rotate the camera, first by looking left or right, then up or down
122 view = view * rotation_matrix
123
124 # Use a projection matrix with a depth
125 var projection = new Matrix.perspective(pi*field_of_view_y/2.0,
126 display.aspect_ratio, near, far)
127
128 return view * projection
129 end
130
131 # Reset the camera position so that `height` world units are visible on the y axis at z=0
132 #
133 # By default, `height` is set to `display.height`.
134 #
135 # After the reset, the camera sits on the Z axis and rotation values are reset to 0.
136 # The X axis is horizontal on the screen and the Y axis is vertical.
137 # Higher values on the Z axis are closer to the camera.
138 fun reset_height(height: nullable Float)
139 do
140 if height == null then height = display.height.to_f
141
142 var opp = height / 2.0
143 var angle = field_of_view_y / 2.0
144 var adj = opp / angle.tan
145
146 position.x = 0.0
147 position.y = 0.0
148 position.z = adj
149
150 pitch = 0.0
151 yaw = 0.0
152 roll = 0.0
153 end
154 end
155
156 # Orthogonal camera to draw UI objects with services to work with screens of different sizes
157 #
158 # X axis: left to right of the screen, from `position.x` to `position.x + width`
159 # Y axis: top to bottom of the screen, from `position.y` to `position.y + height`
160 # Z axis: far to near the camera (usually when values are higher), from `far` to `near`
161 class UICamera
162 super Camera
163
164 # Clipping wall near the camera, defaults to 100.0
165 var near = 100.0 is writable
166
167 # Clipping wall the farthest of the camera, defaults to -100.0
168 var far: Float = -100.0 is writable
169
170 # Width in world units, defaults to the width in pixels of the screen
171 var width: Float = display.width.to_f is lazy
172
173 # Height in world units, defaults to the height in pixels of the screen
174 var height: Float = display.height.to_f is lazy
175
176 # Reset the camera position so that `height` world units are visible on the Y axis
177 #
178 # By default, `height` is set to `display.height`.
179 #
180 # This can be used to set standardized UI units independently from the screen resolution.
181 fun reset_height(height: nullable Float)
182 do
183 if height == null then height = display.height.to_f
184
185 self.height = height
186 self.width = height * display.aspect_ratio
187 end
188
189 # Convert the position `x, y` on screen, to UI coordinates
190 fun camera_to_ui(x, y: Numeric): Point[Float]
191 do
192 # FIXME this kind of method should use something like a canvas
193 # instead of being hard coded on the display.
194
195 var wx = x.to_f * width / display.width.to_f - position.x
196 var wy = y.to_f * height / display.height.to_f - position.y
197 return new Point[Float](wx, wy)
198 end
199
200 # Anchor in the top left corner of the screen, at z = 0
201 fun top_left: Point3d[Float] do return new Point3d[Float](position.x, position.y, 0.0)
202
203 # Anchor in the top right corner of the screen, at z = 0
204 fun top_right: Point3d[Float] do return new Point3d[Float](position.x + width, position.y, 0.0)
205
206 # Anchor in the bottom left corner of the screen, at z = 0
207 fun bottom_left: Point3d[Float] do return new Point3d[Float](position.x, position.y + height, 0.0)
208
209 # Anchor in the bottom right corner of the screen, at z = 0
210 fun bottom_right: Point3d[Float] do return new Point3d[Float](position.x + width, position.y + height, 0.0)
211
212 # TODO cache the anchors and the matrix
213
214 redef fun mvp_matrix
215 do
216 var view = new Matrix.identity(4)
217
218 # Translate the world away from the camera
219 view.translate(-position.x/2.0, -position.y/2.0, -position.z/2.0)
220
221 # Use a projection matrix with a depth
222 var projection = new Matrix.orthogonal(0.0, width, -height, 0.0, near, far)
223
224 return view * projection
225 end
226 end