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.
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.
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.
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} ) $$The dot product is equal to the cosine angle between two unit vectors.
$$ \| \vec{N} \| \cdot \| \vec{L} \| = \cos{\alpha} $$Light intensity falls off by the distance as shown by the inverse square law:
$$ \text{intensity} \propto \frac{1}{\text{distance}^2} $$
Snell's law