gamnit: enable dynamic window resizing on desktop
[nit.git] / lib / gamnit / cameras.nit
index fa23be8..688082b 100644 (file)
@@ -29,7 +29,7 @@ abstract class Camera
        var display: GamnitDisplay
 
        # Position of this camera in world space
-       var position = new Point3d[Float](0.0, 0.0, 0.0) is writable
+       var position = new Point3d[Float](0.0, 0.0, 0.0)
 
        # The Model-View-Projection matrix created by this camera
        #
@@ -38,7 +38,7 @@ abstract class Camera
        fun mvp_matrix: Matrix is abstract
 end
 
-# Simple camera with perspective oriented with Euler angles (`pitch`, `yaw`, `roll`)
+# Simple camera with perspective oriented with Euler angles (`pitch, yaw, roll`)
 class EulerCamera
        super Camera
 
@@ -63,8 +63,8 @@ class EulerCamera
 
        # Clipping wall the farthest of the camera, in world dimensions
        #
-       # Default at `100.0` but this one should be adapted to each context.
-       var far = 100.0 is writable
+       # Default at `10000.0` but this one should be adapted to each context.
+       var far = 10000.0 is writable
 
        # Look around sensitivity, used by `turn`
        var sensitivity = 0.005 is writable
@@ -98,6 +98,17 @@ class EulerCamera
                position.y += dy
        end
 
+       # Aim the camera at `x, y, z`
+       fun look_at(x, y, z: Float)
+       do
+               var dx = position.x
+               var dy = position.y
+               var dz = position.z
+
+               yaw = atan2(dx, dz)
+               pitch = atan2(-dy, dz)
+       end
+
        # Rotation matrix produced by the current rotation of the camera
        protected fun rotation_matrix: Matrix
        do
@@ -116,15 +127,203 @@ class EulerCamera
                var view = new Matrix.identity(4)
 
                # Translate the world away from the camera
-               view.translate(-position.x/2.0, -position.y/2.0, -position.z/2.0)
+               view.translate(-position.x, -position.y, -position.z)
 
                # Rotate the camera, first by looking left or right, then up or down
                view = view * rotation_matrix
 
                # Use a projection matrix with a depth
