Skip to content

ZhaLuc/RayTracing

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RayTracing

A physically based, multithreaded CPU path tracer written in C11

Renders shadows, mirror reflections, Snell's-law refraction, and full global illumination (color bleeding, soft indirect light) from a plain-text scene description - accelerated with a bounding volume hierarchy and parallelized across every CPU core with POSIX threads.

Cornell box, path traced at 1000 samples per pixel

C11 · gcc · pthreads · zero third-party dependencies (only libm + libpthread)


Table of contents


What this is

This project is a ray tracer / path tracer: a program that renders a 2D image of a 3D scene by simulating the physics of light. For every pixel it shoots one or more rays out of a virtual camera, finds what those rays hit, and recursively follows how light scatters, reflects, refracts, and is emitted through the scene until it can estimate how much light - and of what color - arrives back at the camera.

It is written entirely in C11, compiles with a single make, and depends on nothing beyond the C math library and POSIX threads. Everything - the vector math, camera model, intersection routines, material shading, acceleration structure, thread pool, scene parser, and image writer - is implemented from first principles in this repository.

The renderer was built in layered, independently verifiable stages, each of which is documented in depth in docs/.

flowchart LR
    A["scene.txt<br/>(text description)"] --> B["Parser<br/>src/scene.c"]
    B --> C["In-memory scene<br/>camera - materials - spheres - BVH"]
    C --> D["Multithreaded<br/>render loop<br/>src/render.c"]
    D --> E["Linear HDR<br/>framebuffer"]
    E --> F["Gamma + quantize"]
    F --> G["output.ppm<br/>(P3 image)"]
Loading

Feature highlights

Category What is implemented
Camera Configurable pinhole camera: position, look-at, up vector, vertical FOV, aspect ratio; correct top-left image origin.
Geometry Analytic ray-sphere intersection; flat surfaces (walls) modeled as giant spheres.
Materials Lambertian diffuse, Metal (fuzzy mirror), Dielectric (glass with Snell refraction, total internal reflection, Schlick Fresnel), and Emissive area lights.
Lighting Two modes: classic direct lighting (explicit shadow rays to point lights) and full Monte Carlo path tracing (global illumination).
Sampling Multisample anti-aliasing (jittered supersampling), cosine-weighted diffuse scattering, Russian-roulette path termination.
Color Linear-space accumulation with gamma-2.0 correction on output.
Acceleration Bounding Volume Hierarchy (AABB slab test, median split) - verified byte-identical to brute force.
Parallelism pthreads row-band partitioning, lock-free shared framebuffer, deterministic per-thread PRNG.
Scene I/O Human-readable, commentable text scene format with line-accurate error reporting.
Determinism Identical inputs produce byte-identical output, single- or multi-threaded.

Gallery

The images below are placeholders. See Full documentation for what each one will show; final renders will replace them.

Progression of the rendering pipeline - each stage was validated before the next was added:

flowchart LR
    S1["Camera rays<br/>+ background"] --> S2["Ray-sphere<br/>geometry"]
    S2 --> S3["Diffuse shading<br/>+ hard shadows"]
    S3 --> S4["Metal + glass<br/>reflection / refraction"]
    S4 --> S5["Anti-aliasing<br/>+ gamma"]
    S5 --> S6["BVH +<br/>multithreading"]
    S6 --> S7["Path-traced<br/>global illumination"]
Loading

Monte Carlo convergence - noise decreases roughly as 1/sqrt(N) as samples per pixel increase:

Convergence 10/100/1000 spp

Global illumination color bleeding - the signature proof that indirect light is being simulated:

Color bleeding closeup


Quick start

# 1. Build (needs gcc + make; links -lm and -lpthread)
make

# 2. Render the bundled Cornell box demo to a PPM image
./raytracer scenes/cornell_box.txt output.ppm

# 3. View it (PPM opens in GIMP/IrfanView/feh, or convert with ImageMagick)
magick output.ppm output.png

Want a cleaner (less noisy) image? Crank the sample count:

RAYTRACER_SPP=1000 ./raytracer scenes/cornell_box.txt output.ppm

Full build/run/troubleshooting details: docs/building-and-usage.md.


How it works (end-to-end)

At the highest level, rendering one image is the following pipeline. Each box is a real module in src/, documented in its own page under docs/.

