title: Python raytracing distorts object shapes when close to the camera
body: <p><strong>The Problem</strong></p>
  
  <p>I recently found someone's awesome little pure-Python raytracing script <a href="http://pastebin.com/f8f5ghjz" rel="nofollow noreferrer">from this link</a>, and extended it a little bit for more convenient functions. However, sometimes it distorts the shapes of the objects and I'm wondering if someone with raytracing/3d experience might have any clue as to what might be causing it?</p>
  
  <p><strong>Some Info</strong></p>
  
  <p>The scene I'm testing with consists of a ground-level plane with three colored spheres placed on top of it. It produces good-looking scenes when the camera is looking down on the scene from above/at angles and at a certain distance (see the first two pics); however, when the camera gets closer to the ground level and closer to the objects the spheres end up changing their shapes and becoming oblong as if they're being stretched up towards the sky (see third pic). Note that the camera in the third pic with the distorted spheres is sort of upside down, which is because I'm still figuring out how to control the camera and not sure how to "spin it" upright when that happens; it seems to automatically look towards the general area where the spheres/light source is located, and only if I change some parameters will it look in different directions. </p>
  
  <p><img src="https://i.stack.imgur.com/UXDkv.png" alt="enter image description here"></p>
  
  <p><img src="https://i.stack.imgur.com/iQyW7.png" alt="enter image description here"></p>
  
  <p><img src="https://i.stack.imgur.com/X8Ty6.png" alt="enter image description here"></p>
  
  <p><img src="https://i.stack.imgur.com/ZtZg2.png" alt="enter image description here"></p>
  
  <p>I'm still trying to decipher and understand what goes on in the original code that I found and based my code on, so I don't know but it could be something about the method or approach to raytracing taken by the original author. I've attached the entire code of my module script which should run when you press F5 if anyone is up for the challenge. The image rendering requires PIL, and if you want to play with the position of the camera, just look at the Camera class, and change its options in the "normaltest" function. </p>
  
  <p><strong>Update</strong></p>
  
  <p>Someone pointed out that when running the script it doesn't reproduce the problem in the third image. I have now changed the camera position for the normaltest function so that it will reproduce the problem (see the new fourth image for how it should look like). In case you're wondering why the light seems to be shooting out of the spheres it's bc I placed the lightsource somewhere in between all of them. </p>
  
  <p>Im starting to think that the problem is with the camera and me not understanding it completely. </p>
  
  <ul>
  <li>The camera options zoom, xangle, and yangle may not do what their names imply; that's just how I named them based on what they seemed to do when I changed them up. Originally they were not variables but rather some constant nr in a calculation that had to be changed manually. Specifically they are used to define and produce the rays through the scene on line 218 in the renderScene function.</li>
  <li>For instance, sometimes when I change the zoom value it also changes the direction and position of the camera.</li>
  <li>It's a bit odd that in the original code the camera was just defined as a point with no direction (the xangle and yangle variables were at first just static nrs with no option for defining them), and almost always starts out looking towards the object automatically.</li>
  <li>I cant find a way to "spin"/tilt the camera around itself.</li>
  </ul>
  
  <p>Try also to heighten the camera from its current z-coordinate of 2 to a z of 5, a very small change but it makes the distortion dramatically better looking (though still bad), so proximity to the ground or the shift in angle that comes with it seems to play some role.</p>
  
  <pre><code>"""
  Pure Python ray-tracer :)
  taken directly from http://pastebin.com/f8f5ghjz with modifications
  another good one alternative at http://www.hxa.name/minilight/
  some more equations for getting intersection with other 3d geometries, https://www.cl.cam.ac.uk/teaching/1999/AGraphHCI/SMAG/node2.html#SECTION00023200000000000000
  """
  
  #IMPORTS
  from math import sqrt, pow, pi
  import time
  import PIL,PIL.Image
  
  #GEOMETRIES
  class Vector( object ):
  
      def __init__(self,x,y,z):
          self.x = x
          self.y = y
          self.z = z
  
      def dot(self, b):
          return self.x*b.x + self.y*b.y + self.z*b.z
  
      def cross(self, b):
          return (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**2+self.y**2+self.z**2)
  
      def normal(self):
          mag = self.magnitude()
          return Vector(self.x/mag,self.y/mag,self.z/mag)
  
      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):
          assert type(b) == float or type(b) == int
          return Vector(self.x*b, self.y*b, self.z*b)     
  
  class Sphere( object ):
  
      def __init__(self, center, radius, color):
          self.c = center
          self.r = radius
          self.col = color
  
      def intersection(self, l):
          q = l.d.dot(l.o - self.c)**2 - (l.o - self.c).dot(l.o - self.c) + self.r**2
          if q &lt; 0:
              return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
          else:
              d = -l.d.dot(l.o - self.c)
              d1 = d - sqrt(q)
              d2 = d + sqrt(q)
              if 0 &lt; d1 and ( d1 &lt; d2 or d2 &lt; 0):
                  return Intersection(l.o+l.d*d1, d1, self.normal(l.o+l.d*d1), self)
              elif 0 &lt; d2 and ( d2 &lt; d1 or d1 &lt; 0):
                  return Intersection(l.o+l.d*d2, d2, self.normal(l.o+l.d*d2), self)
              else:
                  return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)    
  
      def normal(self, b):
          return (b - self.c).normal()
  
  class Cylinder( object ):
  
      "just a copy of sphere, needs work. maybe see http://stackoverflow.com/questions/4078401/trying-to-optimize-line-vs-cylinder-intersection"
  
      def __init__(self, startpoint, endpoint, radius, color):
          self.s = startpoint
          self.e = endpoint
          self.r = radius
          self.col = color
  
      def intersection(self, l):
          q = l.d.dot(l.o - self.c)**2 - (l.o - self.c).dot(l.o - self.c) + self.r**2
          if q &lt; 0:
              return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
          else:
              d = -l.d.dot(l.o - self.c)
              d1 = d - sqrt(q)
              d2 = d + sqrt(q)
              if 0 &lt; d1 and ( d1 &lt; d2 or d2 &lt; 0):
                  return Intersection(l.o+l.d*d1, d1, self.normal(l.o+l.d*d1), self)
              elif 0 &lt; d2 and ( d2 &lt; d1 or d1 &lt; 0):
                  return Intersection(l.o+l.d*d2, d2, self.normal(l.o+l.d*d2), self)
              else:
                  return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)    
  
      def normal(self, b):
          return (b - self.c).normal()
  
  class LightBulb( Sphere ):
          pass
  
  class Plane( object ):
      "infinite, no endings"
      def __init__(self, point, normal, color):
          self.n = normal
          self.p = point
          self.col = color
  
      def intersection(self, l):
          d = l.d.dot(self.n)
          if d == 0:
              return Intersection( vector(0,0,0), -1, vector(0,0,0), self)
          else:
              d = (self.p - l.o).dot(self.n) / d
              return Intersection(l.o+l.d*d, d, self.n, self)
  
  class Rectangle( object ):
      "not done. like a plane, but is limited to the shape of a defined rectangle"
      def __init__(self, point, normal, color):
          self.n = normal
          self.p = point
          self.col = color
  
      def intersection(self, ray):
          desti = ray.dest.dot(self.n)
          if desti == 0:
                          #??
              return Intersection( vector(0,0,0), -1, vector(0,0,0), self)
          else:
              desti = (self.p - ray.orig).dot(self.n) / desti
              return Intersection(ray.orig+ray.desti*desti, desti, self.n, self)
  
  class RectangleBox( object ):
          "not done. consists of multiple rectangle objects as its sides"
          pass
  
  class AnimatedObject( object ):
  
          def __init__(self, *objs):
                  self.objs = objs
  
          def __iter__(self):
                  for obj in self.objs:
                          yield obj
  
          def __getitem__(self, index):
                  return self.objs[index]
  
          def reverse(self):
                  self.objs = [each for each in reversed(self.objs)]
                  return self
  
  #RAY TRACING INTERNAL COMPONENTS
  class Ray( object ):
  
      def __init__(self, origin, direction):
          self.o = origin
          self.d = direction
  
  class Intersection( object ):
      "keeps a record of a known intersection bw ray and obj?"
      def __init__(self, point, distance, normal, obj):
          self.p = point
          self.d = distance
          self.n = normal
          self.obj = obj
  
  def testRay(ray, objects, ignore=None):
      intersect = Intersection( Vector(0,0,0), -1, Vector(0,0,0), None)
  
      for obj in objects:
          if obj is not ignore:
              currentIntersect = obj.intersection(ray)
              if currentIntersect.d &gt; 0 and intersect.d &lt; 0:
                  intersect = currentIntersect
              elif 0 &lt; currentIntersect.d &lt; intersect.d:
                  intersect = currentIntersect
      return intersect
  
  def trace(ray, objects, light, maxRecur):
      if maxRecur &lt; 0:
          return (0,0,0)
      intersect = testRay(ray, objects)       
      if intersect.d == -1:
          col = vector(AMBIENT,AMBIENT,AMBIENT)
      elif intersect.n.dot(light - intersect.p) &lt; 0:
          col = intersect.obj.col * AMBIENT
      else:
          lightRay = Ray(intersect.p, (light-intersect.p).normal())
          if testRay(lightRay, objects, intersect.obj).d == -1:
              lightIntensity = 1000.0/(4*pi*(light-intersect.p).magnitude()**2)
              col = intersect.obj.col * max(intersect.n.normal().dot((light - intersect.p).normal()*lightIntensity), AMBIENT)
          else:
              col = intersect.obj.col * AMBIENT
      return col
  
  def gammaCorrection(color,factor):
      return (int(pow(color.x/255.0,factor)*255),
              int(pow(color.y/255.0,factor)*255),
              int(pow(color.z/255.0,factor)*255))
  
  #USER FUNCTIONS
  class Camera:
  
      def __init__(self, cameraPos, zoom=50.0, xangle=-5, yangle=-5):
          self.pos = cameraPos
          self.zoom = zoom
          self.xangle = xangle
          self.yangle = yangle
  
  def renderScene(camera, lightSource, objs, imagedims, savepath):
          imgwidth,imgheight = imagedims
          img = PIL.Image.new("RGB",imagedims)
          #objs.append( LightBulb(lightSource, 0.2, Vector(*white)) )
          print "rendering 3D scene"
          t=time.clock()
          for x in xrange(imgwidth):
                  #print x
                  for y in xrange(imgheight):
                          ray = Ray( camera.pos, (Vector(x/camera.zoom+camera.xangle,y/camera.zoom+camera.yangle,0)-camera.pos).normal())
                          col = trace(ray, objs, lightSource, 10)
                          img.putpixel((x,imgheight-1-y),gammaCorrection(col,GAMMA_CORRECTION))
          print "time taken", time.clock()-t
          img.save(savepath)
  
  def renderAnimation(camera, lightSource, staticobjs, animobjs, imagedims, savepath, saveformat):
          "NOTE: savepath should not have file extension, but saveformat should have a dot"
          time = 0
          while True:
                  print "time",time
                  timesavepath = savepath+"_"+str(time)+saveformat
                  objs = []
                  objs.extend(staticobjs)
                  objs.extend([animobj[time] for animobj in animobjs])
                  renderScene(camera, lightSource, objs, imagedims, timesavepath)
                  time += 1
  
  #SOME LIGHTNING OPTIONS
  AMBIENT = 0.05 #daylight/nighttime
  GAMMA_CORRECTION = 1/2.2 #lightsource strength?
  
  #COLORS
  red = (255,0,0)
  yellow = (255,255,0)
  green = (0,255,0)
  blue = (0,0,255)
  grey = (120,120,120)
  white = (255,255,255)
  purple = (200,0,200)
  
  def origtest():
          print ""
          print "origtest"
          #BUILD THE SCENE
          imagedims = (500,500)
          savepath = "3dscene_orig.png"
          objs = []
          objs.append(Sphere( Vector(-2,0,-10), 2, Vector(*green)))      
          objs.append(Sphere( Vector(2,0,-10), 3.5, Vector(*red)))
          objs.append(Sphere( Vector(0,-4,-10), 3, Vector(*blue)))
          objs.append(Plane( Vector(0,0,-12), Vector(0,0,1), Vector(*grey)))
          lightSource = Vector(0,10,0)
          camera = Camera(Vector(0,0,20))
  
          #RENDER
          renderScene(camera, lightSource, objs, imagedims, savepath)
  
  def normaltest():
          print ""
          print "normaltest"
          #BUILD THE SCENE
          """
          the camera is looking down on the surface with the spheres from above
          the surface is like looking down on the xy axis of the xyz coordinate system
          the light is down there together with the spheres, except from one of the sides
          """
          imagedims = (200,200)
          savepath = "3dscene.png"
          objs = []
          objs.append(Sphere( Vector(-4, -2, 1), 1, Vector(*red)))
          objs.append(Sphere( Vector(-2, -2, 1), 1, Vector(*blue)))
          objs.append(Sphere( Vector(-2, -4, 1), 1, Vector(*green)))
          objs.append(Plane( Vector(0,0,0), Vector(0,0,1), Vector(*grey)))
          lightSource = Vector(-2.4, -3, 2)
          camera = Camera(Vector(-19,-19,2), zoom=2.0, xangle=-30, yangle=-30)
  
          #RENDER
          renderScene(camera, lightSource, objs, imagedims, savepath)
  
  def animtest():
          print ""
          print "falling ball test"
          #BUILD THE SCENE
          imagedims = (200,200)
          savepath = "3d_fallball"
          saveformat = ".png"
          staticobjs = []
          staticobjs.append(Sphere( Vector(-4, -2, 1), 1, Vector(*red)))
          staticobjs.append(Sphere( Vector(-2, -4, 1), 1, Vector(*green)))
          staticobjs.append(Plane( Vector(0,0,0), Vector(0,0,1), Vector(*purple)))
          animobjs = []
          fallingball = AnimatedObject(Sphere( Vector(-2, -2, 20), 1, Vector(*yellow)),
                                       Sphere( Vector(-2, -2, 15), 1, Vector(*yellow)),
                                       Sphere( Vector(-2, -2, 9), 1, Vector(*yellow)),
                                       Sphere( Vector(-2, -2, 5), 1, Vector(*yellow)),
                                       Sphere( Vector(-2, -2, 1), 1, Vector(*yellow)))
          animobjs.append(fallingball)
          lightSource = Vector(-4,-4,10)
          camera = Camera(Vector(0,0,30))
  
          #RENDER
          renderAnimation(camera, lightSource, staticobjs, animobjs, imagedims, savepath, saveformat)
  
  #RUN TESTS
  #origtest()
  normaltest()
  #animtest()
  </code></pre>