Back to projects

Offline Rendering

CUDA Path Tracer

A GPU path tracer implemented in CUDA for physically based rendering, BSDF material sampling, refraction, depth of field, OBJ mesh loading, HDR environment lighting, BVH acceleration, stream compaction, and stochastic sampling.

CUDA C++ Path Tracing BSDF BVH Monte Carlo

Overview

This project explores a CUDA rendering pipeline from ray generation to material scattering and progressive accumulation. The renderer supports multiple material types through a unified BSDF shading kernel, physically based transparent materials, camera effects, triangle meshes, environment maps, and GPU-oriented acceleration techniques.

CUDA path tracer rendered head result
Rendered head result from the CUDA path tracer.

Part 1

Core Rendering Features

Unified BSDF Shading

Implemented a material shading kernel that evaluates BSDF behavior and scatters rays for multiple material types inside the same rendering pipeline.

Basic BSDF rendering result
Basic BSDF material result rendered with the CUDA path tracer.
__global__ void shadeMaterial_with_BSDF()
...
__host__ __device__ void scatterRay()

Material Sorting

Grouped rays by material ID before shading to improve memory contiguity and reduce GPU warp divergence. The optimization is controlled by SORT_MATERIAL for performance comparison.

// Group paths by material ID before shading.
thrust::stable_sort_by_key(materialIds, materialIds + num_paths, paths);

Stochastic Antialiasing

Added sub-pixel jitter during ray generation. Random offsets in the [0, 1) range smooth jagged edges through Monte Carlo convergence.

CUDA path tracer render without antialiasing
Without antialiasing
CUDA path tracer render with stochastic antialiasing
With stochastic antialiasing
#define ANTI_ALIASING 1

float2 pixelOffset = make_float2(u01(rng), u01(rng));

Part 2

Materials, Camera, and Scene Loading

Physically Based Refraction

Implemented transparent material support for glass-like surfaces, including entry/exit detection, Schlick Fresnel approximation, total internal reflection handling, and JSON material configuration.

CUDA path tracer refraction render
Refractive material result with glass-like light transport.
bool entering = cosTheta > 0;
float fresnel = schlickFresnel(cosTheta, eta);
if (glm::length(refracted) < 0.001f) {
    // total internal reflection
}

Depth of Field Camera

Simulated a real camera lens with configurable aperture radius and focal distance. Rays are sampled across a circular lens aperture to create natural defocus and bokeh through progressive sampling.

CUDA path tracer depth of field render example one
Depth of field render example 1
CUDA path tracer depth of field render example two
Depth of field render example 2
#if DEPTH_OF_FIELD
glm::vec3 focalPoint = cam.position + cam.focalDistance * rayDir;

float theta = u01(rng) * 2.0f * 3.14159265f;
float r = cam.lensRadius * sqrt(u01(rng));
glm::vec3 lensOffset = r * (cos(theta) * cam.right + sin(theta) * cam.up);

segment.ray.origin = cam.position + lensOffset;
segment.ray.direction = glm::normalize(focalPoint - segment.ray.origin);
#endif

OBJ Mesh Loading

Built a lightweight OBJ loader for vertices, normals, and faces using the v//vn format. Mesh data is transformed into world space, assigned material IDs, and integrated into the renderer's triangle array.

HDR Environment Map

Loaded HDR environment maps into CUDA texture objects for hardware-accelerated sampling with linear filtering and wrap addressing. The renderer supports JSON-driven environment configuration.

CUDA path tracer environment map render
HDR environment lighting result using an environment map.
envMap.loadToCPU(fullenvpath);
envmapHandle = scene->envMap.loadToCuda();

Acceleration

BVH and GPU Traversal

The renderer uses a BVH acceleration structure to handle complex meshes such as Suzanne and larger character models. The BVH is built with Surface Area Heuristic partitioning and stored in a linear GPU-friendly memory layout for iterative traversal.

16,689 Suzanne triangles
73,184 Manny mesh triangles
12 SAH buckets
BVHAccel::BVHAccel(std::vector<std::shared_ptr<Primitive>>& prims,
                   int maxPrimsInNode)
...
BVHBuildNode* BVHAccel::recursiveBuild(...)
...
__global__ void computeIntersectionsBVH(
    int depth,
    int num_paths,
    PathSegment* pathSegments,
    Geom* geoms,
    Triangle* triangles,
    LinearBVHNode* bvhNodes,
    ShadeableIntersection* intersections)

Performance

Optimization Notes

Stream Compaction

Used path termination detection and thrust::stable_partition to move active paths to the front of the array, reducing wasted work on terminated paths.

#if COMPACTION
if (depth % 2 == 1 || depth == traceDepth - 1) {
    auto lastPath = dev_paths + num_paths;
    auto mid = thrust::stable_partition(thrust::device,
                                        dev_paths,
                                        lastPath,
                                        IsAlive{});
    num_paths = mid - dev_paths;
}
#endif

Improved Random Number Generation

Added a BETTER_RANDOM mode using an optimized 32-bit hash seed to reduce collisions and improve distribution quality for ray generation, material sampling, antialiasing, and depth of field.

#define BETTER_RANDOM 1

uint32_t seed = index + (iter << 16) + (depth << 8);
return thrust::default_random_engine(fastHash(seed));

Russian Roulette Termination

Implemented probabilistic termination for low-contribution paths. Surviving rays are reweighted to preserve an unbiased estimator while reducing unnecessary bounces.

// Applied near the end of a path.
float survivalProbability = 0.8f;
if (u01(rng) > survivalProbability) {
    remainingBounces = 0;
} else {
    throughput /= survivalProbability;
}