BoSLOO/renderer.py

175 lines
7.8 KiB
Python

import numpy, pygame, math
class Point:
"""Numpy 3-vec"""
def __init__(self, x, y, z):
self.vector = numpy.array([x, y, z])
def magnitude(self):
return numpy.linalg.norm(self.vector)
def normalize(self):
self.vector = self.vector/self.magnitude()
return self
def distanceFromPoint(self, otherPoint:"Point"):
return numpy.linalg.norm(self.vector - otherPoint.vector)
def distanceFromLine(self, line:"Line"):
return numpy.linalg.norm(numpy.cross(line.p2.vector - line.p1.vector, self.vector - line.p1.vector)/numpy.linalg.norm(line.p2.vector - line.p1.vector))
def add(p1, p2):
sum = numpy.add(p1.vector, p2.vector)
return Point(sum[0], sum[1], sum[2])
def subtract(p1, p2):
diff = numpy.subtract(p1.vector, p2.vector)
return Point(diff[0], diff[1], diff[2])
def dot(p1, p2):
return numpy.dot(p1.vector, p2.vector)
def scalarMult(p1, scalar):
mult = p1.vector * scalar
return Point(mult[0], mult[1], mult[2])
Point.zero = Point(0, 0, 0)
class Ray:
def __init__(self, origin:Point, direction:Point):
self.origin = origin
self.direction = direction
class Line:
def __init__(self, p1:Point, p2:Point):
self.p1 = p1
self.p2 = p2
def intersectWithPlane(self, plane):
lineVec = Point.subtract(self.p2, self.p1)
dot = Point.dot(plane.normal, lineVec)
if abs(dot) > 1e-6:
w = Point.subtract(self.p1, plane.point)
fac = -Point.dot(plane.normal, w) / dot
u = Point.scalarMult(lineVec, fac)
return Point.add(self.p1, u)
else:
return None
class Plane:
def __init__(self, point:Point, normal:Point):
self.point = point
self.normal = normal
class Camera:
"""Object which will be used to paint pixels on screen."""
def __init__(self, surface:pygame.Surface, location:Point, target:"Planet", objects, hFOV = 55, vFOV = 55):
self.surface = surface
self.objects = objects
self.location = location
self.target = target
self.hFOV = hFOV
self.vFOV = vFOV
def isInside(self, planet:"Planet"):
"""returns True if camera is inside the planet."""
return numpy.linalg.norm(self.location.magnitude) < planet.radius
def renderFrame(self):
"""generates a frame and draws it to the surface. Does not update screen; use pygame.display.flip()"""
winWidth, winHeight = self.surface.get_size()
winDistance = winWidth * numpy.cos(numpy.radians(self.hFOV)/2) / 2 #distance for a virtual screen to exist in-space to give the correct FOV
vecToCenter = Point.subtract(self.target.location, self.location)
vecToCenter.normalize()
screenPlane = Plane(Point.add(self.location, Point.scalarMult(vecToCenter, winDistance)), vecToCenter)
screenSurface = pygame.Surface((winWidth, winHeight))
#pygame uses 0,0 as the top left corner
for obj in self.objects:
if type(obj).__name__ == "OrbitingBody":
lineToCamera = Line(obj.location, self.location)
intersectPoint = lineToCamera.intersectWithPlane(screenPlane)
if intersectPoint is not None:
intersectPoint = Point.add(intersectPoint, Point(int(winWidth/2), int(winHeight/2), 0))
pygame.draw.circle(screenSurface, (255,255,150), (int(intersectPoint.vector[0]), int(intersectPoint.vector[1])), obj.displaySize)
elif type(obj).__name__ == "Planet":
lineToCamera = Line(obj.location, self.location)
intersectPoint = lineToCamera.intersectWithPlane(screenPlane)
if intersectPoint is not None:
intersectPoint = Point.add(intersectPoint, Point(int(winWidth/2), int(winHeight/2), 0))
pygame.draw.circle(screenSurface, (255,255,150), (int(intersectPoint.vector[0]), int(intersectPoint.vector[1])), 15)
elif isinstance(obj, list):
for orbitline in obj:
if orbitline.color != (0,0,0):
lineToCamera = Line(orbitline.location, self.location)
intersectPoint = lineToCamera.intersectWithPlane(screenPlane)
if intersectPoint is not None:
intersectPoint = Point.add(intersectPoint, Point(int(winWidth/2), int(winHeight/2), 0))
pygame.draw.circle(screenSurface, orbitline.color, (int(intersectPoint.vector[0]), int(intersectPoint.vector[1])), 1)
screenSurface = pygame.transform.flip(screenSurface, False, True)
self.surface.blit(screenSurface, (0,0))
def renderImage(self, sat:"OrbitingBody"):
"""generates a single image and saves it to disk"""
frozenSat = sat.location
winWidth, winHeight = self.surface.get_size()
winDistance = winWidth * numpy.cos(numpy.radians(self.hFOV)/2) / 2 #distance for a virtual screen to exist in-space to give the correct FOV
vecToCenter = Point.subtract(self.target.location, self.location)
vecToCenter.normalize()
screenPlane = Plane(Point.add(self.location, Point.scalarMult(vecToCenter, winDistance)), vecToCenter)
screenPlaneOrigin = Point.subtract(screenPlane.point, Point(int(winWidth/2), int(winHeight/2), 0))
screenSurface = pygame.Surface((winWidth, winHeight))
#pygame uses 0,0 as the top left corner
satDistance = -1
#DEBUG
minlat = 1
for column in range(0, winWidth):
for row in range(0, winHeight):
#get line in world going through this pixel
worldLine = Line(self.location, Point.add(screenPlaneOrigin, Point(column, row, 0)))
#compare distance from center of planet to radius of planet to determine intersection
dist = frozenSat.distanceFromLine(worldLine)
if satDistance < 0 or dist < satDistance:
satDistance = dist
satPixel = (column, row)
if self.target.location.distanceFromLine(worldLine) < self.target.radius:
epsilon = 0.1
yPrime = (row + screenPlaneOrigin.vector[1]) * (self.location.vector[2] / winDistance)
xPrime = min([abs((column + screenPlaneOrigin.vector[0]) * (self.location.vector[2] / winDistance)), self.target.radius])
try:
lat = math.modf((math.acos(yPrime / self.target.radius) / (3.141592/12.0)))[0] * math.sin(math.acos(xPrime / self.target.radius))
except:
screenSurface.set_at((column, row), (20,20,20))
continue
if lat < minlat:
minlat = lat
if -epsilon < lat < epsilon:
screenSurface.set_at((column, row), (200,200,200))
else:
screenSurface.set_at((column, row), (20,20,20))
if screenSurface.get_at(satPixel) == (0,0,0):
circleBorder = 0
else:
if self.location.distanceFromPoint(frozenSat) > self.location.distanceFromPoint(self.target.location):
circleBorder = 2
else:
circleBorder = 0
pygame.draw.circle(screenSurface, (230, 227, 64), satPixel, 4, width = circleBorder)
screenSurface = pygame.transform.flip(screenSurface, False, True)
pygame.image.save(screenSurface, "test.png")
#for row in range(int(-winHeight/2), int(winHeight/2)):
# for column in range(int(-winWidth/2), int(winWidth/2)):
# line = Line(self.location, Point(self.location.x + column))