from __future__ import annotations
from math import pi, tan
from pyglet.math import Mat4, Vec2, Vec3, Vec4
from arcade.camera.data_types import (
CameraData,
OrthographicProjectionData,
PerspectiveProjectionData,
)
from arcade.types import Point
[docs]
def generate_view_matrix(camera_data: CameraData) -> Mat4:
"""
Using the ViewData it generates a view matrix from the pyglet Mat4 look at function
"""
# Even if forward and up are normalized floating point error means every vector
# must be normalized.
fo = Vec3(*camera_data.forward).normalize() # Forward Vector
up = Vec3(
*camera_data.up
) # Initial Up Vector (Not necessarily perpendicular to forward vector)
ri = fo.cross(up).normalize() # Right Vector
up = ri.cross(fo).normalize() # Up Vector
po = Vec3(*camera_data.position)
# fmt: off
return Mat4(
ri.x, up.x, -fo.x, 0.0,
ri.y, up.y, -fo.y, 0.0,
ri.z, up.z, -fo.z, 0.0,
-ri.dot(po), -up.dot(po), fo.dot(po), 1.0
)
# fmt: on
[docs]
def generate_orthographic_matrix(
perspective_data: OrthographicProjectionData, zoom: float = 1.0
) -> Mat4:
"""
Using the OrthographicProjectionData a projection matrix is generated where
the size of an object is not affected by depth.
Generally keep the scale value to integers or negative powers of integers
(``2^-1, 3^-1, 2^-2``, etc.) to keep the pixels uniform in size. Avoid a zoom of 0.0.
"""
# Scale the projection by the zoom value. Both the width and the height
# share a zoom value to avoid ugly stretching.
left = perspective_data.left / zoom
right = perspective_data.right / zoom
bottom = perspective_data.bottom / zoom
top = perspective_data.top / zoom
z_near, z_far = perspective_data.near, perspective_data.far
width = right - left
height = top - bottom
depth = z_far - z_near
sx = 2.0 / width
sy = 2.0 / height
sz = 2.0 / -depth
tx = -(right + left) / width
ty = -(top + bottom) / height
tz = -(z_far + z_near) / depth
# fmt: off
return Mat4(
sx, 0.0, 0.0, 0.0,
0.0, sy, 0.0, 0.0,
0.0, 0.0, sz, 0.0,
tx, ty, tz, 1.0
)
# fmt: on
[docs]
def generate_perspective_matrix(
perspective_data: PerspectiveProjectionData, zoom: float = 1.0
) -> Mat4:
"""
Using the OrthographicProjectionData a projection matrix is generated where
the size of the objects is not affected by depth.
Generally keep the scale value to integers or negative powers of integers
(``2^-1, 3^-1, 2^-2``, etc.) to keep the pixels uniform in size. Avoid a zoom of 0.0.
"""
fov = perspective_data.fov / zoom
z_near, z_far, aspect = (
perspective_data.near,
perspective_data.far,
perspective_data.aspect,
)
xy_max = z_near * tan(fov * pi / 360)
y_min = -xy_max
x_min = -xy_max
width = xy_max - x_min
height = xy_max - y_min
depth = z_far - z_near
q = -(z_far + z_near) / depth
qn = -2 * z_far * z_near / depth
w = 2 * z_near / width
w = w / aspect
h = 2 * z_near / height
# fmt: off
return Mat4(
w, 0, 0, 0,
0, h, 0, 0,
0, 0, q, -1,
0, 0, qn, 0
)
# fmt: on
[docs]
def project_orthographic(
world_coordinate: Point,
viewport: tuple[int, int, int, int],
view_matrix: Mat4,
projection_matrix: Mat4,
) -> Vec2:
x, y, *_z = world_coordinate
z = 0.0 if not _z else _z[0]
world_position = Vec4(x, y, z, 1.0)
projected_position = projection_matrix @ view_matrix @ world_position
screen_coordinate_x = viewport[0] + (0.5 * projected_position.x + 0.5) * viewport[2]
screen_coordinate_y = viewport[1] + (0.5 * projected_position.y + 0.5) * viewport[3]
return Vec2(screen_coordinate_x, screen_coordinate_y)
[docs]
def unproject_orthographic(
screen_coordinate: Point,
viewport: tuple[int, int, int, int],
view_matrix: Mat4,
projection_matrix: Mat4,
) -> Vec3:
x, y, *_z = screen_coordinate
z = 0.0 if not _z else _z[0]
screen_x = 2.0 * (x - viewport[0]) / viewport[2] - 1
screen_y = 2.0 * (y - viewport[1]) / viewport[3] - 1
_projection = ~projection_matrix
_view = ~view_matrix
_unprojected_position = _projection @ Vec4(screen_x, screen_y, 0.0, 1.0)
_world_position = _view @ Vec4(_unprojected_position.x, _unprojected_position.y, z, 1.0)
return Vec3(_world_position.x, _world_position.y, _world_position.z)
[docs]
def project_perspective(
world_coordinate: Point,
viewport: tuple[int, int, int, int],
view_matrix: Mat4,
projection_matrix: Mat4,
) -> Vec2:
x, y, *_z = world_coordinate
z = 1.0 if not _z else _z[0]
world_position = Vec4(x, y, z, 1.0)
semi_projected_position = projection_matrix @ view_matrix @ world_position
div_val = semi_projected_position.w
projected_x = semi_projected_position.x / div_val
projected_y = semi_projected_position.y / div_val
screen_coordinate_x = viewport[0] + (0.5 * projected_x + 0.5) * viewport[2]
screen_coordinate_y = viewport[1] + (0.5 * projected_y + 0.5) * viewport[3]
return Vec2(screen_coordinate_x, screen_coordinate_y)
[docs]
def unproject_perspective(
screen_coordinate: Point,
viewport: tuple[int, int, int, int],
view_matrix: Mat4,
projection_matrix: Mat4,
) -> Vec3:
x, y, *_z = screen_coordinate
z = 1.0 if not _z else _z[0]
screen_x = 2.0 * (x - viewport[0]) / viewport[2] - 1
screen_y = 2.0 * (y - viewport[1]) / viewport[3] - 1
screen_x *= z
screen_y *= z
projected_position = Vec4(screen_x, screen_y, 1.0, 1.0)
view_position = ~projection_matrix @ projected_position
world_position = ~view_matrix @ Vec4(view_position.x, view_position.y, z, 1.0)
return Vec3(world_position.x, world_position.y, world_position.z)