### Raytracing in Python

This project aims to build a simple rendering pipeline for educational purposes. It's not the fastest renderer in the world and has no hardware acceleration, but good fun to design, build and play with. Theoretically this could render anything, as the framework isn't tied to the object it is rendering, but see below for a simple demo image:

It has a few key components; the `three_space`

list, the `Model`

class and the `TwoSpace`

class. The `three_space`

is a list of all of the models, each represented by an instance of the `Model`

class, in our 'scene'. The `Model`

class is what defines an object that we want to render and it is simply a combination of two functions, a material function and a bounding function. The `TwoSpace`

class holds our final render, as well as a method that traces the line of sight for each pixel and works out what models, if any, intersect with it. This is done using the bounding function, and then the point of intersection is fed into the material function. The material function then takes this point of intersection and returns a colour.

The source code for the program is below:

raw file

` ````
from PIL import Image
import numpy as np
# An Object in the Three Dimensional Space
class Model:
def __init__( self, bounding_function, material_function ):
# function defining what it means for a ray to 'hit' this Object
self.bounding_function = bounding_function
# function that tells the ray how to respond to hitting this Object
self.material_function = material_function
# The Plane Rendered onto the Screen
class TwoSpace:
def __init__( self, size_x, size_y, ray_funct ):
# the file to be 'rendered into'
self.im = Image.open( 'im.png' )
# no pixels in this file
self.size_x, self.size_y = size_x, size_y
# the field of view, defined as the angle subtended by the two vertical screen edges from the point of the camera
self.fov = 1.2
# localises the 'ray tracing' function
self.ray_funct = ray_funct
def render( self ):
# run through all the pixels in final render, runs ray function for each
pixels = []
for y in range( 0, self.size_y ):
for x in range( 0, self.size_x ):
# well... esentially just 'slices' the fov angle up between the available pixels
pixels.append( self.ray_funct( { 'direction': np.array([ x/self.size_x - 0.5, y/self.size_y - 0.5, 0.5 / np.tan( self.fov ) ]), 'origin': np.array([ 0, 0, 0 ]) } ) )
# produce final render
self.im.putdata( pixels )
# display final render ( SLOW as mentioned in Pillow docs )
self.im.show()
# takes a ray object ( with a direction and origin, both 3D vectors )
def trace( ray ):
# checks agains *every* object in the three_space, this could be optimised a whole load with some thought?
for object in three_space:
# find possible points of intersection
intersection = object.bounding_function( ray )
# if found intersection
if type( intersection ) == type( np.array([0]) ):
return object.material_function( intersection )
# otherwise output background colour
else:
return ( 0, 0, 0 )
def sphere_bounds( ray ):
# defining sphere properties, probably a suggestion this should be a method for an object
radius = 20
position = np.array([ 0, 0, 30 ])
# lets find some vectors for vector projection - also range of a ray definitely shouldn't be hard coded here
ray_to_sphere_centre = np.subtract( ray[ 'origin' ], position )
ray_vect = ray[ 'direction' ] * 10000 - ray[ 'origin' ]
# find normal to sphere perpendicular to ray, is a vector rejection of ray origin to sphere centre vector onto ray vector
perpendicular = ray_to_sphere_centre - np.multiply( ( ray_to_sphere_centre.dot( ray_vect ) / ray_vect.dot( ray_vect ) ), ray_vect )
if perpendicular.dot( perpendicular ) < radius ** 2:
# return point of intersection - my first sqrt in this function I think I did a good :P
return ( radius ** 2 - perpendicular.dot( perpendicular ) ) ** 0.5 - (ray_to_sphere_centre - perpendicular)
else:
return 0
# is this a shader yet?
def point_light_at_zero( point ):
intensity = point.dot( point )
return ( int( (intensity/8) ), 0, int( (intensity/8) ) )
three_space = [ Model( sphere_bounds, point_light_at_zero ) ]
```