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 ) ]