Skip to content

Nathan5563/ntraycer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ntraycer

A physically-based Monte Carlo path tracer written in pure D with zero external dependencies. This project implements a complete ray tracing pipeline from scratch, including all mathematical primitives, native file I/O (Linux syscalls, Windows API), and advanced rendering techniques.

Features

  • Zero External Dependencies: Every component is implemented from first principles
    • Custom math library (vectors, rays, trigonometry, RNG)
    • Native OS file I/O (Linux syscalls on Linux, kernel32.dll on Windows)
    • Custom mutable string implementation for PPM image output
  • Monte Carlo Path Tracing with unbiased global illumination
    • Next Event Estimation (NEE) for efficient direct lighting
    • Multiple Importance Sampling (MIS) combining BSDF and light sampling
  • BVH Acceleration for fast ray-scene intersection
  • Physically-based Materials: Lambertian, Metal, Dielectric, Emissive

Build System

The project uses DUB with the LDC2 compiler for optimized builds.

Prerequisites

  • LDC2 (LLVM-based D compiler for faster binary)
  • DUB (D package manager)
  • Linux x86-64 or Windows x86-64

Building

There are a set of bash scripts for use on Linux:

# Release build
./build/build.sh

# Build and run
./build/run.sh

# Run unit tests
./build/test.sh

# Clean artifacts
./build/clean.sh

Or manually with DUB, which works on both Linux and Windows:

dub build --compiler=ldc2 --build=release && ./ntraycer   # Linux
dub build --compiler=ldc2 --build=release && ntraycer.exe # Windows

The rendered image is written to image.ppm in the current directory.

Architecture

Project Structure

ntraycer/
├── dub.sdl                              # D build configuration
├── build/
│   ├── build.sh                         # Release build script
│   ├── run.sh                           # Build and execute
│   ├── test.sh                          # Run unit tests
│   └── clean.sh                         # Clean build artifacts
├── demos/
│   ├── cornell_box.png                  # Cornell Box scene
│   └── spheres.png                      # Ray Tracing in One Weekend scene
└── source/
    ├── app.d                            # Main entry point & scene setup
    └── lib/
        ├── core/
        │   ├── math.d                   # Vec2, Vec3, Ray, RNG, math functions
        │   ├── file.d                   # Linux syscall wrappers (open/write/close)
        │   └── mutstring.d              # Mutable string buffer
        ├── accel/
        │   ├── aabb.d                   # Axis-Aligned Bounding Box
        │   └── bvh.d                    # Bounding Volume Hierarchy
        ├── renderer/
        │   ├── renderer.d               # Path tracing renderer with NEE/MIS
        │   └── film.d                   # Image buffer and output
        └── scene/
            ├── scene.d                  # Scene container with optional BVH
            ├── background.d             # Environment backgrounds
            ├── light.d                  # Light sampling interface
            ├── camera/
            │   ├── camera.d             # Camera interface
            │   └── pinhole.d            # Perspective pinhole camera
            ├── hittable/
            │   ├── hittable.d           # Ray intersection interface
            │   ├── sphere.d             # Sphere primitive
            │   └── mesh.d               # Triangle and Quad primitives
            └── material/
                ├── material.d           # Material interface with BSDF
                ├── lambertian.d         # Diffuse material
                ├── metal.d              # Reflective material
                ├── dielectric.d         # Refractive material
                └── emissive.d           # Light source material

This project deliberately avoids all external dependencies, including D's standard library (std). Everything is implemented from scratch as a learning exercise.

Rendering Techniques

Monte Carlo Path Tracing

The renderer uses unbiased Monte Carlo integration to solve the rendering equation. For each pixel, multiple random samples are traced through the scene, bouncing off surfaces according to their material properties. The results are averaged to produce the final color.

