Path tracing

I used Ray tracing in one weekend as study material as well as Physically Based Rendering: From Theory To Implementation and implemented the code in Python using MathTools and no other dependencies.

Fill screen with RGB values

import math
import mathtools as mt

def export_file(file_out, image_in):
    with open(file_out, 'w') as outfile:
        outfile.write(image_in)
    outfile.closed
    return

def write_color(pixel_color):
    output_color = mt.Vector([(255.999 * pixel_color.data[0]),
                              (255.999 * pixel_color.data[1]), 
                              (255.999 * pixel_color.data[2])])

    output_color = str(int(output_color.data[0])) + ' ' + str(int(output_color.data[1])) + 
    ' '  + str(int(output_color.data[2])) + '\n' 
    return output_color

image_width = 512
image_height = 320

image = 'P3\n' + str(image_width) + ' ' + str(image_height) + '\n255\n'

for y in range(image_height):
    for x in range(image_width):
        image += write_color(mt.Vector([x / (image_width-1), 
                                        y / (image_height-1),
                                        0.25]))

export_file('render.ppm', image)

Fill screen with rgb color values.


Ray-object intersection

Implicit equation of a sphere centered at the origin with radius \( r \).

$$ x^2 + y^2 + z^2 - r^2 = 0 $$

Parametric ray \( \vec{r} \).

$$ \vec{r}(t) = \vec{o} + t \vec{d} $$

Our ray \( \vec{r} \) has an origin \( \vec{o} \) and a direction \( \vec{d} \). The ray points from the origin of the view frustum onto our virtual camera sensor or image plane. \( t \) is the scalar value that makes our ray reach out into world space.

$$ (o_{x} + t d_{x})^2 + (o_{y} + t d_{y})^2 + (o_{z} + t d_{z})^2 - r^2 = 0 $$

Ray hit sphere.

Normals on the sphere

import math
import mathtools as mt

def export_file(file_out, image_in):
    with open(file_out, 'w') as outfile:
        outfile.write(image_in)
    outfile.closed
    return

def hit_sphere(center, radius, r):
    oc = r.origin - center
    a = math.sqrt(r.direc.len)
    half_b = oc.dot(r.direc)
    c = math.sqrt(oc.len) - radius*radius
    discriminant = half_b*half_b - a*c
    if discriminant < 0:
        return -1.0
    else:
        return (-half_b - math.sqrt(discriminant)) / a

class ray():
    def __init__(self, origin, direc, t):
        self.origin = origin
        self.direc = direc
        self.t = t

def at(ray, t):
    return ray.origin + ray.direc * t

def ray_color(r):
    t = hit_sphere(mt.Vector([0,0,-1]), 0.5, r)
    if (t > 0.0):
        N = at(r, t) - mt.Vector([0, 0, -1])
        return mt.Vector([N.data[0]+1, N.data[1]+1, N.data[2]+1]) * 0.5

    unit_direc = r.direc.unit
    t = 0.5 * (unit_direc.data[1] + 1.0)
    return (mt.Vector([1.0, 1.0, 1.0]) * (1.0 - t)) + (mt.Vector([0.5, 0.7, 1.0]) *  t)

def write_color(pixel_color):
    # write the translated [0,255] value of each color component
    output_color = mt.Vector([(255.999 * pixel_color.data[0]),
                              (255.999 * pixel_color.data[1]), 
                              (255.999 * pixel_color.data[2])])
    output_color = str(int(output_color.data[0])) + ' ' + str(int(output_color.data[1])) +
    ' ' + str(int(output_color.data[2])) + '\n' 
    return output_color


# Image
aspect_ratio = 16.0 / 9.0
image_width = 640
image_height = int(image_width / aspect_ratio)

# Camera
viewport_height = 2.0
viewport_width = aspect_ratio * viewport_height
focal_length = 1.0

origin = mt.Vector([0, 0, 0])
horizontal = mt.Vector([viewport_width, 0, 0])
vertical = mt.Vector([0, viewport_height, 0])
lower_left_corner = origin-horizontal/2-vertical/2-mt.Vector([0,0,focal_length])

# Render
image = 'P3\n' + str(image_width) + ' ' + str(image_height) + '\n255\n'

for i in range(image_height-1):
    for j in range(image_width):
        u = j / (image_width - 1)
        v = i / (image_height - 1)
        direc_ray_color = lower_left_corner+(horizontal*u)+(vertical*v)-origin
        r = ray(origin, direc_ray_color, 0)
        image += write_color(ray_color(r))

export_file('render.ppm', image)

Sphere surface normals. Direction from origin of sphere to surface ray intersection.


The rendering equation.

$$ \textit{L}_{o} ( p, \vec{\omega}_{o} ) = \textit{L}_{o} ( p, \vec{\omega}_{o} ) + \int_{S^2} f ( p, \vec{\omega}_{o}, \vec{\omega}_{i} ) \textit{L}_{i} ( p, \vec{\omega}_{i} \vert \cos{\theta_{i}} \vert d \vec{\omega}_{i}) $$

$$ f ( p, \vec{\omega}_{o}, \vec{\omega}_{i} ) $$

Materials

Lambert's cosine law (Lambertian reflectance)

The dot product is equal to the cosine angle between two unit vectors.

$$ \| \vec{N} \| \cdot \| \vec{L} \| = \cos{\alpha} $$

Lights

Light intensity falls off by the distance as shown by the inverse square law:

$$ \text{intensity} \propto \frac{1}{\text{distance}^2} $$
Light types


Refraction

Snell's law