flowchart TD
    subgraph Load
        S["scene.txt"] --> P["scene_load()<br/>parse & validate"]
        P --> M["materials[]"]
        P --> SP["spheres[]"]
        P --> CAM["camera_init()"]
        SP --> BVH["bvh_build()<br/>acceleration tree"]
    end

    subgraph Render["render_scene() - one pthread per row band"]
        CAM --> R1["camera_get_ray(u,v)"]
        R1 --> RC["ray_color() - recursive"]
        BVH --> RC
        M --> RC
        RC -->|"hit"| SC["material_scatter() / emitted()"]
        SC --> RC
        RC -->|"miss"| BG["background"]
        RC --> ACC["accumulate & average<br/>samples per pixel"]
    end

    ACC --> FB["linear framebuffer"]
    FB --> OUT["framebuffer_write_ppm()<br/>gamma + quantize -> P3"]
Loading
  1. Parse the scene file into materials, spheres, lights, and a camera.
  2. Build a BVH over the spheres so intersection queries are O(log n).
  3. Spawn threads, each owning a contiguous band of image rows.
  4. For every pixel, shoot N jittered rays and average the results (anti-aliasing).
  5. Each ray is traced recursively by ray_color(): intersect → shade → spawn a scattered ray → repeat, bounded by depth and Russian roulette.
  6. Gamma-correct the accumulated linear color and write it as an ASCII PPM.

Architecture at a glance

Modules and their dependencies (headers are mostly static inline for speed):

flowchart LR
    main["main.c<br/>CLI, env, orchestration"]
    scene["scene.c/.h<br/>parser + LoadedScene"]
    render["render.c/.h<br/>threads, ray_color, PPM"]
    camera["camera.c/.h"]
    bvh["bvh.c/.h"]
    sphere["sphere.c/.h"]
    hl["hittable_list.c/.h"]
    material["material.h"]
    light["light.h"]
    aabb["aabb.h"]
    hittable["hittable.h"]
    ray["ray.h"]
    vec3["vec3.h<br/>math + PRNG"]

    main --> scene
    main --> render
    scene --> camera
    scene --> bvh
    scene --> sphere
    scene --> material
    scene --> light
    render --> camera
    render --> bvh
    render --> hl
    render --> material
    render --> light
    bvh --> aabb
    bvh --> sphere
    sphere --> hittable
    hl --> sphere
    material --> hittable
    light --> bvh
    aabb --> ray
    hittable --> ray
    ray --> vec3
    material --> vec3
    aabb --> vec3
Loading

vec3.h sits at the bottom: it defines the double-precision vector type and the thread-local PRNG that every stochastic part of the renderer draws from. A deeper walkthrough is in docs/architecture.md.


The render loop

The recursive core, ray_color(), is the heart of the program. In path-tracing mode it evaluates the rendering equation by Monte Carlo integration:

sequenceDiagram
    participant Cam as Camera
    participant RC as ray_color(depth)
    participant World as BVH / world
    participant Mat as Material

    Cam->>RC: primary ray (u, v)
    loop until miss / absorbed / depth 0 / RR kill
        RC->>World: nearest hit in (0.001, INF)?
        alt miss
            World-->>RC: none -> return background
        else hit
            World-->>RC: HitRecord (p, normal, material)
            RC->>Mat: emitted() + scatter()
            Mat-->>RC: emission, attenuation, scattered ray
            Note over RC: Russian roulette past min depth<br/>color = emitted + attenuation * ray_color(scattered)
        end
    end
Loading

Details, including the exact unbiased Russian-roulette estimator and the shadow-acne epsilon, are in docs/lighting-and-path-tracing.md.


Rendering modes

The renderer chooses its lighting model automatically from the scene:

flowchart TD
    Q{"Scene contains an<br/>emissive material?"}
    Q -->|Yes| PT["Path tracing<br/>(global illumination)<br/>light = emissive surfaces only<br/>cosine-weighted bounces + Russian roulette"]
    Q -->|No| DL["Direct lighting<br/>explicit shadow rays to point lights<br/>+ one indirect bounce"]
Loading
  • Path tracing produces physically correct indirect light (color bleeding, soft shadows) but needs hundreds to thousands of samples per pixel to converge.
  • Direct lighting is fast and clean for hard-shadow previews but cannot produce indirect illumination.

See docs/lighting-and-path-tracing.md.


Command-line interface & environment