// Core path tracing loop (simplified)
for (int bounce = 0; bounce < depth; bounce++)
{
    if (!scene.hit(currentRay, hitInfo))
    {
        radiance += throughput * background;
        break;
    }
    
    // Sample BSDF for next direction
    material.scatter(ray, hitInfo, rng, scatterResult);
    throughput *= scatterResult.weight;
    currentRay = scatterResult.scattered;
}

Russian Roulette termination is applied after 5 bounces to prevent infinite paths while maintaining an unbiased estimator.

Next Event Estimation (NEE)

Instead of waiting for paths to randomly hit light sources, NEE explicitly samples lights at each diffuse bounce to reduce noise when the only light sources are small area lights (like in the Cornell Box):

  1. Randomly select a light source from the scene
  2. Sample a point on that light's surface
  3. Cast a shadow ray to check visibility
  4. Add the direct lighting contribution if unoccluded

This dramatically reduces variance for scenes with small light sources.

if (scene.hasLights() && !mat.isSpecular())
{
    LightSample lightSample;
    scene.sampleLight(hitPoint, rng, lightSample);
    
    // Shadow ray test
    if (!inShadow)
    {
        Vec3 directLight = lightSample.emission * brdf * cosTheta / pdf;
        radiance += throughput * directLight;
    }
}

Multiple Importance Sampling (MIS)

MIS combines BSDF sampling and light sampling using the power heuristic to minimize variance. When a path could have been generated by either strategy, the contribution is weighted by the relative probability:

$$w_{\text{light}} = \frac{p_{\text{light}}^2}{p_{\text{light}}^2 + p_{\text{BSDF}}^2}$$

This prevents bright fireflies when the BSDF PDF is near zero but the light PDF is high (or vice versa).

// Convert area PDF to solid angle PDF
float pLightOmega = lightSample.pdfArea * dist * dist / cosLight;

// Get BSDF pdf for this direction  
float pBsdf = mat.pdf(wo, wi, hitInfo);

// MIS weight using power heuristic
float misWeight = powerHeuristic(pLightOmega, pBsdf);

Object-Oriented Design

Material System

Materials implement the Material interface with three key methods for physically-based rendering:

interface Material
{
    /// Sample a scattered direction from the BSDF
    bool scatter(Ray ray, HitInfo hitInfo, ref RNG rng, out ScatterResult result);
    
    /// Evaluate the BSDF: f(wo, wi)
    Vec3 eval(Vec3 wo, Vec3 wi, HitInfo hitInfo);
    
    /// Return the PDF for sampling direction wi given wo
    float pdf(Vec3 wo, Vec3 wi, HitInfo hitInfo);
    
    /// True if this is a delta (specular) BSDF
    bool isSpecular();
}
Material Description BSDF
Lambertian Diffuse scattering $f = \frac{\rho}{\pi}$, cosine-weighted sampling
Metal Specular reflection Delta BSDF at mirror direction, optional fuzz
Dielectric Glass/water Fresnel reflection + Snell's law refraction
Emissive Area lights Returns emission, no scattering

Hittable System

All geometric primitives implement the Hittable interface:

interface Hittable
{
    /// Test ray intersection in [timeMin, timeMax]
    bool hit(Ray r, float timeMin, float timeMax, out HitInfo hitInfo);
}

Primitives that support BVH acceleration also implement Boundable:

interface Boundable
{
    AABB boundingBox();
}

Available Primitives:

  • Sphere — Analytic ray-sphere intersection
  • Triangle — Möller–Trumbore intersection algorithm
  • Quad — Two triangles with unified light sampling

Light Sampling

Objects can implement the Light interface to be sampled for NEE:

interface Light
{
    LightSample sampleLight(Vec3 hitPoint, ref RNG rng);
    Vec3 getEmission();
    float getArea();
    float getPdfArea();
}

Both Sphere and Quad implement this interface when assigned an Emissive material.

License

MIT License — Copyright (c) 2026 Nathan Abebe

About

A ray tracer written entirely from scratch in the D programming language. Support coming for meshes imported from external files (.obj, .stl).

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors