A Basic Raytracer in quasi Python :-)
Published: 2020-11-10 19:31:53

RapydRay is a basic raytracer in [RapydScript](https://github.com/atsepkov/RapydScript).
You can play with it in real time here [RapydRay](http://salvatore.pythonanywhere.com/RapydRay)


```python
#
# Based on:
#http://viola.informatik.uni-bremen.de/typo/html/qingteng/index.html
 
# Documentation :
# http://www.scratchapixel.com
 
# Thanks to Alexander Tsepkov the creator of RapydScript, and
# Charles Law for allowing testing RapydScript online
# Post comments and suggestions on RapydScript Google Group
 
# You can modify the script and see the modifications
# in direct live (quite)
# When modifying you can reduce WIDTH and HEIGHT to avoid lag

# Authored by Salvatore DI DIO (email : salvatore dot didio at gmail)

# Best performance with Firefox
# On my mac book pro : 1280 x 800 rendered in less than a minute

WIDTH = 320
HEIGHT = 240
          
class Vector:
    def __init__(self, x=0, y=0, z=0):
        self.x = x
        self.y = y
        self.z = z

    def copy(self):
        return Vector(self.x, self.y, self.z)
     
    def length(self):
        return Math.sqrt(self.x*self.x + self.y*self.y + self.z*self.z)
    
    def sqrLength(self):
        return (self.x*self.x + self.y*self.y + self.z*self.z)
        
    def normalize(self):
        inv = 1.0/self.length()
        return Vector(self.x*inv, self.y*inv,self.z*inv)
    
    def negate(self):
        return Vector(-self.x, -self.y, -self.z)
        
    def add(self,v):
        return Vector(self.x + v.x, self.y + v.y, self.z + v.z)
               
    def sub(self,v):
        return Vector(self.x - v.x, self.y - v.y, self.z - v.z)
        
    def times(self, k):
        return Vector(k*self.x, k*self.y, k*self.z)
        
    def dot(self,t):
        return self.x*t.x + self.y*t.y+self.z*t.z
          
    def cross(self,w):
        v = Vector()
        v.x =  self.y * w.z - self.z * w.y
        v.y = -self.x * w.z + self.z * w.x
        v.z = self.x * w.y - self.y * w.x
        return v
    
    @staticmethod    
    def zero():
        return Vector()
              
class Ray:
    def __init__(self, vectOrigin, vectDest):
        self.origin = vectOrigin
        self.direction = vectDest
        
    def getPoint(self, t):
        # p = self.origin + t * self.direction
        return self.origin.add(self.direction.times(t))
       
class IntersectionResult:
    def __init__(self):
        self.sceneobject = null
        self.distance = 0
        self.position = Vector.zero()
        self.normal = Vector.zero()
        
    @staticmethod    
    def nohit():
        return IntersectionResult()
    
class Sphere:
    def __init__(self, center = Vector(0,2,-1), radius = 1):
        self.center = center
        self.radius = radius
        self.name = "Sphere"
        
    def initialize(self):
        self.sqrRadius = self.radius * self.radius
    
    def double(self):
        return 100
    
    def intersect(self, ray):    
        v = ray.origin.sub(self.center)
        a0 = v.sqrLength() - self.sqrRadius
        DdotV = ray.direction.dot(v)
        
        if DdotV <= 0: 
            discr = DdotV * DdotV - a0   
            if discr >= 0:
                r = IntersectionResult()
                r.sceneobject = self
                r.distance =  -DdotV - Math.sqrt(discr)
                r.position = ray.getPoint(r.distance)
                r.normal =  r.position.sub(self.center)
                return r
        return IntersectionResult.nohit()

class Camera:
    def __init__(self,eye, front, up, fov,aspectratio):
        self.eye = eye
        self.front = front 
        self.RefUp = up
        self.fov = fov
        self.aspectratio = aspectratio
           
    def initialize(self):
        self.right =  self.front.cross(self.RefUp)
        self.up = self.right.cross(self.front)
        self.fovScale = Math.tan(self.fov * 0.5 * Math.PI/180)*2
        
    def generateRay(self,x,y):
        r = self.right.times((x - 0.5) * self.fovScale*self.aspectratio)
        u = self.up.times((y - 0.5) * self.fovScale)
        ray = Ray(self.eye, self.front.add(r).add(u).normalize())
        return  ray
         
class Union:
    def __init__(self,geometries):
        self.geometries = geometries
        
    def initialize(self):
        for geo in self.geometries:
            geo.initialize()
            
    def intersect(self, ray):
        minDistance = Infinity
        minResult = IntersectionResult()
        for geo in self.geometries:
            result = geo.intersect(ray)
            if ((result.sceneobject != null) and (result.distance < minDistance)):        
                minDistance = result.distance
                minResult = result
        return minResult
                  
class Plane:
    def __init__(self, normal, distance):
        self.normal = normal
        self.d = distance
        self.name = "Plane"
        
    def initialize(self):
        self.position = self.normal.times(self.d)
        
    def intersect(self, ray):
        a =  ray.direction.dot(self.normal)
        if a >= 0:
            return IntersectionResult.nohit()
        
        b = self.normal.dot(ray.origin.sub(self.position))
        result = IntersectionResult()
        result.sceneobject = self
        result.distance = -b/a
        result.position =  ray.getPoint(result.distance)
        result.normal = self.normal
        return result
        
class Color:
   def __init__(self, r=0, g=0, b=0):
       self.r = r
       self.g = g
       self.b = b
       
   def add(self,c):
       return Color(self.r + c.r, self.g + c.g, self.b + c.b)
       
   def times(self, k):
       return Color(k*self.r, k*self.g, k*self.b)
       
   def modulate(self, c):
       return Color(self.r * c.r, self.g * c.g, self.b * c.b)
        
Color.black = Color()
Color.red = Color(1,0,0)
Color.white = Color(1,1,1)
Color.green = Color(0,1,0)
Color.blue = Color(0,0,1)
Color.gold = Color(1,0.8746,0)
Color.cyan = Color(0,1,1)

class CheckerMaterial:
    def __init__(self, scale, reflectiveness):
        self.scale = scale
        self.reflectiveness = reflectiveness
  
    def sample(self,ray,position, normal):
        v =  Math.abs((Math.floor(position.x * 0.1) \
           + Math.floor(position.z * self.scale)) % 2)
        if v < 1:
            return Color.black
        else:
            return Color.white

lightDir =  Vector(6, 8.5, 15).normalize()
lightColor = Color(1,1,1)
        
class PhongMaterial:
    def __init__(self,diffuse, specular, shininess, reflectiveness):
        self.diffuse = diffuse
        self.specular = specular
        self.shininess = shininess
        self.reflectiveness = reflectiveness
        
    def sample(self,ray,position,normal):
        NdotL = normal.dot(lightDir)
        H = (lightDir.sub(ray.direction)).normalize()
        NdotH = normal.dot(H)
        diffuseTerm = self.diffuse.times(Math.max(NdotL, 0))        
        specularTerm =  self.specular.times(Math.pow(Math.max(NdotH, 0), self.shininess))
        return lightColor.modulate(diffuseTerm.add(specularTerm))
    
def rayTraceRecursive(scene, ray, maxReflect):
    result = scene.intersect(ray)
    if result.sceneobject:
        reflectiveness = result.sceneobject.material.reflectiveness
        mat = result.sceneobject.material
        color = mat.sample(ray, result.position,result.normal)
        color.times(1 - reflectiveness)
        if (reflectiveness > 0 and maxReflect > 0):
            r = result.normal.times(-2 * result.normal.dot(ray.direction)).add(ray.direction)
            ray = Ray(result.position, r)
            reflectedColor = rayTraceRecursive(scene, ray, maxReflect - 1)
            color = color.add(reflectedColor.times(reflectiveness))
        return color
    return Color(0,0,0)

def main():
    canvas = document.getElementById("canvas")
    ctx = canvas.getContext("2d")
    canvas.width = WIDTH
    canvas.height = HEIGHT    
    w  = canvas.width
    h = canvas.height
    ctx.fillStyle = "rgb(255,255,255)"
    ctx.fillRect(0, 0, w, h)
    
    aspectratio = w/h
    camera = Camera(Vector(0,0.56,8),Vector(0,0,-1),Vector(0,1,0),30,aspectratio)
    camera.initialize()
    
    sphere = Sphere(Vector(1.5,0,-1),1)
    sphere.material = PhongMaterial(Color.red, Color.white, 500,0.25)
    
    sphere2 = Sphere(Vector(-1.5,0,-1),1)
    sphere2.material = PhongMaterial(Color.gold, Color.white, 500,0.25)
    
    sphere3 = Sphere(Vector(0,0,-2),1)
    sphere3.material = PhongMaterial(Color.blue, Color.white, 500,0.25)    
    
    plane = Plane(Vector(0,1,0),-1)
    #plane.material = new CheckerMaterial(0.1,0.9);
    plane.material =   PhongMaterial(Color.cyan, Color.white, 500,1.0)   
    
    scene = Union([plane,sphere,sphere2,sphere3])
    scene.initialize()
    
    maxReflection = 2
    
    imgdata = ctx.createImageData(w,h)
    #start
    for y in range(h):
        for x in range(w):
            sy = 1- y/h
            sx = x/w
            index = (x + y * w) * 4
            ray = camera.generateRay(sx, sy)
            result = scene.intersect(ray)
            if (result.sceneobject != null):
                col =  rayTraceRecursive(scene, ray, maxReflection)
                imgdata.data[index + 0] = col.r * 255
                imgdata.data[index + 1] = col.g * 255
                imgdata.data[index + 2] = col.b * 255
                imgdata.data[index + 3] = 255
            else:
                imgdata.data[index + 0] = 0
                imgdata.data[index + 1] = 0
                imgdata.data[index + 2] = 0
                imgdata.data[index + 3] = 255                
                        
    ctx.putImageData(imgdata,0,0)
    
main()
```