Sophie

Sophie

distrib > Mandriva > 2010.2 > i586 > media > contrib-backports > by-pkgid > df29c83ca401d91ec9c00bfcf7fea4ea > files > 211

shedskin-0.8-2mdv2010.2.i586.rpm

# Copyright 2010 Eric Uhrhane.
#
# This file is part of Pylot.
#
# Pylot is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pylot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Foobar.  If not, see <http://www.gnu.org/licenses/>.

import math

from Utils import Roughly #, debug_print
#{
from Utils import add_debug_ray
#}
import Color
from Ray import Ray
from Vector4 import Vector4, fresnel_reflectance_at_angle
from World import World
from Shape import Sphere, ParallelogramAt, rayHitsPlane
import Material

MAX_GENERATIONS = 10

# Later we'll have a subclass of Camera for each of raytracing, pathtracing,
# etc.  If possible, we'll keep all the differences here.
class Camera(object):
  def __init__(self, world, eye, target, up, distance,
               w, h, cols, rows, ortho=False):
#{
    assert type(world) == World
    assert type(eye) == Vector4
    assert type(target) == Vector4
    assert type(up) == Vector4
#}
    assert eye.w == 1
    assert target.w == 1
    self.world = world
    if not up.w: # Up can be a vector or a point.
      up = up + eye
    assert up.w == 1 # Use points for all three.
    self.eye = eye
    self.direction = (target - eye).normalize()
    self.up = -(up-eye).cross(self.direction).cross(self.direction).normalize()
    assert w > 0
    assert h > 0
    assert distance > 0
    self.distance = distance
    self.w = float(w)
    self.h = float(h)
    self.cols = cols
    self.rows = rows
    self.left = self.up.cross(self.direction)
    self.screenCenter = self.eye + self.direction.scale(self.distance)
    self.screenTopLeft = self.screenCenter + self.up.scale(self.h / 2) + \
                                        self.left.scale(self.w / 2)
    self.xIncrement = self.left.scale(-self.w / float(self.cols))
    self.yIncrement = self.up.scale(-self.h / float(self.rows))
    self.ortho = ortho

    screenBottomRight = self.screenTopLeft + \
                        self.xIncrement.scale(self.cols - 0.5) + \
                        self.yIncrement.scale(self.rows - 0.5)

  def __repr__(self):
    s = ("eye %s; direction %s; up %s; dist %f; w %f; h %f; cols %d; " +
         "rows %d; ortho %s") % (repr(self.eye), repr(self.direction),
                                 repr(self.up), self.distance, self.w, self.h,
                                 self.cols, self.rows, repr(self.ortho))
    return s

  def getDirectLighting(self, ray, hitLocation, lights, shape, surfaceNormal):
    material = shape.material
    color = Color.BLACK
    for light in lights:
      if light is not shape:
        lightDirection = (light.getLocation() - hitLocation).normalize()
        lightDotSurfaceNormal = lightDirection.dot(surfaceNormal)
        if lightDotSurfaceNormal > 0:
          lightHit = self.world.intersect(Ray(hitLocation, lightDirection),
                                          ignore=shape)
          # intersect *should* never miss, but...
          if lightHit.hit and lightHit.shape is light:
            lightReflection = -lightDirection.reflect(surfaceNormal)
            highlight = math.pow(lightReflection.dot(-ray.offset), 20)
            intensity = light.getIntensity(hitLocation)
            color += (material.color * light.material.emissive).scale(
                lightDotSurfaceNormal * intensity)
            color += (light.material.emissive * material.specular).scale(
                highlight * intensity)
#            debug_print("light is " + repr(light))
#            debug_print("got direct lighting color " + repr(color))
    return color

  def getLightingFromSecondaryRay(self, ray, lights, generation, strength,
                                  materialStack, ignore=None, inside=None):
#    debug_print("In getLightingFromSecondaryRay(%d)" % generation)
    insideMaterial = materialStack[-1]
    hit = self.world.intersect(ray, ignore=ignore, inside=inside,
                               insideMaterial=insideMaterial)
    color = Color.BLACK
    if hit.hit:
#      add_debug_ray(Ray(ray.origin, ray.offset.scale(hit.distance)), "red")
      if insideMaterial.attenuation_distances:
        # Here we lower strength due to absorbtion in insideMaterial in the
        # hit.distance traveled.  We attenuate by 50% for each unit of distance,
        # where the unit length is a property of the material.  So
        #   strength *= pow(0.5, (distance / attenuation_distance))
        # We compute a custom attenuator per component.
        attenuators = []
        for d in insideMaterial.attenuation_distances.toList():
          if d:
            p = math.pow(0.5, hit.distance / d)
          else:
            p = 1.0
          attenuators.append(p)
#        debug_print("attenuators is " + repr(attenuators))
#        debug_print("strength was " + repr(strength))
        strength *= Color.fromList(attenuators)
#        debug_print("strength is " + repr(strength))
        # Too dim to matter after attenuation?
        if sum(strength.toList()) < 0.01:
#          debug_print("Out early getLightingFromSecondaryRay(%d)" % generation)
          return color
      color = self.getLighting(ray, hit, lights,
                               generation=generation+1,
                               strength=strength,
                               materialStack=materialStack)
#    debug_print("returning color " + repr(color))
#    debug_print("Out getLightingFromSecondaryRay(%d)" % generation)
    return color

  def getReflectedLighting(self, ray, hit, hitLocation, lights, shape,
                           surfaceNormal, materialStack, strength, generation):
#    debug_print("In getReflectedLighting(%d)" % generation)
    material = shape.material
    reflection = Ray(hitLocation, ray.offset.reflect(surfaceNormal))
    color = self.getLightingFromSecondaryRay(reflection, lights, generation,
                                             strength, materialStack,
                                             ignore=shape)
#    debug_print("Reflected color is " + repr(color))
#    debug_print("Out getReflectedLighting(%d)" % generation)
    return color

  def getRefractedLighting(self, ray, hit, hitLocation, lights, shape,
                           surfaceNormal, materialStack, fromMaterial,
                           toMaterial, insideShape, strength, generation):
#    debug_print("In getRefractedLighting(%d)" % generation)
#    debug_print("strength is " + repr(strength))
#    debug_print("shape is " + repr(shape))

    if fromMaterial == toMaterial:
      refractionVector = ray.offset
    else:
      refractionVector = ray.offset.refract(surfaceNormal,
                                            fromMaterial.indexOfRefraction,
                                            toMaterial.indexOfRefraction)
    color = Color.BLACK
    if refractionVector:
      # Ideally this would never fail, since we check the strength of the
      # refracted ray using fresnel_reflectance_at_angle before getting here.
      # But it's possible that, due to floating point errors, this disagrees
      # about whether we've hit total internal reflection or not.
      refraction = Ray(hitLocation, refractionVector)
#      debug_print("Refracted ray is " + repr(refraction))
      color = self.getLightingFromSecondaryRay(refraction, lights, generation,
                                               strength, materialStack,
                                               inside=insideShape)
#    debug_print("Out getRefractedLighting(%d)" % generation)
    return color

  # By default this assumes that the camera is always in free space, not inside
  # an object with a material other than EMPTY.  If you want to start out under
  # water, in glass, etc., you'll have to set up the materialStack
  # appropriately.
  def getLighting(self, ray, hit, lights, generation=1, strength=None,
                  materialStack=None):
#    debug_print("In getLighting(%d)" % generation)
    assert Roughly(ray.offset.length(), 1)
    shape = hit.shape
    if not materialStack:
      assert(not hit.inverted)
      materialStack = [Material.EMPTY]
    fromMaterial = materialStack[-1]
    if hit.inverted:
      assert len(materialStack) > 1
      refractMaterialStack = materialStack[:-1]
      insideShape = None
    else:
      refractMaterialStack = materialStack + [shape.material]
      insideShape = shape
      # insideShape is the shape we just entered.  This is a hack for spheres,
      # so that we know to detect the refracted ray hitting the inside of the
      # shape without bouncing off anything else first.  All other shapes are
      # currently planar, so this won't affect them.
    toMaterial = refractMaterialStack[-1]
    if not strength:
      strength = Color.WHITE

    if not hit.inverted and toMaterial.emissive:
      color = toMaterial.emissive * strength
    else:
      color = Color.BLACK
    hitLocation = ray.origin + ray.offset.scale(hit.distance)
    surfaceNormal = shape.getNormal(hitLocation)

#    debug_print("Before getDirectLighting, strength is " + repr(strength))
#    debug_print("Before getDirectLighting, color is " + repr(color))
    temp = self.getDirectLighting(ray, hitLocation, lights, shape,
                                  surfaceNormal)
