I am trying to do ray tracing in python (following the tutorial given in Ray Tracing in a weekend). Basically I am shooting rays from eye and the recursively bounce around, each time they hit something they become weaker (actually this is reverse ray casting but you get the idea). The output I am getting is incorrect.
My output:
Expected output:
The shadows are messed up. What could be wrong?
My code:
import numpy as np
import sys
import random
from PIL import Image
from math import *
from util import *
width = 60
height = 60
samples = 20
#-----------------------------------------------------------------------
def reflected(vector, axis):
return vector - axis * 2 * vector.dot(axis)
def RandomPointInSphere():
p = None
while True:
p = Vector(random.uniform(0,1),random.uniform(0,1),random.uniform(0,1))*2 - Vector(1,1,1)
if(p.dot(p) < 1):
break
return p
def GetNearestObject(objects, ray):
nearest_obj = None
min_hit = Intersection(None, INF, None, None)
for obj in objects:
hit = obj.intersect(ray)
if(hit.distance < min_hit.distance):
nearest_obj = obj
min_hit = hit
return min_hit
#-----------------------------------------------------------------------
def RayColor(objects, ray):
# Part 1: Diffuse Material
result = GetNearestObject(objects, ray)
if(result.point != None):
P = result.point
N = result.normal
E = RandomPointInSphere()
target = P + N + E
newRay = Ray(ray.origin, (target - ray.origin).normalize())
return RayColor(objects, newRay)*0.5
else:
t = 0.5 * (ray.direction.y + 1.0);
color = Vector(1.0, 1.0, 1.0)*(1.0 - t) + Vector(0.5, 0.7, 1.0)*t
color.x = min(color.x,1.0)
color.y = min(color.y,1.0)
color.z = min(color.z,1.0)
return Vector(1,1,1)
#-----------------------------------------------------------------------
def main():
global bitmap
eye = Vector(0,0,1)
ratio = float(width) / height
screen = Screen(-1, 1 / ratio, 1, -1 / ratio, 0)
objects = []
objects.append(Sphere(Vector(-0.2,0,-1), 0.7, Material(Vector(0.1,0,0), Vector(0.7,0,0), Vector(1,1,1), 100, 0.5)))
objects.append(Sphere(Vector(0,-9000,0), 9000-0.7, Material(Vector(0.1,0.1,0.1),Vector(0.6,0.6,0.6),Vector(1,1,1), 100, 0.5)))
light = Light(Vector(5,5,5), Material(Vector(1,1,1),Vector(1,1,1),Vector(1,1,1)))
for frame in range(1):
img = Image.new(mode = "RGB", size=(width, height), color=Color.WHITE)
bitmap = img.load() # create the pixel data
#--------------------------------------------------------------
#--- YOUR CODE HERE ---
#--------------------------------------------------------------
sys.setrecursionlimit(10000)
#breakpoint()
deltaX = (screen.right - screen.left)/(width-1)
deltaY = (screen.top - screen.bottom)/(height-1)
for y in range(height):
for x in range(width):
pixel = Vector(screen.left+x*deltaX, screen.top-y*deltaY, screen.z)
direction = (pixel - eye).normalize()
pixelRay = Ray(eye, direction)
# Part 1: Diffuse Material
color = Vector(0,0,0)
for s in range(samples):
color += RayColor(objects, pixelRay)
color *= 1.0/samples
#color = Vector(sqrt(color.x), sqrt(color.y), sqrt(color.z))
bitmap[x,y] = (int(color.x*255), int(color.y*255), int(color.z*255))
print("progress: %d %%" % ((y+1)/height*100.0))
#--------------------------------------------------------------
#--------------------------------------------------------------
#--------------------------------------------------------------
img.show()
img.save("pic1.png")
#img.save("images/fig" + f'{frame:06}' + ".png")
#print("Saving ---> images/fig" + f'{frame:06}' + ".png")
#img.close()
main()
util.py file:
from math import *
from abc import ABC, abstractmethod
PI = 3.14159265
INF = 9999999999
#-----------------------------------------------------------------------
class Color:
BLACK = (0,0,0)
RED = (245,23,32)
BLUE = (46,139,192)
GREEN = (24,165,88)
PINK = (250,38,160)
GOLD = (248,210,16)
TEAL = (43,124,133)
WHITE = (255,255,255)
CHROMA= (0,255,0)
#-----------------------------------------------------------------------
def dot(a, b):
return a.x*b.x + a.y*b.y + a.z*b.z
def normalize(a):
mag = a.magnitude()
return Vector(a.x/mag,a.y/mag,a.z/mag)
class Vector:
def __init__(self,x,y,z):
self.x = x
self.y = y
self.z = z
self.w = 1
def toString(self):
return "S:["+"{:.3f}".format(self.x)+", "+"{:.3f}".format(self.y)+", "+"{:.3f}".format(self.z)+"]"
def dot(self, b):
return self.x*b.x + self.y*b.y + self.z*b.z
def cross(self, b):
return Vector(self.y*b.z-self.z*b.y, self.z*b.x-self.x*b.z, self.x*b.y-self.y*b.x)
def magnitude(self):
return sqrt(self.x*self.x + self.y*self.y + self.z*self.z)
def normalize(self):
mag = self.magnitude()
return Vector(self.x/mag, self.y/mag, self.z/mag)
# Provide "overridden methods via the "__operation__" notation; allows you to do, for example, a+b, a-b, a*b
def __add__(self, b):
return Vector(self.x + b.x, self.y+b.y, self.z+b.z)
def __sub__(self, b):
return Vector(self.x-b.x, self.y-b.y, self.z-b.z)
def __mul__(self, b):
if type(b) == float or type(b) == int:
return Vector(self.x*b, self.y*b, self.z*b)
elif type(b) == Vector:
return Vector(self.x*b.x, self.y*b.y, self.z*b.z)
else:
print(type(b))
assert False
class Material:
def __init__(self, ambient, diffuse, specular, shininess = 0.0, reflection = 0.0):
self.ambient = ambient # 3D Vector
self.diffuse = diffuse # 3D Vector
self.specular = specular # 3D Vector
self.shininess = shininess # 0 - 100
self.reflection= reflection # 0.0 - 1.0
class Light:
def __init__(self, position, material):
self.position = position
self.material = material
class Hittable(ABC):
@abstractmethod
def intersect(self, ray):
pass
class Sphere(Hittable):
def __init__(self,center, radius, material):
self.center = center
self.radius = radius
self.material = material
def intersect(self, ray):
OC = ray.origin - self.center
a = ray.direction.dot(ray.direction)
b = 2.0 * ray.direction.dot(OC)
c = OC.dot(OC) - self.radius*self.radius
descriminant = b*b - 4*a*c
if(descriminant > 0):
t1 = (-b + sqrt(descriminant))/2
t2 = (-b - sqrt(descriminant))/2
if t1 > 0 and t2 > 0:
dist = min(t1,t2)
point = ray.pointAtParameter(dist)
normal = (point - self.center).normalize()
return Intersection(point, dist, normal, self)
return Intersection(None, INF, None, None)
def normal(self, b):
return (b - self.center).normalize()
class Ray:
def __init__(self, origin, direction):
self.origin = origin
self.direction = direction
def pointAtParameter(self, t):
return self.origin + self.direction*t
def toString(self):
return "R->{ "+ self.origin.toString() + " -- " + self.direction.toString() +" }"
class Intersection:
def __init__(self, point, distance, normal, object):
self.point = point
self.distance = distance
self.normal = normal
self.object = object
class Screen:
def __init__(self, left, top, right, bottom, z):
self.left = left
self.top = top
self.right = right
self.bottom = bottom
self.z = z
class Camera:
def __init__(self):
pass
```