Skip to content

About

Um teste matemático sobre RayTracing usando Python

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

🔦 Vectorized Ray Tracer in Python

A physically-based path tracer written in pure Python — but with a twist that matters for a data-focused portfolio: instead of looping over pixels one ray at a time, it traces millions of rays at once as NumPy arrays. The same instinct that makes vectorized feature pipelines fast (replace Python loops with batched array math) is what makes this renderer fast.

Ray-traced scene with matte, metal and glass spheres

Matte, metal and glass spheres on a diffuse ground plane — global illumination, soft shadows, reflections, refraction and depth of field, all from ~600 lines of documented Python.


Why this project?

I work in data engineering and analytics, and ray tracing turned out to be a perfect playground for the core skill of that world: thinking in arrays.

  • A ray tracer is, mathematically, a giant pile of dot products and quadratic equations. That vectorizes beautifully.
  • The "naive" textbook implementation loops over every pixel in Python. The version here expresses the whole image as (N, 3) array operations and runs ~12× faster as a result (see the benchmark).
  • It reuses the exact mental model behind NumPy / pandas / vectorized ML feature code — just applied to light instead of dataframes.

The design is hybrid: a clean object-oriented API on the outside (cameras, materials, hittable objects, scenes), with a fully vectorized numerical core on the inside.


✨ Features

Materials Lambertian (matte), Metal (reflective, with fuzz), Dielectric (glass, with Fresnel/Schlick)
Geometry Spheres and infinite planes — each just needs a batched hit / normal_at
Camera Positionable, adjustable field of view, depth-of-field via a thin-lens aperture
Rendering Multi-sample anti-aliasing, configurable bounce depth, gamma correction, sky gradient
Reproducible Seeded RNG → identical renders every run
Tooling Single-image CLI, orbit-animation GIF, performance benchmark, explanatory notebook

Camera orbiting the scene


🚀 Quickstart

# 1. Clone and enter the project
git clone <your-repo-url> && cd RayTracing

# 2. Create a virtual environment and install dependencies
python -m venv .venv
.venv\Scripts\activate          # Windows
# source .venv/bin/activate     # macOS / Linux
pip install -r requirements.txt

# 3. Render an image
python render.py --width 800 --samples 64 --out outputs/render.png

A quick preview render (a few seconds):

python render.py --width 400 --samples 16 --out outputs/preview.png

A portfolio-quality render (a couple of minutes):

python render.py --width 1200 --samples 200 --depth 32 --out outputs/hero.png

Render an orbit animation

python animate.py --frames 36 --width 480 --samples 24 --out docs/orbit.gif

Run the benchmark

python benchmark.py --out docs/benchmark.png

🧠 How it works

A ray tracer asks one question, millions of times: starting from the camera, where does this ray go, and what colour does it collect along the way?

The core loop never iterates over pixels in Python. Instead it keeps every ray in the image alive simultaneously and advances them bounce-by-bounce:

throughput = ones((N, 3))     # colour multiplier accumulated along each path
active     = ones(N, bool)    # rays still bouncing

for _ in range(max_depth):
    rec = scene.hit(origins[active], directions[active], ...)  # all rays at once
    # → misses collect the sky colour and stop
    # → hits scatter according to their material, and `throughput` is updated

Intersecting a batch of rays with a sphere is the heart of it. The condition $\lVert \mathbf{O} + t\mathbf{d} - \mathbf{C}\rVert^2 = r^2$ becomes a quadratic in $t$ whose coefficients are all dot products — and a row-wise dot product over N rays is a single np.einsum('ij,ij->i', ...) call.

📓 The full derivation, with diagrams and a live render, is in notebooks/raytracing_math.ipynb.


📊 Performance: vectorization pays off

The same scene, same maths, rendered two ways: the "obvious" pure-Python ray-by-ray tracer vs. the vectorized NumPy renderer. The vectorized version is roughly an order of magnitude faster, and the gap widens with resolution as NumPy amortizes its per-call overhead.

Render time: pure Python vs vectorized NumPy

Reproduce it with python benchmark.py.


🗂️ Project structure

RayTracing/
├── raytracer/              # the library
│   ├── vectors.py          # batched 3D vector math (normalize, dot, reflect, refract)
│   ├── ray.py              # a batch of rays
│   ├── camera.py           # positionable thin-lens camera
│   ├── materials.py        # Lambertian, Metal, Dielectric
│   ├── objects.py          # Hittable base, Sphere, Plane, HitRecord
│   ├── scene.py            # nearest-hit query over a list of objects
│   ├── renderer.py         # the vectorized path-tracing loop  ← the core
│   ├── image.py            # PNG / GIF output helpers
│   └── scenes.py           # the demo scene & orbiting camera
├── render.py               # CLI: render one image
├── animate.py              # CLI: render an orbit GIF
├── benchmark.py            # CLI: vectorized vs naive, with a chart
├── build_notebook.py       # regenerates the math notebook from code
├── notebooks/
│   └── raytracing_math.ipynb
├── docs/                   # curated images used in this README
└── requirements.txt

Design notes

  • Hittable objects expose two batched methods: hit (returns per-ray distance t, +inf on a miss) and normal_at. Adding a new shape means writing just those two — the renderer doesn't change.
  • Material.scatter receives only the subset of rays that struck a surface of that material and returns, per ray, the outgoing direction, colour attenuation, and whether the ray survived. This keeps each material self-contained and vectorized.
  • Iteration over recursion. Classic ray tracers recurse per ray; recursion doesn't vectorize. Here the bounce loop is iterative and operates on the live set of rays, which is what makes the array formulation possible.

🛠️ Extending it

  • New shapes: implement hit / normal_at on a Hittable (a triangle mesh is a natural next step).
  • New materials: subclass Material and implement scatter (emissive lights, textured albedo, ...).
  • Quality: raise --samples (noise falls as $1/\sqrt{N}$) and --depth.

📦 Requirements

Python 3.10+ and the packages in requirements.txt (numpy, pillow, matplotlib, tqdm). The notebook additionally uses nbformat / jupyter.


📄 License

Released under the MIT License — see LICENSE.


Built as a portfolio project exploring how a graphics problem becomes a linear-algebra-on-arrays problem.

About

Um teste matemático sobre RayTracing usando Python

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages