1
0
mirror of synced 2024-12-13 23:11:06 +01:00
bemaniutils/bemani/format/afp/blend/perspective.py
2021-08-05 17:37:30 +00:00

122 lines
4.3 KiB
Python

from typing import Dict, List, Optional, Tuple
from ..types import Matrix, Point
def perspective_calculate(
imgwidth: int,
imgheight: int,
texwidth: int,
texheight: int,
transform: Matrix,
camera: Point,
focal_length: float,
) -> Tuple[Optional[Matrix], int, int, int, int]:
# Arbitrarily choose three points on the texture to create a pair of vectors
# so that we can interpolate backwards. This isn't as simple as inverting the
# view matrix like in affine compositing because dividing by Z makes the
# perspective transform non-linear. So instead we interpolate 1/Z, u/Z and
# v/Z since those ARE linear, and work backwards from there.
xy: List[Point] = []
uvz: Dict[Point, Point] = {}
minz: Optional[float] = None
maxz: Optional[float] = None
for (texx, texy) in [
(0, 0),
(texwidth, 0),
(0, texheight),
# Include this just to get a good upper bounds for where the texture
# will be drawn.
(texwidth, texheight),
]:
imgloc = transform.multiply_point(Point(texx, texy))
distance = imgloc.z - camera.z
imgx = ((imgloc.x - camera.x) * (focal_length / distance)) + camera.x
imgy = ((imgloc.y - camera.y) * (focal_length / distance)) + camera.y
if minz is None:
minz = distance
else:
minz = min(distance, minz)
if maxz is None:
maxz = distance
else:
maxz = max(distance, maxz)
xy_point = Point(imgx, imgy)
xy.append(xy_point)
uvz[xy_point] = Point(
texx / distance,
texy / distance,
1 / distance,
)
# Clip out anything that is completely off screen.
if minz is None or maxz is None:
raise Exception("Logic error!")
# Calculate the maximum range of update this texture can possibly reside in.
minx = max(int(min(p.x for p in xy)), 0)
maxx = min(int(max(p.x for p in xy)) + 1, imgwidth)
miny = max(int(min(p.y for p in xy)), 0)
maxy = min(int(max(p.y for p in xy)) + 1, imgheight)
if minz <= 0.0 and maxz <= 0.0:
# This is entirely behind the camera, clip it.
return (None, minx, miny, maxx, maxy)
if minx >= imgwidth or maxx < 0 or miny >= imgheight or maxy < 0:
# This is entirely off screen, clip it.
return (None, minx, miny, maxx, maxy)
if minz < 0.0 and maxz > 0.0:
# This clips through the camera, default to drawing the whole image.
minx = 0
maxx = imgwidth
miny = 0
maxy = imgheight
# Now that we have three points, construct a matrix that allows us to calculate
# what amount of each u/z, v/z and 1/z vector we need to interpolate values. The
# below matrix gives us an affine transform that will convert a point that's in
# the range 0, 0 to 1, 1 to a point inside the parallellogram that is made by
# projecting the two vectors we got from calculating the three texture points above.
xy_matrix = Matrix.affine(
a=xy[1].x - xy[0].x,
b=xy[1].y - xy[0].y,
c=xy[2].x - xy[0].x,
d=xy[2].y - xy[0].y,
tx=xy[0].x,
ty=xy[0].y,
)
# We invert that above, which gives us a matrix that can take screen space (imgx,
# imgy) and gives us instead those ratios, which allows us to then interpolate the
# u/z, v/z and 1/z values.
try:
xy_matrix = xy_matrix.inverse()
except ZeroDivisionError:
# This can't be inverted, so this shouldn't be displayed.
return (None, minx, miny, maxx, maxy)
# We construct a second matrix, which interpolates coordinates in the range of
# 0, 0 to 1, 1 and gives us back the u/z, v/z and 1/z values.
uvz_matrix = Matrix(
a11=uvz[xy[1]].x - uvz[xy[0]].x,
a12=uvz[xy[1]].y - uvz[xy[0]].y,
a13=uvz[xy[1]].z - uvz[xy[0]].z,
a21=uvz[xy[2]].x - uvz[xy[0]].x,
a22=uvz[xy[2]].y - uvz[xy[0]].y,
a23=uvz[xy[2]].z - uvz[xy[0]].z,
a31=0.0,
a32=0.0,
a33=0.0,
a41=uvz[xy[0]].x,
a42=uvz[xy[0]].y,
a43=uvz[xy[0]].z,
)
# Finally, we can combine the two matrixes to do the interpolation all at once.
inverse_matrix = xy_matrix.multiply(uvz_matrix)
return (inverse_matrix, minx, miny, maxx, maxy)