-               var projection = new Matrix.perspective(pi*field_of_view_y/2.0,
+               var projection = new Matrix.perspective(field_of_view_y,
                        display.aspect_ratio, near, far)
 
                return view * projection
        end
+
+       # Reset the camera position so that `height` world units are visible on the y axis at z=0
+       #
+       # By default, `height` is set to `display.height`.
+       #
+       # After the reset, the camera sits on the Z axis and rotation values are reset to 0.
+       # The X axis is horizontal on the screen and the Y axis is vertical.
+       # Higher values on the Z axis are closer to the camera.
+       fun reset_height(height: nullable Float)
+       do
+               if height == null then height = display.height.to_f
+
+               var opp = height / 2.0
+               var angle = field_of_view_y / 2.0
+               var adj = opp / angle.tan
+
+               position.x = 0.0
+               position.y = 0.0
+               position.z = adj
+
+               pitch = 0.0
+               yaw = 0.0
+               roll = 0.0
+       end
+
+       # Convert the position `x, y` on screen, to world coordinates on the plane at `target_z`
+       #
+       # `target_z` defaults to `0.0` and specifies the Z coordinates of the plane
+       # on which to project the screen position `x, y`.
+       #
+       # This method assumes that the camera is looking along the Z axis towards higher values.
+       # Using it in a different orientation can be useful, but won't result in valid
+       # world coordinates.
+       fun camera_to_world(x, y: Numeric, target_z: nullable Float): Point[Float]
+       do
+               # TODO, this method could be tweaked to support projecting the 2D point,
+               # on the near plane (x,y) onto a given distance no matter to orientation
+               # of the camera.
+
+               target_z = target_z or else 0.0
+
+               # Convert from pixel units / window resolution to
+               # units on the near clipping wall to
+               # units on the target wall at Z = 0
+               var near_height = (field_of_view_y/2.0).tan * near
+               var cross_screen_to_near = near_height / (display.height.to_f/2.0)
+               var cross_near_to_target = (position.z - target_z) / near
+               var mod = cross_screen_to_near * cross_near_to_target
+
+               var wx = position.x + (x.to_f-display.width.to_f/2.0) * mod
+               var wy = position.y - (y.to_f-display.height.to_f/2.0) * mod
+               return new Point[Float](wx, wy)
+       end
+end
+
+# Orthogonal camera to draw UI objects with services to work with screens of different sizes
+#
+# X axis: left to right of the screen, from `position.x` to `position.x + width`
+# Y axis: top to bottom of the screen, from `position.y` to `position.y + height`
+# Z axis: far to near the camera (usually when values are higher), from `far` to `near`
+class UICamera
+       super Camera
+
+       # Clipping wall near the camera, defaults to 100.0
+       var near = 100.0 is writable
+
+       # Clipping wall the farthest of the camera, defaults to -100.0
+       var far: Float = -100.0 is writable
+
+       # Width in world units, calculated from `height` and the screen aspect ratio
+       fun width: Float do return height * display.aspect_ratio
+
+       # Height in world units, defaults to 1080.0
+       #
+       # Set this value using `reset_height`.
+       var height = 1080.0
+
+       # Reset the camera position so that `height` world units are visible on the Y axis
+       #
+       # This can be used to set standardized UI units independently from the screen resolution.
+       fun reset_height(height: nullable Float)
+       do
+               if height == null then height = display.height.to_f
+               self.height = height
+       end
+
+       # Convert the position `x, y` on screen, to UI coordinates
+       fun camera_to_ui(x, y: Numeric): Point[Float]
+       do
+               # FIXME this kind of method should use something like a canvas
+               # instead of being hard coded on the display.
+
+               var wx = x.to_f * width / display.width.to_f - position.x
+               var wy = y.to_f * height / display.height.to_f - position.y
+               return new Point[Float](wx, -wy)
+       end
+
+       # Center of the screen, from the point of view of the camera, at z = 0
+       var center: IPoint3d[Float] = new CameraAnchor(self, 0.5, -0.5)
+
+       # Center of the top of the screen, at z = 0
+       var top: IPoint3d[Float] = new CameraAnchor(self, 0.5, 0.0)
+
+       # Center of the bottom of the screen, at z = 0
+       var bottom: IPoint3d[Float] = new CameraAnchor(self, 0.5, -1.0)
+
+       # Center of the left border of the screen, at z = 0
+       var left: IPoint3d[Float] = new CameraAnchor(self, 0.0, -1.0)
+
+       # Center of the right border of the screen, at z = 0
+       var right: IPoint3d[Float] = new CameraAnchor(self, 1.0, -1.0)
+
+       # Top left corner of the screen, at z = 0
+       var top_left: IPoint3d[Float] = new CameraAnchor(self, 0.0, 0.0)
+
+       # Top right corner of the screen, at z = 0
+       var top_right: IPoint3d[Float] = new CameraAnchor(self, 1.0, 0.0)
+
+       # Bottom left corner of the screen, at z = 0
+       var bottom_left: IPoint3d[Float] = new CameraAnchor(self, 0.0, -1.0)
+
+       # Bottom right corner of the screen, at z = 0
+       var bottom_right: IPoint3d[Float] = new CameraAnchor(self, 1.0, -1.0)
+
+       redef fun mvp_matrix
+       do
+               var view = new Matrix.identity(4)
+
+               # Translate the world away from the camera
+               view.translate(-position.x, -position.y, -position.z)
+
+               # Use a projection matrix with a depth
+               var projection = new Matrix.orthogonal(0.0, width, -height, 0.0, near, far)
+
+               return view * projection
+       end
+end
+
+# Immutable relative anchors for reference points on `camera`
+private class CameraAnchor
+       super IPoint3d[Float]
+
+       # Reference camera
+       var camera: UICamera
+
+       # Reference position, the top left of the screen
+       var ref: Point3d[Float] = camera.position is lazy
+
+       # X position as proportion of the screen width
+       var relative_x: Float
+
+       # Y position as proportion of the screen height
+       var relative_y: Float
+
+       redef fun x do return ref.x + relative_x*camera.width
+       redef fun y do return ref.y + relative_y*camera.height
+       redef fun z do return ref.z
+
+       redef fun offset(x, y, z) do return new OffsetPoint3d(self, x.to_f, y.to_f, z.to_f)
+end
+
+# Position relative to another point or usually a `CameraAnchor`
+private class OffsetPoint3d
+       super Point3d[Float]
+
+       autoinit ref, offset_x, offset_y, offset_z
+
+       # Reference point to which the offsets are applied
+       var ref: IPoint3d[Float]
+
+       # Difference on the X axis
+       var offset_x: Float
+
+       # Difference on the X axis
+       var offset_y: Float
+
+       # Difference on the X axis
+       var offset_z: Float
+
+       redef fun x do return ref.x + offset_x
+       redef fun y do return ref.y + offset_y
+       redef fun z do return ref.z + offset_z
+
+       redef fun x=(value) do if value != null then offset_x += value - x
+       redef fun y=(value) do if value != null then offset_y += value - y
+       redef fun z=(value) do if value != null then offset_z += value - z
+
+       redef fun offset(x, y, z) do return new OffsetPoint3d(self, x.to_f, y.to_f, z.to_f)
 end