122 lines
4.3 KiB
Python
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)
|