'Is there a good way to speed up raytracing in Python?
I've been following the Raytracing in one weekend tutorial series (which is coded in C++), but I've run into a problem. I know Python is not really the language for raytracing, and it shows. Rendering 4 spheres with 100 samples per pixel at 600x300px with multithreading took a little over an quarter of an hour.
I figured that multithreading wasn't really doing anything. I needed multiprocessing. I tried to implement it, but it gave me the error that pygame.Surface wasn't serializable. Is there a solution for this, or maybe even a better way of implementing this? Or is the best way forward to recreate this in C#/C++?
Multithreaded code:
vec3.py
import math
import random
class vec3:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __add__(self, other):
return vec3(
self.x + other.x,
self.y + other.y,
self.z + other.z
)
def __sub__(self, other):
return vec3(
self.x - other.x,
self.y - other.y,
self.z - other.z
)
def __mul__(self, val):
if type(val) == vec3:
return vec3(
self.x * val.x,
self.y * val.y,
self.z * val.z
)
else:
return vec3(
self.x * val,
self.y * val,
self.z * val
)
def __truediv__(self, val):
return vec3(
self.x / val,
self.y / val,
self.z / val
)
def __pow__(self, pow):
return vec3(
self.x ** pow,
self.y ** pow,
self.z ** pow
)
def __rshift__(self, val):
return vec3(
self.x >> val,
self.y >> val,
self.z >> val
)
def __repr__(self):
return "vec3({}, {}, {})".format(self.x, self.y, self.z)
def __str__(self):
return "({}, {}, {})".format(self.x, self.y, self.z)
def dot(self, other):
return (
self.x * other.x +
self.y * other.y +
self.z * other.z
)
def cross(self, other):
return vec3(
(self.y * other.z) - (self.z * other.y),
-(self.x * other.z) - (self.z * other.x),
(self.x * other.y) - (self.y * other.x)
)
def length(self):
return math.sqrt(
(self.x ** 2) +
(self.y ** 2) +
(self.z ** 2)
)
def squared_length(self):
return (
(self.x ** 2) +
(self.y ** 2) +
(self.z ** 2))
def make_unit_vector(self):
return self / (self.length() + 0.0001)
def reflect(self, normal):
return self - normal * (self.dot(normal) * 2)
Raytracing.py
import uuid
import math
import pygame
from random import random
from vec3 import vec3
from threading import Thread
#import time
imprecision = 0.00000001
mindist = 0.001
maxdist = 999999999
class Material:
def random_in_unit_sphere():
p = vec3(random(), random(), random())*2 - vec3(1, 1, 1)
while p.squared_length() >= 1:
p = vec3(random(), random(), random())*2 - vec3(1, 1, 1)
return p
class Matte:
def __init__(self, albedo):
self.albedo = albedo
def scatter(self, ray, rec, scattered):
target = rec.p + rec.normal + Material.random_in_unit_sphere()
scattered.origin = rec.p
scattered.direction = target - rec.p
return True, self.albedo
class Metal:
def __init__(self, albedo, fuzz = 0):
self.albedo = albedo
self.fuzz = fuzz
def scatter(self, ray, rec, scattered):
reflected = ray.direction.make_unit_vector().reflect(rec.normal)
scattered.origin = rec.p
scattered.direction = reflected + (Material.random_in_unit_sphere() * self.fuzz)
return (scattered.direction.dot(rec.normal) > 0), self.albedo
class Transparent:
def __init__(self, refractive_index):
self.ri = refractive_index
def refract(self, v, normal, ni_over_nt):
uv = v.make_unit_vector()
dt = uv.dot(normal)
discriminant = 1 - (ni_over_nt * ni_over_nt * (1-dt*dt))
if discriminant > 0:
refracted = (uv - normal*dt) * ni_over_nt - (normal * math.sqrt(discriminant))
return True, refracted
else:
return False, None
def scatter(self, ray, rec, scattered):
reflected = ray.direction.reflect(rec.normal)
if ray.direction.dot(rec.normal) > 0:
outward_normal = 0 - rec.normal
ni_over_nt = self.ri
else:
outward_normal = rec.normal
ni_over_nt = 1 / self.ri
refracted = self.refract(ray.direction, outward_normal, ni_over_nt)
return bool(refracted[0]), vec3(1, 1, 1)
class Object:
class Sphere:
def __init__(self, pos, r, material=Material.Matte(0.5)):
self.center = pos
self.r = r
self.material = material
def hit(self, ray, t_min, t_max, rec):
oc = ray.origin - self.center
a = ray.direction.dot(ray.direction)
b = oc.dot(ray.direction)
c = oc.dot(oc) - (self.r * self.r)
discriminant = (b*b) - (a*c)
if discriminant < 0:
return False
else:
t = (-b - math.sqrt(discriminant)) / (a + imprecision)
if (t_min < t < t_max):
rec.t = t
rec.p = ray.point_at_parameter(t)
rec.normal = (rec.p - self.center) / self.r
rec.material = self.material
return True
t = (-b + math.sqrt(discriminant)) / (a + imprecision)
if (t_min < t < t_max):
rec.t = t
rec.p = ray.point_at_parameter(t)
rec.normal = (rec.p - self.center) / self.r
rec.material = self.material
return True
class Scene:
def genUUID(self):
id = uuid.uuid4()
while id in self.objects.keys():
id = uuid.uuid4()
return id
def __init__(self):
self.objects = {}
def add(self, object):
id = self.genUUID()
self.objects[id] = object
return id
def hit(self, ray, t_min, t_max, hit_record):
tmp_hit_record = Camera.HitRec()
hit_any = False
closest = maxdist
for object in self.objects.keys():
if self.objects[object].hit(ray, t_min, closest, tmp_hit_record):
hit_any = True
closest = tmp_hit_record.t
hit_record.t = tmp_hit_record.t
hit_record.p = tmp_hit_record.p
hit_record.normal = tmp_hit_record.normal
hit_record.material = tmp_hit_record.material
return hit_any
class Camera:
class Ray:
def __init__(self, A, B):
self.origin = A
self.direction = B
def point_at_parameter(self, t):
return self.origin + (self.direction * t)
class HitRec:
def __init__(self):
self.t = 0
self.p = vec3(0, 0, 0)
self.normal = vec3(0, 0, 0)
def __init__(self, w, screen, scene = Scene(), screen_ratio = 16/9, focal_length = 1):
self.w = w
self.h = int(w / screen_ratio)
self.screen = screen
self.scene = scene
self.screen_ratio = screen_ratio
self.screen_unit_width = 4
self.focal_length = focal_length
self.sampels_per_pixel = 1
self.max_hit_depth = 5
self.max_block_size = 100
# self.depthT = pygame.Surface((w, self.h))
self.origin = vec3(0, 0, 0)
self.horizontal = vec3(self.screen_unit_width, 0, 0)
self.vertical = vec3(0, (-self.screen_unit_width / self.screen_ratio), 0)
self.top_left_corner = \
self.origin - (self.horizontal/2) - (self.vertical/2) - vec3(0, 0, self.focal_length)
def color(self, ray, depth=0):
# Check for sphere hit
rec = self.HitRec()
if self.scene.hit(ray, mindist, maxdist, rec):
scattered = self.Ray(vec3(0, 0, 0), vec3(0, 0, 0))
hit = rec.material.scatter(ray, rec, scattered) # Scattered is the output ray
if depth < self.max_hit_depth and hit[0]:
return self.color(scattered, depth + 1) * hit[1]
else:
return hit[1]
# target = rec.p + rec.normal + self.random_in_unit_sphere()
# return self.color(self.Ray(rec.p, target - rec.p)) * 0.5
## return vec3(rec.normal.x + 1, rec.normal.y + 1, rec.normal.z + 1) * 0.5, rec.t
# Background if nothing's hit
unit_dir = ray.direction.make_unit_vector()
t = 0.5 * (unit_dir.y + 1)
return (vec3(1, 1, 1) * (1 - t)) + (vec3(0.5, 0.7, 1) * t)#, maxdist
def getRay(self, u, v):
return self.Ray(
self.origin,
self.top_left_corner + (self.horizontal * (u + imprecision)) + (self.vertical * (v + imprecision))
)
def renderSquare(self, xoff, yoff, w, h):
pygame.draw.rect(self.screen, (255, 255, 255), (xoff, yoff, w, h))
pygame.display.update()
for y in range(h):
for x in range(w):
col = vec3(0, 0, 0)
for n in range(self.sampels_per_pixel):
u = (x + xoff + random()-0.5) / self.w
v = (y + yoff + random()-0.5) / self.h
r = self.getRay(u, v)
col += (self.color(r) / self.sampels_per_pixel)
col = vec3(math.sqrt(col.x), math.sqrt(col.y), math.sqrt(col.z))
self.screen.set_at((x + xoff, y + yoff), (col.x * 255, col.y * 255, col.z * 255))
pygame.display.update()
def render(self):
blocks = []
for i in range(math.ceil(self.w / self.max_block_size)):
for j in range(math.ceil(self.h / self.max_block_size)):
blocks.append(Thread( target = self.renderSquare, args=(
i * self.max_block_size,
j * self.max_block_size,
self.max_block_size,
self.max_block_size
)))
for job in blocks:
job.start()
for job in blocks:
job.join()
if __name__ == '__main__':
from Raytracing import Camera, Scene, Object
import pygame
pygame.init()
WIDTH, HEIGHT = 600, 300
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Multithreaded raytracer')
scene = Scene()
scene.add(Object.Sphere(vec3( 0, 0, -1), 0.5, Material.Matte(vec3(0, 0.3, 0.8))))
scene.add(Object.Sphere(vec3(-1, 0, -1), 0.5, Material.Metal(vec3(0.8, 0.8, 0.8), 0.1)))
scene.add(Object.Sphere(vec3( 1, 0, -1), 0.5, Material.Metal(vec3(0.8, 0.6, 0.2), 1)))
scene.add(Object.Sphere(vec3(0, -100.5, -1), 100, Material.Matte(vec3(0.8, 0.8, 0))))
tracer = Camera(WIDTH, screen, scene, screen_ratio=2 / 1)
# tracer.render()
tracer.sampels_per_pixel = 100
tracer.render()
loop = True
while loop:
for event in pygame.event.get():
if event.type==pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
loop = False
if event.type==pygame.QUIT:
loop = False
pygame.display.update()
pygame.quit()
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|