#    debug_print("getDirectLighting returned" + repr(temp))
#    debug_print("product is " + repr(temp * strength))
    color += temp * strength
#    debug_print("After getDirectLighting, strength is " + repr(strength))
#    debug_print("After getDirectLighting, color is " + repr(color))
    if generation < MAX_GENERATIONS:
      if toMaterial.attenuation_distances: # Refraction is possible.
        # Reflection:
        # The cosine of the incident angle Ti is the ratio of adjacent /
        # hypotenuse, which here is |normal.incident| / incident, where incident
        # is of length 1.
        Ti = math.acos(math.fabs(surfaceNormal.dot(ray.offset)))
        proportionReflected = fresnel_reflectance_at_angle(
            Ti, fromMaterial.indexOfRefraction, toMaterial.indexOfRefraction)
        # We use the product of fromMaterial.reflective and
        # toMaterial.reflective so that reflections into and out of objects
        # work.  It's sort of arbitrary, but reflecting off the inside of a
        # glass object shouldn't just use toMaterial [empty space], and bouncing
        # off, say, glass under water might be affected by attributes of both.
        # If you don't set reflective on a transparent material, it'll all just
        # work with no reduction in strength beyond the Fresnel value anyway.
        reflectedStrength = strength.scale(proportionReflected) * \
            fromMaterial.reflective * toMaterial.reflective
        transmittedStrength = strength.scale(1 - proportionReflected)
      else:
        reflectedStrength = \
            strength * fromMaterial.reflective * toMaterial.reflective

      if sum(reflectedStrength.toList()) > 0.01:
        # strong enough to matter
        color += self.getReflectedLighting(
            ray, hit, hitLocation, lights, shape, surfaceNormal, materialStack,
            reflectedStrength, generation)
      # Refraction:
      if (toMaterial.attenuation_distances) and \
          sum(transmittedStrength.toList()) > 0.01:
        color += self.getRefractedLighting(ray, hit, hitLocation, lights,
                                           shape, surfaceNormal,
                                           refractMaterialStack, fromMaterial,
                                           toMaterial, insideShape,
                                           transmittedStrength, generation)
#    debug_print("Out getLighting(%d)" % generation)
    return color

  def runPixel(self, pixel, lights):
    if self.ortho:
      ray = Ray(pixel, self.direction)
    else:
      ray = Ray(self.eye, (pixel - self.eye).normalize())
    hit = self.world.intersect(ray)
    if hit.hit:
      return self.getLighting(ray, hit, lights).toImageColor()
    else:
      return Color.BLACK.toImageColor()

  def pixelForCoords(self, i, j):
    ret = self.screenTopLeft + self.xIncrement.scale(i - 0.5) + \
                               self.yIncrement.scale(j - 0.5)
    return ret

  def runPixelRange(self, r):
    lights = self.world.lightSources()
    (startCol, endCol), (startRow, endRow) = r
    return (r, 
        ''.join([self.runPixel(self.pixelForCoords(i, j), lights)
                 for j in range(startRow, endRow)
                 for i in range(startCol, endCol)]))

  # For raytracing, once you've got a hit, shoot rays at all light sources
  # [Shapes with emissive color set].  Skip any who are behind the surface hit
  # [dot with normal and check the sign], and tell world.intersect to ignore our
  # surface, to avoid glitches in which we self-shadow.  If the light source is
  # the first thing the ray hits, gather light from it to determine our local
  # diffuse lighting.  Add in local emissive lighting.  Later also spawn
  # reflection+transmission rays to add in those components of lighting as well.
  def runImage(self):
    return self.runPixelRange(((0, self.cols), (0, self.rows)))

  def mapPointToScreen(self, point):
    if self.ortho:
      direction = self.direction
    else:
      diff = point - self.eye
      if Roughly(diff.length_2(), 0):
        return None
      direction = diff.normalize()
    ray = Ray(self.eye, direction)
    hit = rayHitsPlane(-self.direction, self.screenCenter, ray)
    if hit.hit:
      assert not hit.inverted
    # TODO: This won't map points behind the eye.
      location = ray.origin + ray.offset.scale(hit.distance)
      offset = self.screenTopLeft - location
      distanceFromLeft = offset.dot(self.left)
      distanceFromTop = offset.dot(self.up)
      pixel = (int(distanceFromLeft / self.w * float(self.cols)),
               int(distanceFromTop / self.h * float(self.rows)))
      return pixel
    return None