Last active
December 7, 2018 17:30
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"])] | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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