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)