def __init__(self, Pos, W, H, Fov=1, Samples=256): """ Initialises a Camera class. Parameters: Pos: Vec3. The position of the camera W: Float. The camera width H: Float. The camera height Optional Parameters: Fov: Float. The field of view constant of the camera. Default 1. Samples: Int. The rendering sample rate. Deafault 256 """ # Init variables self.fov = Fov self.w = W # Width self.h = H # Height self.pos = Pos # Camera position self.samples = Samples # Sample Rate self.normal = Vec3(0, 0, -1) # Normal (used for rotation) self.bgColor = Vec3(1, 1, 1) # background colour self.target = Vec3(0, 0, 0) # Camera target, used for rotation self.barWidth = 50 # progress bar width # Array for image data self.image_array = [[0] * (self.w * 3) for i in range(self.h)] # set rotation matrix self.ca = self.setCamera()
def loadModel(self, path, mtl): """ Loads a model from a obj and mtl file. Parmeters: path: String. The path to the obj file mtl: String. The path to the mtl file """ mat = None if mtl is not None: for line in open(mtl): if "newmtl" in line: # update current material mat = line[7:].strip() elif line[0:2] == "Kd": # add material colour self.materials[mat] = ( Vec3(*(map(float, line[3:].strip().split())))) verts = [] for line in open(path): # If line is a vertex if line[0] == "v": # Add vertex verts.append( Vec3(*(map(float, line[1:].strip().split(" "))))) # If line is a face if line[0] == "f": # add face a = line[1:].strip().split(" ") self.addPrimitive( Triangle(*[Copy(verts[int(i) - 1]) for i in a], mat)) # if line is a material declaration if "usemtl" in line: mat = line[7:].strip() # Update material
def __init__(self, orig=Vec3(0, 0, 0), dir=Vec3(0, 0, 0)): """ Initializes the ray. Optional parameters: orig: Vec3. The origin of the ray. dir: Vec3. The direction of the ray. """ self.o = orig self.d = Normalize(dir)
def midDay(scene): sun = Sun(pos=Vec3(40, 100, 30)) # orient sun sun.lookAt(Vec3(0, 0, 0)) # Add sun to scene scene.addLight(sun) # Create sky sky = Sky() # Add sky to scene scene.addLight(sky)
def dusk(scene): sun = Sun(pos=Vec3(40, 20, 0), colour=Vec3(0.4, 0.2, 0.4)) # orient sun sun.lookAt(Vec3(0, 0, 0)) # Add sun to scene scene.addLight(sun) # Create sky sky = Sky() # Add sky to scene scene.addLight(sky)
def __init__(self, b=AABB(Vec3(0, 0, 0), Vec3(0, 0, 0))): """ Initializes a Braunch Class """ self.leaves = [] self.braunches = [] self.materials = {} self.bounds = b self.lights = [] self.average = 0 self.time = 0
def __init__(self, size=2, normal=Vec3(0, 0, 1), pos=Vec3(1, 0, 0), colour=Vec3(0.77, 0.77, 0.72)): """ Initializes a sun class Optional Parmeters: size: Float. The size of the sun. Default 2 normal: Vec3. The direction the sun's facing. Default Vec3(0, 0, 1) pos: Vec3. The position of the sun. Default Vec3(1, 0, 0) colour: Vec3. The colour of the sun. Default colour=Vec3(0.97, 0.97, 0.72) """ self.size = size self.normal = normal self.pos = pos self.colour = colour
def intersectLeaves(self, leaves, ray): close = (False, float("inf"), Vec3(0, 0, 0)) indie = None self.average += len(leaves) for i in leaves: intersection = i.intersect(ray) if intersection[0] and 0 < intersection[1] < close[1]: close = intersection indie = i return close, indie
def diskPoint(normal): """ Returns a random point on a disk with a radius of 1 normal: Vec3. The normal of the disk to be used return: Vec3. The random point """ theta = random() * 2 * pi # theta in radians (0-2) mag = sqrt(random()) # sqrt makes the distribution uniform y = sin(theta) * mag # Generate x and y coords with some trig x = cos(theta) * mag return orthonormal(Vec3(x, y, 0), normal) # rotate to match normal
def getDir(self, x, y): """ Get direction from pixel coordinates Parameters: x: The pixel's x coordinates y: The pixel's y coordinates """ # calculate direction d = Vec3(x / self.w * 2 - 1, y / self.h * 2 - 1, self.fov) d = Normalize(d) # Rotate to match camera rotation return self.multMat(self.ca, d)
def fromScene(self, scene): """ Constructs an octree from a scene Parameters: Scene: Scene. The scene to construct an octree from. """ # Calculate the bounding box of the scene minx = miny = minz = +10000 maxx = maxy = maxz = -10000 for i in scene.primitives: minx = min(min(i.v0.x, i.v1.x, i.v2.x), minx) miny = min(min(i.v0.y, i.v1.y, i.v2.y), miny) minz = min(min(i.v0.z, i.v1.z, i.v2.z), minz) maxx = max(max(i.v0.x, i.v1.x, i.v2.x), maxx) maxy = max(max(i.v0.y, i.v1.y, i.v2.y), maxy) maxz = max(max(i.v0.z, i.v1.z, i.v2.z), maxz) self.bounds = AABB(Vec3(minx, miny, minz), Vec3(maxx, maxy, maxz)) for t in scene.primitives: self.grow((t)) self.addMaterials(scene.materials) self.addLights(scene.lights)
def setCamera(self): """ Updates the camera's rotation matrix """ ro = self.pos # ray origin ta = self.target # ray target cr = 0 # Angular rotation (should always be 0) # Construct the matix using some fancy linear algebra cw = Normalize(ta - ro) cp = Vec3(sin(cr), cos(cr), 0.0) cu = Normalize(Cross(cw, cp)) cv = Normalize(Cross(cu, cw)) return [cu, cv, cw]
def rendererCalcColor(self, ray, numBounce, tracer): """ Calculates a pixel colour given a starting ray using Monte Carlo magik! Parameters: ray: Ray. The ray to be traced numBounce: Int. The number of bounces the ray is allowed to do tracer: Scene. The scene """ # Variables for colour accumulation tCol = Vec3(0, 0, 0) gCol = Vec3(1, 1, 1) for i in range(numBounce): # intersect with seen isec = tracer.worldIntersect(ray) # intersection information sec = isec["t"] # if no intersection if not sec[0]: # stop the accumulation process or return the sky if i == 0: return self.bgColor else: break # Calculate intersection position pos = ray.o + (ray.d ^ sec[1]) # load material material = isec["index"].mat # Load surface colour and compute direct lighting sCol = tracer.materials[material] dCol = self.applyDirectLighting(pos, sec[2], tracer) # Create new ray ray = Ray(orig=pos + (sec[2] ^ 0.1), dir=OrientedHemiDir(sec[2])) # accumulate colours gCol = sCol * gCol tCol += gCol * dCol # return the total colour return tCol
def applyDirectLighting(self, pos, nor, scene): """ Applies Direct lighting Parameters: pos: Vec3. The point to apply the direct lighting nor: Vec3. The surface normal. scene: Scene. The scene. """ # start the accumulation dCol = Vec3(0, 0, 0) # iterate over lights and accumulate colors for i in scene.lights: dCol += i.calcDirect(pos, nor, scene) return dCol
def multMat(self, mat, vec): """ Multiplies a Vec3 by a matrix. Parameters: mat: List<Vec3>. The matrix to be multiplied vec: Vec3. The vector to be multiplied """ # Initialise the output vector out = Vec3(0, 0, 0) # iterate through the dimensions dimensions = ["x", "y", "z"] for i in range(3): out = out + (mat[i] ^ getattr(vec, dimensions[i])) return out
def intersect(self, ray): """Intersects a ray with a Triangle ray: Ray the ray to be intersected return: Tuple (Bool hit, Float distance, Vec3 normal) """ # Miss tuple miss = (False, 0, Vec3(0, 0, 0)) normal = Vec3(0, 0, 0) v0 = self.v0 v1 = self.v1 v2 = self.v2 # edges of the triangle v0v1 = v1 - v0 v0v2 = v2 - v0 # Determinant of a 3*3 matrix using triple scaler product pvec = Cross(ray.d, v0v2) det = Dot(v0v1, pvec) if det < 0.000001: return miss invDet = 1.0 / det tvec = ray.o - v0 u = Dot(tvec, pvec) * invDet # make sure the barycentric coordinates are in the triangle if u < 0 or u > 1: return miss qvec = Cross(tvec, v0v1) v = Dot(ray.d, qvec) * invDet # make sure the barycentric coordinates are in the triangle if v < 0 or u + v > 1: return miss return (True, Dot(v0v2, qvec) * invDet, self.normal)
def orthonormal(p, normal): """ Translates a normalized vector to have its z axis aligned along a specific normal. p: Vec3 the vector to be translated normal: the normal for the vector to be translated around """ # create orthonormal basis around normal w = normal v = Cross(Vec3(0.00319, 0.0078, 1.0), w) # jittered up v = Normalize(v) # normalize u = Cross(v, w) hemi_dir = (u ^ p.x) + (v ^ p.y) + (w ^ p.z) # linear projection return Normalize(hemi_dir) # make direction
def sampleHemisphere(u1, u2): """Return a point on the hemisphere from polar coordinates u1: Float. the radius of the polar coordinats u2: Float. theta return: Vec3. point on hemisphere """ # make r uniform over the sphere using the inverse itegral # of the distribution density r = sqrt(u1) # scale theta to radians theta = 2 * pi * u2 # calculate x and y x = r * cos(theta) y = r * sin(theta) return Vec3(x, y, sqrt(max(0, 1 - u1)))
def worldIntersect(self, ray): """ intersects the ray with the octree. Parameters: r: Ray. The ray to intersect return: Tuple (Bool hit, Float distance, Vec3 normal) """ # the closest intersection so far close = (False, float("inf"), Vec3(0, 0, 0)) index = -1 for i in range(len(self.primitives)): intersection = self.primitives[i].intersect(ray) self.average += 1 # if there's a new closest intersection update index and close if intersection[0] and 0 < intersection[1] < close[1]: close = intersection index = i return {"t": close, "index": self.primitives[index]}
def initRay(self, x): for y in range(0, self.h): col = Vec3(0, 0, 0) # Aspect correction in the case the output image is not square # This took me super long to figure out mx = ((x + (self.h - self.w) / 2) * self.w/self.h) # average the samples for i in range(self.samples): # calculate direction # Adds the random to get anti-aliasing a = self.getDir(mx + random(), y + random()) # create ray for rendering ray = Ray(orig=self.pos, dir=a) # render! col = col + self.rendererCalcColor(ray, 4, self.tracer) col = col ^ (1 / self.samples) # average the samples self.savePixel(col, x, y) # save pixel return "complete"
def worldIntersect(self, r): """ intersects the ray with the octree. Parameters: r: Ray. The ray to intersect return: Tuple (Bool hit, Float distance, Vec3 normal) """ miss = (False, float("inf"), Vec3(0, 0, 0)) # Breadth first search queue = [self] index = None for i in queue: if i.bounds.intersect(r) < 1000: # Check leaves intersect, indet = self.intersectLeaves(i.leaves, r) if intersect[0] and 0 < intersect[1] < miss[1]: miss = intersect index = indet # Check braunches queue += i.braunches # print(total) return {"t": miss, "index": index}
def render(self, tracer, imgOut): """ Renders a scene to an image Parameters: tracer: Scene. The scene to be rendered. imgOut: String. The name of the output file """ # loop through all the pixels in the image for x in range(self.w): for y in range(self.h): col = Vec3(0, 0, 0) # Aspect correction in the case the output image is not square # This took me super long to figure out mx = ((x + (self.h - self.w) / 2) * self.w / self.h) # average the samples for i in range(self.samples): # calculate direction # Adds the random to get anti-aliasing a = self.getDir(mx + random(), y + random()) # create ray for rendering ray = Ray(orig=self.pos, dir=a) # render! col = col + self.rendererCalcColor(ray, 4, tracer) col = col ^ (1 / self.samples) # average the samples self.savePixel(col, x, y) # save pixel # ===Update progress bar=== # Clear terminal os.system('cls' if os.name == 'nt' else 'clear') # Calculate progress prog = int(round((x) / self.w * self.barWidth)) # Print progress bar print("[" + "=" * prog + ">" + " " * (self.barWidth - prog) + "] " + str(round(prog * (100 / self.barWidth))) + "% completed") # ===Save Image=== self.saveImage(imgOut) # save image
def subDivide(self): """ Subdivides into 8 smaller AABB's returns: List. 8 smaller AABB's """ # A-H represent the 8 corners of the box # M is the midpoint of the box a = self.info[0] g = self.info[1] b = Vec3(g.x, a.y, a.z) c = Vec3(g.x, a.y, g.z) d = Vec3(a.x, a.y, g.z) e = Vec3(a.x, g.y, a.z) f = Vec3(b.x, g.y, b.z) h = Vec3(d.x, g.y, g.z) # Calculate midpoint m = self.info[0] + ((self.info[1] - self.info[0]) ^ 0.5) boxes = [] # Construct all 8 boxes for i in [a, b, c, d, e, f, g, h]: boxes.append(AABB(i, m)) return boxes
from Vector3 import Vec3, Cross, Normalize from random import random from math import pi, sin, cos normal = Normalize(Vec3(1, 0, 0)) f = open("data.dat", "w") def diskPoint(): theta = random() * pi * 2 mag = random() x = cos(theta) * mag y = sin(theta) * mag return Vec3(x, y, 0) for i in range(1000): p = diskPoint() f.write("{0} {1}\n".format(p.x, p.y)) w = normal v = Cross(Vec3(0.00319, 0.0078, 1.0), w) # jittered up v = Normalize(v) # normalize u = Cross(v, w) hemi_dir = (u ^ p.x) + (v ^ p.y) + (w ^ p.z) hemi_dir = Normalize(hemi_dir) # f.write("{0} {1} {2}\n".format(hemi_dir.x, hemi_dir.y, hemi_dir.z)) print(Normalize(hemi_dir))
def __init__(self, colour=Vec3(0.6, 0.6, 0.55)): """initializes a sky class Optional parameters: colour: Vec3. The colour of the sun. Default Vec3(0.4, 0.4, 0.45) """ self.colour = colour
from Vector3 import Vec3 from Camera import Camera from Scene import Scene from Sun import Sun from Sky import Sky from timeit import default_timer as timer from Octree import Braunch # turn on tree useTree = False # Create new scene scene = Scene() # Load Model and materials scene.loadModel("models/pyramid.obj", "models/pyramid.mtl") # Create Sun sun = Sun(pos=Vec3(80, 50, 50)) sun.lookAt(Vec3(0, 0, 0)) scene.addLight(sun) # Create sky sky = Sky() scene.addLight(sky) # Create Camera cam = Camera(Vec3(8, 4, 8), int(200), int(200), Fov=1.4, Samples=2) cam.lookAt(Vec3(0, 3, 0)) # Render scene ts = timer() if useTree: tree = Braunch() tree.fromScene(scene) cam.render(tree, "pyramid.png") else:
message="File does not exist") material = validInput("Material File (.obj): ", path.isfile, message="File does not exist") width = inputInt("Output Width: ") height = inputInt("Output Height: ") while True: proto = input("lighting (" + str([i for i in lighting.presets]) + "): ") if proto in lighting.presets: break preset = proto output = input("output image name: ") # add the extension png if necessary output += "" if output[-4:] == ".png" else ".png" # Create new scene scene = Scene() # Load Model and materials scene.loadModel(model, material) # Add lighting lighting.presets[preset](scene) # Create Camera cam = Camera(Vec3(-8, 5, 8), width, height, Fov=1.4, Samples=samples) cam.lookAt(Vec3(0, 3, 0)) # Render scene ts = timer() cam.render(scene, output) print("Render time: {}".format(timer() - ts)) print("Intersections: ", scene.average)
tmin, tmax = tmax, tmin for a in ["y", "z"]: b = getattr(ray.d, a) if b == 0: b += 0.000001 tymin = (getattr(self.min, a) - getattr(ray.o, a)) / \ b tymax = (getattr(self.max, a) - getattr(ray.o, a)) / \ b # Swap if necessary if tymin > tymax: tymin, tymax = tymax, tymin # Check collision if (tmin > tymax) or (tymin > tmax): return False # Update tmin and tmax tmin = min(tmin, tymin) tmax = max(tmax, tymax) t = tmin if (t < 0): t = tmax if t < 0: return False return True aabb = AABB(Vec3(-1, -1, -1), Vec3(1, 1, 1)) ray = Ray(orig=Vec3(0, 0, -3), dir=Vec3(0, 0, -2)) z = aabb.intersect(ray) print(z)
from Octree import Braunch from Scene import Scene from Vector3 import Vec3 from timeit import default_timer as timer from Sun import Sun from Sky import Sky from Camera import Camera # Create new scene scene = Scene() # Load Model and materials scene.loadModel("untitled.obj", "untitled.mtl") sun = Sun(pos=Vec3(-20, 30, 30)) sun.lookAt(Vec3(0, 0, 0)) scene.addLight(sun) sun.size = 3 # Create sky sky = Sky(colour=Vec3(0.3, 0.2, 0.3)) scene.addLight(sky) # Create Camera cam = Camera(Vec3(-4, 3, -5), 256, 256, Fov=1, Samples=7) cam.lookAt(Vec3(0, 0, 0)) tree = Braunch() tree.fromScene(scene) tree.display() # Render scene ts = timer() cam.render(tree) treeTime = timer() - ts # print("Render time: {}".format(timer()-ts))
def diskPoint(): theta = random() * pi * 2 mag = random() x = cos(theta) * mag y = sin(theta) * mag return Vec3(x, y, 0)