Educational ray tracer written in Python + NumPy + Numba, inspired by the NVIDIA article "Writing Ray Tracing Applications in Python Using Numba for PyOptix" and Peter Shirley's Ray Tracing in One Weekend.
- Shapes: sphere, infinite plane, triangle (Moller-Trumbore), axis-aligned cube (slab method), finite cylinder (quadratic + disk caps), triangle mesh (OBJ loader)
- Textures: solid color, 3D checker pattern, image texture (UV-mapped)
- Materials: Lambertian (diffuse), Metal (specular + fuzz), Dielectric (glass with Snell + Schlick), DiffuseLight (emissive) — accept
Vec3orTexture - Camera: configurable FOV, aspect ratio, look-at, depth of field (thin-lens model)
- Rendering: recursive path tracing with emission, multi-sample antialiasing, sky gradient background
- Acceleration: Bounding Volume Hierarchy (BVH) with longest-axis split
- Numba JIT:
@njitkernels for vec3 math, intersections, and color conversion (~3x speedup) - Output: PNG (Pillow) and PPM formats
- CLI: render any scene from the command line, with JSON logging option
python-ray-tracing/
├── src/raytracer/
│ ├── vec3.py # Vec3 operations on NumPy arrays
│ ├── ray.py # Ray(origin, direction) dataclass
│ ├── camera.py # Camera with FOV, DOF, look-at, aspect_ratio
│ ├── config.py # Centralized settings (dataclasses)
│ ├── aabb.py # AABB bounding box (slab test)
│ ├── base.py # ABCs: Hittable, Material
│ ├── hit.py # HitRecord (+ factory), HittableList
│ ├── materials.py # Lambertian, Metal, Dielectric
│ ├── renderer.py # Recursive ray_color + render loop
│ ├── bvh.py # Bounding Volume Hierarchy
│ ├── color.py # Gamma correction, sRGB conversion
│ ├── utils.py # PNG/PPM output
│ ├── logger.py # Centralized logging + JSONFormatter
│ ├── textures.py # SolidColor, CheckerTexture, ImageTexture
│ ├── shapes/
│ │ ├── __init__.py # Re-exports all shapes
│ │ ├── sphere.py # Ray-sphere intersection
│ │ ├── plane.py # Infinite plane
│ │ ├── triangle.py # Moller-Trumbore (single-pass t+uv)
│ │ ├── cube.py # Axis-aligned box (slab method)
│ │ ├── cylinder.py # Finite cylinder (quadratic + disk caps)
│ │ └── mesh.py # OBJ loader + TriangleMesh with BVH
│ └── jit/
│ ├── __init__.py # NUMBA_ACTIVE flag + select() dispatcher
│ └── kernels.py # @njit kernels
├── src/scenes/ # Demo scene builders
│ ├── shape_gallery.py # All primitives: sphere, cylinder, cube, tetrahedron, plane
│ ├── three_spheres.py # Diffuse, metal, glass on checker ground
│ ├── cornell_box.py # Cornell box with two cubes + emissive ceiling
│ └── random_spheres.py # 400+ spheres (RTIOW finale)
├── main.py # CLI entry point (argparse)
├── tests/ # Unit tests (pytest)
├── notebooks/ # Interactive tutorial
├── assets/ # Static resources (README images)
├── pyproject.toml
└── LICENSE # MIT
| Package | Description | Link |
|---|---|---|
| NumPy | N-dimensional arrays for Vec3 math and image buffers | Docs |
| Numba | JIT compiler — @njit kernels for hot-path acceleration |
Docs |
| Pillow | PNG image output and texture loading | Docs |
| Matplotlib | Rendering visualization and image display | Docs |
| tqdm | Progress bar for the render loop | GitHub |
For notebook support add --extra notebook (installs ipykernel).
Requires Python >= 3.10 and uv.
git clone https://github.com/skateddu/python-ray-tracing.git
cd python-ray-tracing
uv sync# List available scenes
uv run python -m raytracer list-scenes
# Render a scene
uv run python -m raytracer render --scene three_spheres
# Render with Numba acceleration (~3x faster)
uv run python -m raytracer --numba render --scene three_spheres
# JSON log output
uv run python -m raytracer --json-log render --scene shape_gallery
# Full options
uv run python -m raytracer render \
--scene random_spheres \
--width 800 \
--spp 100 \
--depth 50 \
--output data/my_render.png| Option | Default | Description |
|---|---|---|
--scene |
three_spheres |
Scene name (use list-scenes) |
--width |
480 |
Image width in pixels |
--spp |
25 |
Samples per pixel (antialiasing) |
--depth |
15 |
Max ray bounce depth |
--output |
data/<scene>.png |
Output file path |
--no-bvh |
BVH enabled | Disable BVH acceleration |
--numba |
disabled | Enable Numba JIT (before render) |
--json-log |
plain text | Structured JSON log output |
from raytracer.bvh import BVHNode
from raytracer.utils import save_png
from raytracer.renderer import render
from scenes import SCENE_REGISTRY
# Build a scene
camera, world = SCENE_REGISTRY["three_spheres"]()
world = BVHNode(objects=world.objects)
# Render
image = render(camera=camera, world=world, image_width=480, image_height=270, samples_per_pixel=25)
save_png(image_array=image, path="data/my_render.png")| Scene | Description |
|---|---|
shape_gallery |
All primitives: sphere, cylinder, cube, tetrahedron, plane |
three_spheres |
Three material types on checker ground: diffuse, metal, glass |
cornell_box |
Cornell box with colored walls, emissive ceiling, and two cubes |
random_spheres |
400+ random spheres (RTIOW finale scene, uses DOF) |
The ray tracer maps to the standard GPU ray tracing pipeline, implemented on CPU:
graph LR
A[Camera] -->|generate rays| B[Renderer]
B -->|traverse| C[BVH]
C -->|intersect| D[Shapes]
D -->|closest hit| E[Materials]
E -->|scatter| B
B -->|miss| F[Sky Gradient]
B -->|max depth| G[Black]
B -->|final color| H[sRGB + PNG]
| GPU Concept (OptiX) | CPU Implementation | Module |
|---|---|---|
| Ray Generation Program | Camera + render loop | renderer.py |
| Closest Hit Program | Material scatter | materials.py |
| Miss Program | Sky gradient | renderer.py |
| Acceleration Structure | BVH with AABB | bvh.py |
| Shader Binding Table | Material-geometry via HitRecord | hit.py |
| Numba JIT (CPU) | @njit kernels + select() |
jit/ |
The --numba flag (or RAYTRACER_USE_NUMBA=1 environment variable) replaces hot-path functions with @njit-compiled versions:
- Vec3 math:
dot,cross,normalize,reflect,refract,length,length_squared - Intersection kernels:
sphere_intersect_t,triangle_intersect,aabb_hit_check - Color:
linear_to_srgb_value,schlick_reflectance
The first run compiles the kernels (a few seconds of overhead). Subsequent runs use the disk cache (cache=True).
# Run all tests
uv run pytest
# Run a specific test file
uv run pytest tests/test_shapes.py -v
# Run tests matching a pattern
uv run pytest -k "sphere" -v- NVIDIA: Writing Ray Tracing Applications in Python Using Numba for PyOptix
- Peter Shirley: Ray Tracing in One Weekend
- Moller-Trumbore: Fast, Minimum Storage Ray-Triangle Intersection
- Schlick: An Inexpensive BDRF Model for Physically based Rendering
