Advertisement
Geometrian

Forward/Backward 2D Python Raytracer

Sep 18th, 2014
480
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.43 KB | None | 0 0
  1. import pygame
  2. import pygame.gfxdraw
  3. from pygame.locals import *
  4. import sys, os, traceback
  5. from math import *
  6. import random
  7. if sys.platform in ["win32","win64"]: os.environ["SDL_VIDEO_CENTERED"]="1"
  8. pygame.display.init()
  9. pygame.font.init()
  10.  
  11. fonts = [
  12.     pygame.font.SysFont("Times New Roman",12),
  13.     pygame.font.SysFont("Times New Roman",16)
  14. ]
  15.  
  16. screen_size = [800,400]
  17. icon = pygame.Surface((1,1)); icon.set_alpha(0); pygame.display.set_icon(icon)
  18. pygame.display.set_caption("Adjunct Raytracer - Ian Mallett - v.1.0.0 - 2014")
  19. surface = pygame.display.set_mode(screen_size)
  20.  
  21. #1 == no refraction; thin lens only
  22. #2 == refraction through thin lens
  23. REFRACT_MODE = 2
  24. #Focal length of lens (pixels) (only matters if REFRACT_MODE==2)
  25. FOCAL = 50.0
  26. #Aperture size (pixels)
  27. APERTURE = 100
  28.  
  29. #Color of scene geometry
  30. COLOR_GEOM = (255,128,0)
  31. #Color of path tracing rays
  32. COLOR_PT_HIT = (0,255,0)
  33. COLOR_PT_MISS = (255,0,0)
  34. #Color of light tracing rays
  35. COLOR_LT_HIT = (0,0,255)
  36. COLOR_LT_MISS = (255,0,255)
  37.  
  38. def rndint(x): return int(round(x))
  39. def dot(u,v): return u[0]*v[0] + u[1]*v[1]
  40. def perp(u,v): return u[0]*v[1] - u[1]*v[0] #perp product (2D)
  41. def sc(sc,vec): return sc*vec[0], sc*vec[1]
  42. def add(vec0,vec1): return vec0[0]+vec1[0], vec0[1]+vec1[1]
  43. def sub(vec0,vec1): return vec0[0]-vec1[0], vec0[1]-vec1[1]
  44. def lengthsq(vec): return dot(vec,vec)
  45. def length(vec): return lengthsq(vec) ** 0.5
  46. def normalized(vec): return sc(1.0/length(vec),vec)
  47. def rand_semi(normal):
  48.     angle = random.random() * 2.0*pi
  49.     vec = (cos(angle),sin(angle))
  50.     if dot(vec,normal)<0.0:
  51.         return (-vec[0],-vec[1]), 1.0/(1.0*pi)
  52.     else:
  53.         return vec, 1.0/(1.0*pi)
  54. def intersect_ray_circle(ray,circle):
  55.     omc = [ray.x-circle.x,ray.y-circle.y]
  56.     l_dot_omc = dot((ray.dx,ray.dy),omc)
  57.     discr = l_dot_omc*l_dot_omc - lengthsq(omc) + circle.radius*circle.radius
  58.     if discr<=0.0: return False,-1,-1
  59.     discr_sqrt = discr ** 0.5
  60.     d0,d1 = -l_dot_omc-discr_sqrt,-l_dot_omc+discr_sqrt
  61.     return d0>0,d0,d1
  62. def intersect_line_line(p0,p1, p2,p3):
  63.     u = sub(p1,p0)
  64.     v = sub(p3,p2)
  65.     w = sub(p0,p2)
  66.     D = perp(u,v)
  67.  
  68.     sI = perp(v,w) / D;
  69.     if sI<0 or sI>1: return False,None
  70.     tI = perp(u,w) / D;
  71.     if tI<0 or tI>1: return False,None
  72.    
  73.     return True,add(p0,sc(sI,u))
  74. def intersect_ray_line(ray,line):
  75.     hit,pt = intersect_line_line(
  76.         (ray.x,ray.y), (ray.x+10000.0*ray.dx,ray.y+10000.0*ray.dy),
  77.         line.get_p0(), line.get_p1()
  78.     )
  79.     if hit:
  80.         return True,length(sub(pt,(ray.x,ray.y)))
  81.     else:
  82.         return False,-1
  83.  
  84. class ObjectBase(object):
  85.     def __init__(self, x,y):
  86.         self.x = x
  87.         self.y = y
  88.     @staticmethod
  89.     def to_screen(x,y):
  90.         return rndint(x+screen_size[0]*0.5),rndint(screen_size[1]-(y+screen_size[1]*0.5))
  91.     def draw(self): pass
  92. class ObjectLineBase(ObjectBase):
  93.     def __init__(self, x,y, size):
  94.         ObjectBase.__init__(self, x,y)
  95.         self.size = size
  96.     def get_p0(self):
  97.         return self.x, self.y-self.size*0.5
  98.     def get_p1(self):
  99.         return self.x, self.y+self.size*0.5
  100.     def draw(self):
  101.         pygame.draw.aaline(
  102.             surface, COLOR_GEOM,
  103.             ObjectBase.to_screen(*self.get_p0()), ObjectBase.to_screen(*self.get_p1())
  104.         )
  105. class ObjectSensor(ObjectLineBase): #a line
  106.     def __init__(self, x,y, size):
  107.         ObjectLineBase.__init__(self, x,y, size)
  108.     def get_random(self):
  109.         x,y = self.x,self.y+(random.random()-0.5)*self.size
  110.         pdf_pos = 1.0 / self.size
  111.         vec,pdf_dir = rand_semi((1,0))
  112.         pdf = pdf_pos * pdf_dir
  113.         return Ray(x,y,vec[0],vec[1]),(1,0),pdf
  114.     def draw(self):
  115.         ObjectLineBase.draw(self)
  116.         surface.blit(fonts[0].render("sensor",True,(0,0,0)),ObjectBase.to_screen(self.x-10,self.y+self.size//2+25))
  117. class ObjectLens(ObjectLineBase): #a line
  118.     def __init__(self, x,y, size, f):
  119.         ObjectLineBase.__init__(self, x,y, size)
  120.         self.size = size
  121.         self.f = f
  122.     def get_refracted(self, incoming_ray,hit_distance):
  123.         lensx,lensy = incoming_ray.at(hit_distance)
  124.         if   REFRACT_MODE == 1:
  125.             #Do nothing (no refraction; just an aperture)
  126.             return Ray(lensx,lensy, incoming_ray.dx,incoming_ray.dy)
  127.         elif REFRACT_MODE == 2:
  128.             #Thin lens
  129.             #   Solve by considering the ray's origin to be an "object", finding the "image" point that focuses to it,
  130.             #       then the refracted ray will start at the hit position on the lens and pass through the image point.
  131.             o = abs(self.x - incoming_ray.x)
  132.             lens_to_obj = sub((incoming_ray.x,incoming_ray.y),(self.x,self.y))
  133.             lens_to_objn = normalized(lens_to_obj)
  134.             if o == self.f:
  135.                 #Object is exactly at the focal length; rays are parallel and no finite image is formed.
  136.                 d = (-lens_to_objn[0],-lens_to_objn[1])
  137.             else:
  138.                 #Thin lens equation
  139.                 i = 1.0 / (1.0/self.f - 1.0/o)
  140.                 if o < self.f:
  141.                     #Object is inside the focal length; rays diverge and virtual image is behind object on the same
  142.                     #   side of the lens.
  143.                     image = add( sc(-i/o,lens_to_obj), (self.x,self.y) )
  144.                     d = normalized(sub( (lensx,lensy), image ))
  145.                 else:
  146.                     #Object is outside the focal length; rays converge and real image is on the other side of the lens.
  147.                     image = add( sc(i/-o,lens_to_obj), (self.x,self.y) )
  148.                     d = normalized(sub( image, (lensx,lensy) ))
  149.             return Ray(lensx,lensy, d[0],d[1])
  150.     def draw(self):
  151.         ObjectLineBase.draw(self)
  152.         if REFRACT_MODE == 2:
  153.             pygame.draw.circle(surface,COLOR_GEOM,ObjectBase.to_screen(self.x-self.f,self.y),2)
  154.             pygame.draw.circle(surface,COLOR_GEOM,ObjectBase.to_screen(self.x+self.f,self.y),2)
  155.         surface.blit(fonts[0].render("lens",True,(0,0,0)),ObjectBase.to_screen(self.x-10,self.y+self.size//2+20))
  156. class ObjectLight(ObjectBase): #a circle
  157.     def __init__(self, x,y,radius, radiance):
  158.         ObjectBase.__init__(self, x,y)
  159.         self.radius = radius
  160.         self.radiance = radiance
  161.     def get_random(self):
  162.         angle = random.random() * 2*pi
  163.         nx,ny = cos(angle),sin(angle)
  164.         pdf_pos = 1.0 / (2.0*pi*self.radius)
  165.         vec,pdf_dir = rand_semi((nx,ny))
  166.         pdf = pdf_pos * pdf_dir
  167.         return Ray(self.x+self.radius*nx,self.y+self.radius*ny,vec[0],vec[1]),(nx,ny),pdf
  168.     def draw(self):
  169.         sx,sy = ObjectBase.to_screen(self.x,self.y)
  170.         #pygame.draw.circle(surface, COLOR_GEOM, (sx,sy), self.radius,1)
  171.         pygame.gfxdraw.aacircle(surface, sx,sy, self.radius, COLOR_GEOM)
  172.         surface.blit(fonts[0].render("light",True,(0,0,0)),(sx-10,sy-6))
  173. class Ray(ObjectBase):
  174.     def __init__(self, x,y,dx,dy):
  175.         ObjectBase.__init__(self, x,y)
  176.         self.dx = dx
  177.         self.dy = dy
  178.     def at(self, t):
  179.         return self.x+self.dx*t, self.y+self.dy*t
  180.     def draw(self,t,color):
  181.         pygame.draw.aaline(
  182.             surface, color,
  183.             ObjectBase.to_screen(self.x,self.y), ObjectBase.to_screen(*self.at(t))
  184.         )
  185.        
  186. img = ObjectSensor(-300,0,100)
  187. lns = ObjectLens(-200,0,APERTURE, FOCAL)
  188. lgt = ObjectLight(200,0,50, 1.0)
  189.  
  190. def path_trace():
  191.     #Get random ray from image plane
  192.     sensor_ray,normal,pdf = img.get_random()
  193.     hit,d_1 = intersect_ray_line(sensor_ray,lns)
  194.     if not hit: #didn't hit the lens
  195.         sensor_ray.draw(10.0, COLOR_PT_MISS)
  196.         return 0.0 #monte carlo estimate for flux is zero since radiance is zero
  197.     #If the sensor ray hit the lens, refract it
  198.     lens_ray = lns.get_refracted(sensor_ray,d_1)    
  199.     hit,d_2,d_3 = intersect_ray_circle(lens_ray,lgt)
  200.     if not hit:
  201.         sensor_ray.draw(d_1, COLOR_PT_HIT)
  202.         lens_ray.draw(10.0, COLOR_PT_MISS)
  203.         return 0.0 #monte carlo estimate for flux is zero since radiance is zero
  204.     #The refracted sensor ray hit the light
  205.     sensor_ray.draw(d_1, COLOR_PT_HIT)
  206.     lens_ray.draw(d_2, COLOR_PT_HIT)
  207.     #   Note: sensor_ray.dx is img's normal==(1,0) dotted with sensor_ray.dir==(sensor_ray.dx,sensor_ray.dy)
  208.     #   Note: 1.0 is the importance (self-importance of sensor)
  209.     return lgt.radiance * 1.0 * sensor_ray.dx / pdf
  210. def light_trace():
  211.     #Get a random ray from the light
  212.     light_ray,normal,pdf = lgt.get_random()
  213.     hit,d_1 = intersect_ray_line(light_ray,lns)
  214.     if not hit:
  215.         light_ray.draw(10.0, COLOR_LT_MISS)
  216.         return 0.0 #monte carlo estimate for flux is zero since importance is zero
  217.     #If the light ray hit the lens, refract it
  218.     lens_ray = lns.get_refracted(light_ray,d_1)    
  219.     hit,d_2 = intersect_ray_line(lens_ray,img)
  220.     if not hit:
  221.         light_ray.draw(d_1, COLOR_LT_HIT)
  222.         lens_ray.draw(10.0, COLOR_LT_MISS)
  223.         return 0.0 #monte carlo estimate for flux is zero since importance is zero
  224.     #The refracted light ray hit the sensor
  225.     light_ray.draw(d_1, COLOR_LT_HIT)
  226.     lens_ray.draw(d_2, COLOR_LT_HIT)
  227.     return lgt.radiance * 1.0 * dot(normal,(light_ray.dx,light_ray.dy)) / pdf
  228.  
  229. accum = [0.0,0.0] #estimate from eye, estimate from light
  230. n = 0
  231. def update_and_draw():
  232.     global accum, n
  233.     surface.fill((255,255,255))
  234.  
  235.     N = 100
  236.     #Trace from eye, drawing paths: path tracing
  237.     for i in range(N):
  238.         mc_flux = path_trace()
  239.         accum[0] += mc_flux
  240.     #Trace from light, drawing paths: light tracing
  241.     for i in range(N):
  242.         mc_flux = light_trace()
  243.         accum[1] += mc_flux
  244.     n += N
  245.  
  246.     #Draw scene
  247.     img.draw()
  248.     lns.draw()
  249.     lgt.draw()
  250.  
  251.     #Draw readout(s)
  252.     surface.blit(fonts[1].render(u"\u03a6 (eye)  = " +str(accum[0]/n),True,(0,0,0)),(20,20))
  253.     surface.blit(fonts[1].render(u"\u03a6 (light) = "+str(accum[1]/n),True,(0,0,0)),(20,40))
  254.     surface.blit(fonts[1].render("n = "+str(n),True,(0,0,0)),(20,60))
  255.    
  256.     pygame.display.flip()
  257.  
  258. def main():
  259.     clock = pygame.time.Clock()
  260.     continuing = True
  261.     while continuing:
  262.         for event in pygame.event.get():
  263.             if   event.type == QUIT: continuing=False
  264.             elif event.type == KEYDOWN:
  265.                 if   event.key == K_ESCAPE: continuing=False
  266.         update_and_draw()
  267.         clock.tick(60)
  268.     pygame.quit()
  269. if __name__ == "__main__":
  270.     try:
  271.         main()
  272.     except:
  273.         traceback.print_exc()
  274.         pygame.quit()
  275.         input()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement