Skip to content

Instantly share code, notes, and snippets.

@bjodah
Last active December 7, 2018 17:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bjodah/5092698 to your computer and use it in GitHub Desktop.
Save bjodah/5092698 to your computer and use it in GitHub Desktop.
A faster version (12.8x speedup) of the ray tracer at: http://www.reddit.com/r/tinycode/comments/169ri9/ray_tracer_in_140_sloc_of_python_with_picture/ It uses Cython for speed but I am pretty certain there are lots of opportunities for optimization! To compile: python setup.py build_ext --inplace To run: python tiny_tracer.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
setup(
cmdclass = {'build_ext': build_ext},
ext_modules = [Extension("tracer", ["tracer.pyx"])]
)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Original source:
# Source http://www.reddit.com/r/tinycode/comments/169ri9/ray_tracer_in_140_sloc_of_python_with_picture/
# Modified to use Cython by Björn Dahlgren 2013
from __future__ import division
import argparse
import sys
import os
from itertools import product
from multiprocessing import cpu_count
from PIL import Image
from math import sqrt, pow, pi
from collections import namedtuple
import numpy as np
from tracer import Vector, Sphere, Plane, Ray, PixelGetter, populate_image_data
objs = []
RED, GREEN, BLUE, WHITE = Vector(0, 255, 0), Vector(255, 0, 0), \
Vector(0, 0, 255), Vector(255,255,255)
objs.append(Sphere( Vector(-2.0,0.0,-10.0), 2.0, RED))
objs.append(Sphere( Vector(2.0,0.0,-10.0), 3.5, GREEN))
objs.append(Sphere( Vector(0.0,-4.0,-10.0), 3.0, BLUE))
objs.append(Plane( Vector(0.0,0.0,-12.0), Vector(0.0,0.0,1.0), WHITE))
lightSource = Vector(5.0,5.0,2.0)
def main(width, output, anti_alias, processes, timeout, gamma_correction, ambient):
cameraPos = Vector(0.0,0.0,20.0)
get_pix = PixelGetter(width, cameraPos, objs, lightSource, ambient, gamma_correction)
data = np.zeros((width, width, 3), dtype = np.int8)
populate_image_data(get_pix, data, width)
img = Image.frombuffer("RGB",(width,width), data, 'raw', "RGB", 0, 1)
if anti_alias:
img = img.resize((width // 2, width // 2), Image.ANTIALIAS)
img.save(output,"BMP")
return os.EX_OK
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=main.__doc__)
parser.add_argument('-a', '--anti-alias', action = 'store_true',
help = 'Do anti-aliazation of final image')
parser.add_argument('-w', '--width', type = int, default = 500,
help = "Width of final image")
parser.add_argument('-p', '--processes', type = int,
default = cpu_count(),
help = "Number of parrallell processes")
parser.add_argument('-t', '--timeout', type = int, default = 3600,
help = "Timeout for rendering")
parser.add_argument('-o', '--output', type = str, default = 'trace.bmp',
help = 'Path to output file')
parser.add_argument('-g', '--gamma-correction', type = float, default = 1 / 2.2,
help = 'Gamma correction value')
parser.add_argument('-m', '--ambient', type = float, default = 0.1,
help = 'Ambient value')
args = parser.parse_args()
sys.exit(main(**vars(args)))
import cython
import numpy as np
cimport numpy as np
from libc.math cimport sqrt as c_sqrt
from libc.math cimport pow as c_pow
cdef float pi = 3.14159265
cdef Vector vector0 = Vector(0.0,0.0,0.0)
cdef class Intersection:
cdef Vector point, normal
cdef double distance
cdef Obj obj
def __cinit__(Intersection self, Vector point, double distance, Vector normal, Obj obj):
self.point = point
self.distance = distance
self.normal = normal
self.obj = obj
cdef class Ray:
cdef Vector origin, direction
def __cinit__(Ray self, Vector origin, Vector direction):
self.origin = origin
self.direction = direction
cdef class Obj:
cdef Vector color
cdef intersection(Obj self, Ray l):
pass
cdef class Sphere(Obj):
cdef Vector center
cdef double radius
def __cinit__(Sphere self, Vector center, double radius, Vector color):
self.center = center
self.radius = radius
self.color = color
cdef intersection(Sphere self, Ray l):
cdef double q, d, d1, d2
cdef Vector from_center = l.origin - self.center
q = c_pow(l.direction.dot(from_center), 2) - \
from_center.dot(from_center) + c_pow(self.radius, 2)
if q < 0.0:
return Intersection(vector0, -1.0, vector0, self)
else:
d = -l.direction.dot(from_center)
d1 = d - c_sqrt(q)
d2 = d + c_sqrt(q)
if 0.0 < d1 and ( d1 < d2 or d2 < 0.0):
return Intersection(l.origin+l.direction*d1, d1,
self.normal(l.origin+l.direction*d1), self)
elif 0.0 < d2 and ( d2 < d1 or d1 < 0.0):
return Intersection(l.origin+l.direction*d2, d2,
self.normal(l.origin+l.direction*d2), self)
else:
return Intersection( vector0, -1, vector0, self)
cdef normal(Sphere self, Vector b):
cdef Vector diff = b - self.center
return diff.normal()
cdef class Plane(Obj):
cdef Vector point, normal
def __cinit__(Plane self, Vector point, Vector normal, Vector color):
self.point = point
self.normal = normal
self.color = color
cdef intersection(Plane self, Ray l):
cdef double d
cdef Vector from_origin
d = l.direction.dot(self.normal)
if d == 0.0:
return Intersection( vector0, -1.0, vector0, self)
else:
from_origin = (self.point - l.origin)
d = from_origin.dot(self.normal) / d
return Intersection(l.origin+l.direction*d, d, self.normal, self)
cdef testRay(Ray ray, objects, ignore=None):
cdef Intersection intersect, currentIntersect
cdef Obj obj
intersect = Intersection( vector0, -1, vector0, None)
for obj in objects:
if obj is not ignore:
currentIntersect = obj.intersection(ray)
if currentIntersect.distance > 0.0 and intersect.distance < 0.0:
intersect = currentIntersect
elif 0 < currentIntersect.distance < intersect.distance:
intersect = currentIntersect
return intersect
cdef trace(Ray ray, objects, Vector light, int maxRecur, double ambient):
cdef Intersection intersect, testIntersect
cdef Vector col, intersect_normal
cdef Vector light_from_intersect
cdef Ray lightRay
cdef double lightIntensity
if maxRecur < 0:
return (0.0,0.0,0.0)
intersect = testRay(ray, objects)
light_from_intersect = light-intersect.point
if intersect.distance == -1:
col = Vector(ambient,ambient,ambient)
elif intersect.normal.dot(light_from_intersect) < 0.0:
col = intersect.obj.color * ambient
else:
lightRay = Ray(intersect.point, light_from_intersect.normal())
testIntersect = testRay(lightRay, objects, intersect.obj)
if testIntersect.distance == -1.0:
lightIntensity = 1000.0/(4.0*pi*c_pow(light_from_intersect.magnitude(), 2))
intersect_normal = intersect.normal.normal()
col = intersect.obj.color * max(
intersect_normal.dot(light_from_intersect.normal()*lightIntensity),
ambient)
else:
col = intersect.obj.color * ambient
return col
cdef class Vector:
cdef double x, y, z
def __cinit__(Vector self, double x, double y, double z):
self.x = x; self.y = y; self.z = z
def __add__(Vector self, Vector other):
return Vector(self.x + other.x, self.y+other.y, self.z+other.z)
def __sub__(Vector self, Vector other):
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
def __mul__(Vector self, double b):
return Vector(self.x*b, self.y*b, self.z*b)
def __div__(Vector self, double b):
return Vector(self.x / b, self.y / b, self.z / b)
def __iadd__(Vector self, Vector other):
self.x += other.x
self.y += other.y
self.z += other.z
def __isub__(Vector self, Vector other):
self.x -= other.x
self.y -= other.y
self.z -= other.z
def __imul__(Vector self, double b):
self.x *= b
self.y *= b
self.z *= b
def __idiv__(Vector self, double b):
self.x /= b
self.y /= b
self.z /= b
def __abs__(Vector self):
return self.magnitude()
def __pow__(Vector self, double p, modu):
if modu is None:
return self.dot(self) if p == 2.0 else c_pow(abs(self), p)
else:
return self.__pow__(p, None) % modu
def __richcmp__(Vector self, Vector other, int op):
if op == 2: # eq
return True if abs(self-other) < 1e-15 else False
elif op == 3: # ne
return False if abs(self-other) < 1e-15 else True
cdef dot(Vector self, Vector other):
return self.x * other.x + self.y * other.y + self.z * other.z
cdef cross(Vector self, Vector other):
return (self.y*other.z-self.z*other.y, self.z*other.x-self.x*other.z,
self.x*other.y-self.y*other.x)
cdef magnitude(Vector self):
return c_sqrt(self.dot(self))
cdef normal(Vector self):
return self / self.magnitude()
cdef class PixelGetter:
cdef int _width
cdef double _ambient, _gamma_correction
cdef Vector _cameraPos, _lightSource
cdef list _objs
def __init__(PixelGetter self, width, cameraPos, objs,
lightSource, ambient, gamma_correction):
self._width =width
self._cameraPos =cameraPos
self._objs =objs
self._lightSource =lightSource
self._ambient =ambient
self._gamma_correction=gamma_correction
@cython.cdivision(True)
cdef get_color(PixelGetter self, int i):
cdef int x, y
cdef Ray ray
cdef Vector col, screen_pos
x = i % self._width
y = i // self._width
screen_pos = Vector(10.0*x/self._width-5.0, 10.0*y/self._width-5.0, 0.0) \
-self._cameraPos
ray = Ray(self._cameraPos, screen_pos.normal())
col = trace(ray, self._objs, self._lightSource, 10, self._ambient)
return self.gammaCorrection(col)
cdef gammaCorrection(PixelGetter self, Vector color):
color.x = c_pow(color.x/255,self._gamma_correction)*255
color.y = c_pow(color.y/255,self._gamma_correction)*255
color.z = c_pow(color.z/255,self._gamma_correction)*255
return color
@cython.cdivision(True)
def populate_image_data(PixelGetter get_pix, char[:,:,:] data, int width):
cdef int i, x, y
cdef Vector pixel_color
for i in range(width ** 2):
x = i % width
y = i // width
pixel_color = get_pix.get_color(i)
data[width - 1 - x, y, 0] = <char>pixel_color.x
data[width - 1 - x, y, 1] = <char>pixel_color.y
data[width - 1 - x, y, 2] = <char>pixel_color.z
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment