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] = {} 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 xy_point = Point(imgx, imgy) xy.append(xy_point) uvz[xy_point] = Point( texx / distance, texy / distance, 1 / distance, ) # 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 maxx <= minx or maxy <= miny: # This image is entirely off the screen! return (None, minx, miny, maxx, maxy) # 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)