raytracer <scene.txt> <output.ppm>
Variable Effect Default
RAYTRACER_SPP Override samples per pixel (e.g. 1000). 100
RAYTRACER_BRUTEFORCE If set, disable the BVH and use brute-force intersection (verification). unset (BVH on)

The program prints a one-line summary to stderr (resolution, spp, depth, thread count, BVH/brute-force, lighting mode, wall-clock time). Full reference: docs/building-and-usage.md.


Scene file format (quick reference)

Plain text, one directive per line. # starts a comment; blank lines are ignored; tokens are whitespace-separated.

size 500 500
camera_pos 278 278 -800
camera_lookat 278 278 0
camera_up 0 1 0
camera_vfov 40
background 0 0 0

material white lambertian 0.73 0.73 0.73
material glass dielectric 1.5
material light emissive 12 12 12

sphere 190 90 190 90 glass
sphere 278 555 278 90 light
Directive Arguments
size <width> <height>
camera_pos / camera_lookat / camera_up <x> <y> <z>
camera_vfov <degrees> in (0, 180)
material ... lambertian <name> lambertian <r> <g> <b>
material ... metal <name> metal <r> <g> <b> <fuzz>
material ... dielectric <name> dielectric <ior>
material ... emissive <name> emissive <r> <g> <b>
sphere <cx> <cy> <cz> <radius> <material>
light <px> <py> <pz> <r> <g> <b>
background <r> <g> <b> (default: sky gradient)

The complete grammar, validation rules, and error messages are in docs/scene-format.md.


Repository layout

RayTracing/
├── Makefile                 # build: gcc -O2 -Wall -Wextra, links -lm -lpthread
├── README.md                # this file
├── scenes/
│   └── cornell_box.txt       # bundled demo scene
├── docs/                    # in-depth documentation (start at docs/README.md)
│   └── images/              # figures (currently placeholders)
└── src/
    ├── vec3.h               # double-precision vectors + thread-local PRNG
    ├── ray.h                # ray type + ray_at()
    ├── camera.{h,c}         # pinhole camera, primary-ray generation
    ├── hittable.h           # HitRecord + set_face_normal()
    ├── sphere.{h,c}         # ray-sphere intersection, bounding box
    ├── hittable_list.{h,c}  # brute-force scene container
    ├── aabb.h               # axis-aligned box + NaN-safe slab test
    ├── bvh.{h,c}            # bounding volume hierarchy
    ├── material.h           # scatter() + emitted() for all material types
    ├── light.h              # point light + shadow-ray direct lighting
    ├── render.{h,c}         # threads, ray_color(), PPM writer
    ├── scene.{h,c}          # text scene parser -> LoadedScene
    └── main.c               # CLI entry point

Full documentation

Every subsystem has a dedicated, in-depth page:

Document Covers
Overview & reading guide How the docs fit together.
Architecture Module graph, data flow, memory ownership, lifecycles.
Building & usage Toolchain, Makefile internals, CLI, env vars, platform notes.
Scene format Full grammar, every directive, validation, errors.
Math & vectors vec3 operations, sampling helpers, reflect/refract, the PRNG.
Camera & rays Pinhole model, orthonormal basis, (u,v) mapping, coordinate system.
Geometry & intersection Ray-sphere quadratic, hit records, face normals.
Materials Lambertian, metal, dielectric, emissive; Snell, TIR, Schlick.
Lighting & path tracing Rendering equation, direct vs. path tracing, Russian roulette.
Acceleration (BVH) AABBs, the slab test, build & traversal, exactness verification.
Concurrency pthreads partitioning, lock-free framebuffer, deterministic RNG.
Output & color Linear HDR accumulation, gamma correction, PPM format.

Performance

Representative timings for scenes/cornell_box.txt at 500×500, path traced with the BVH across 12 threads:

Samples / pixel Wall-clock time
10 ~1.3 s
100 ~10.5 s
1000 ~99 s

Time scales roughly linearly with sample count; Russian roulette keeps average path length low, and multithreading is what makes high sample counts practical. Methodology and analysis: docs/concurrency.md and docs/acceleration-bvh.md.

libpthread. It builds with one command.

  • Readable first. Hot paths are static inline; every non-obvious decision (epsilon values, index-of-refraction ratios, box enclosure) is commented in the source and expanded in docs/.

About

Physically based, multithreaded CPU path tracer in C11: Cornell-box global illumination with soft shadows, reflections, Snell's-law refraction, a BVH, and pthreads, driven by a plain-text scene format.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages