University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 3
- Xinran Tao
- Tested on:
- Ubuntu 22.04, i7-11700K @ 3.60GHz × 16, RAM 32GB, GeForce RTX 3080 Ti 12GB (Personal)
This project showcases an advanced path tracer powered by CUDA, emphasising on expanding the material capabilities of path tracing. While many aspects of path tracing can be explored, I have chosen to enhance material features for this project rather than adding accelerated structures.
Below are two versions of the final renders. The scene file can be found at ./scenes/jsons/final_scene/final.json.
![]() |
![]() |
|---|---|
| The Lonely Bathroom | The Neon Bathroom |
The only difference between the two images is that the walls of the first one uses a microfacet material:
"wall":
{
"TYPE":"Microfacet",
"RGB":[0.99215686, 0.82352941, 0.28235294],
"SPEC_RGB":[1.0, 1.0, 1.0],
"ROUGHNESS": 0.8,
"IOR": 1.5
}whereas those of the second one uses a light material:
"wall":
{
"TYPE":"Emitting",
"RGB":[0.99215686, 0.82352941, 0.28235294],
"EMITTANCE":1.0
}For the rest of the scene, the both the ceiling and the floor use microfacet materials. The mesh (the sink and the bathtub) is loaded from a .glb file with its default textures and uses a diffuse material. The bubbles are dielectrics with index of refraction 1.33. Furthermore, we have a big white light hanging on the ceiling and a mirror on the left wall. Additionally, the depth of field (DOF) effect can be seen at the blurrred bubbles.
Prerequisites: install Ninja (winget install Ninja-build.Ninja).
Run the build script from the project root:
scripts\build.batThis will:
- Configure and build the project using the Visual Studio 2026 generator (produces
build/cis565_path_tracer.slnx) - Run a second CMake configure with Ninja to generate
compile_commands.jsonfor clangd - Copy
compile_commands.jsonintobuild/and remove the temporary Ninja build folder
Open build/cis565_path_tracer.slnx in Visual Studio to run.
If you prefer to run the steps yourself, open x64 Native Tools Command Prompt for VS 2026 (search in Start Menu — make sure it says x64), then run:
cmake -S . -B build -G "Visual Studio 18 2026"Open build/cis565_path_tracer.slnx in Visual Studio to build and run.
Why x64 Native Tools? NVCC requires MSVC's
cl.exeas its host compiler, targeting x64. The regular "Developer PowerShell" defaults to x86 and causes CUDA compiler detection to crash. Always use the x64 variant.
This project uses clangd for code intelligence (autocomplete, go-to-definition, diagnostics). clangd requires a compile_commands.json file, which the Visual Studio generator does not produce. A separate Ninja configuration is used solely to generate this file — the VS solution in build/ remains the one used for building.
From the same x64 Native Tools Command Prompt for VS 2026, run:
cmake -S . -B build_ninja -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
copy build_ninja\compile_commands.json build\compile_commands.json
rmdir /S /Q build_ninjacompile_commands.json is produced during the configure step — no build required.
Both images are generated after 1000 iterations.
A diffse material should be define in json like this:
"diffuse_white":
{
"TYPE":"Diffuse",
"RGB":[0.98, 0.98, 0.98]
}Below is the image of the default Cornell box scene with a diffuse material.
![]() |
|---|
| Diffuse Material |
The average time for each frame is 500 ms and the FPS is 2.0.
A mirror material should be define in json like this:
"mirror":
{
"TYPE": "Mirror",
"SPEC_RGB": [1.0,1.0,1.0]
}Below is the image of the default Cornell box scene with a mirror material.
![]() |
|---|
| Mirror Material |
The average time for each frame is 533.5 ms and the FPS is 1.9.
- The scene file to test stream compaction and matwrial sort features is in
../scenes/jsons/test_performance/. Remember to replace the path to the meshes and textures with the absolute path on your machine.- The scene file to test antialiasing is the default cornell box scene.
For each of the experiments conducted in this section, the number of iterations is set to 500. When testing each feature, the other two features are turned on for faster result generation.
To better analyze the performance improvements brought about by stream compaction in the context of path tracing, we can look at the percentage reduction in rendering times across different scene complexities and triangle counts. This allows us to quantify the efficiency gains more precisely and see how stream compaction scales with scene complexity.
For the closed scene, stream compaction shows significant improvements as the complexity (triangle count) increases:
- At 960 triangles, the reduction in time per frame from 2,414.80 ms without stream compaction to 960.00 ms with stream compaction translates to an improvement of approximately 60.2%.
- At 5,760 triangles, the time decreases from 15,877.50 ms to 5,760.00 ms, which is an improvement of about 63.7%.
- At 23,040 triangles, there is a reduction from 61,907.50 ms to 24,141.30 ms, resulting in an improvement of approximately 61.0%.
These improvements demonstrate that stream compaction becomes more effective as the number of triangles increases, consistently offering around a 60% decrease in rendering time in closed environments. This consistent performance enhancement underscores the effectiveness of stream compaction in managing ray interactions in denser geometries where rays are likely to remain within the scene longer.
In the open scene, the performance gains from stream compaction are even more pronounced, especially at higher complexities:
- At 960 triangles, the time reduction from 568.90 ms without stream compaction to 260.37 ms with stream compaction results in an improvement of approximately 54.2%.
- At 5,760 triangles, the time decreases from 14,278.82 ms to 2,863.59 ms, showing an improvement of around 79.9%.
- At 23,040 triangles, there is a dramatic reduction from 55,210.47 ms to 9,281.79 ms, which translates to an improvement of approximately 83.2%.
The more substantial percentage improvements in the open scene can be attributed to the higher likelihood of rays escaping or becoming ineffective quickly due to less complex interactions with scene elements. Stream compaction effectively eliminates these non-contributing rays early, thereby significantly reducing computational waste.
Overall, stream compaction offers substantial performance enhancements, particularly in open scenes where many rays do not significantly interact with the scene elements. The technique is highly scalable, showing greater benefits as the number of triangles and scene complexity increase. By focusing computational resources on rays that significantly contribute to the final image, stream compaction makes path tracing more efficient and viable for complex scenes, particularly in real-time applications.
The performance chart provided indicates the impact of material sorting on the rendering time of scenes at various levels of complexity, measured by triangle counts. Material sorting is a technique used in path tracing to optimize the shading step by making rays, path segments, or intersections contiguous in memory by material type. This strategy aims to streamline the computation process during the shading phase by grouping similar materials together, potentially reducing the overhead associated with switching between different material types.
From the data provided in the chart:
- At 960 triangles, the rendering time with material sort is 2,414.80 ms compared to 2,324.50 ms without material sort, showing a slight increase in time by about 3.9%. This indicates that at lower complexities, the overhead of sorting may not be justified by the performance gains in shading.
- At 5,760 triangles, the time with material sort is 11,202.00 ms compared to 11,536.40 ms without material sort, resulting in a performance improvement of about 2.9%. As the scene complexity increases, material sorting starts to show benefits, likely due to more significant material diversity and the increasing impact of efficient shading.
- At 23,040 triangles, the time with material sort is 24,141.30 ms compared to 32,292.10 ms without material sort, which translates to a significant improvement of about 25.2%. At this level of complexity, the benefits of material sorting are most pronounced, likely due to a large variety of materials that can cause more severe computational overhead when not sorted.
The results demonstrate that material sorting can lead to significant performance improvements, especially in more complex scenes with a higher variety of materials. The minor performance degradation observed in very simple scenes suggests that the overhead of sorting might not always be beneficial when the material complexity and diversity are low. However, as the complexity increases, material sorting effectively reduces the time taken per frame by improving the efficiency of BSDF evaluations during shading, making it a worthwhile optimization for complex, material-diverse scenes in path tracing. This approach aligns well with optimizations aimed at reducing divergence in GPU computations and enhancing memory access patterns, which are crucial for achieving high performance in graphics rendering.
Below are the images showcasing the antialiasing feature. Both images are rendered with 500 iterations.
![]() |
![]() |
|---|---|
| With Antialiasing | Without Antialiasing |
![]() |
![]() |
|---|---|
| Zoom With Antialiasing | Zoom Without Antialiasing |
We can see that with antialiasing, the edges of the objects are smoother and the image is less noisy. Given that this feature is implemented by jittering the rays once when the camera is generating them, it does not add much overhead to the rendering process. The average time for each frame is around 550 ms with and without antialiasing. The FPS is 2.0 for both cases.
All visual improvements closely follow the theories in Physically Based Rendering:From Theory To Implementation (PBRT).
To use a dielectric material, the following json should be defined in the scene file:
"dielectric_white":
{
"TYPE": "Dielectric",
"SPEC_RGB": [1.0,1.0,1.0],
"IOR": 1.5
}where SPEC_RGB is the colour of the material and IOR is the index of refraction.
The following image is rendered using the scene file ./scenes/jsons/test_materials/cornell_dielectrics__diffSpecCol.json. The IOR is 1.5 for all spheres.
![]() |
|---|
| Dielectric Material with Different SPEC_RGB |
The following image is rendered using the scene file ./scenes/jsons/test_materials/cornell_dielectrics__diffIOR.json. The IOR used from the leftmost sphere to the rightmost one are 1.2, 1.4, 1.6, and 1.8.
![]() |
|---|
| Dielectric Material with Different IOR |
To use a microfacet material, the following json should be defined in the scene file:
"microfacet":
{
"TYPE": "Microfacet",
"RGB": [0.5,0.5,0.5],
"SPEC_RGB": [1.0,1.0,1.0],
"ROUGHNESS": 0.6,
"IOR": 1.5
}The following image is rendered using the scene file ./scenes/jsons/test_materials/cornell_microfacets.json. The ROUGHNESS used from the leftmost sphere to the rightmost one are 0.01, 0.3, 0.6, and 0.9.
![]() |
|---|
| Microfacet Material with Different Roughness |
We can see that as the roughness increases, the reflection becomes more diffuse.
To enable DOF, the following properties should be defined for the camera in the scene file:
"FOCAL_DISTANCE": 4.0,
"LENS_RADIUS": 0.4Below are the images rendered using the scene file ./scenes/jsons/test_dof/cornell.json.
The set of images below vary the FOCAL_DISTANCE from 2.0 to 8.0 with a fixed LENS_RADIUS of 0.2.
![]() |
![]() |
|---|---|
| Radius 0.2, Distance 2.0 | Radius 0.2, Distance 4.0 |
![]() |
![]() |
|---|---|
| Radius 0.2, Distance 6.0 | Radius 0.2, Distance 8.0 |
The set of images below vary the LENS_RADIUS from 0.1 to 0.4 with a fixed FOCAL_DISTANCE of 4.0.
![]() |
![]() |
|---|---|
| Radius 0.1, Distance 4.0 | Radius 0.2, Distance 4.0 |
![]() |
![]() |
|---|---|
| Radius 0.3, Distance 4.0 | Radius 0.4, Distance 4.0 |
The cover image is a good example of texture loading. A simple procedural texture checkerboard is also implemented in pathtrace.cu. You can toggle this texture by setting USE_CHECKERBOARD_TEXTURE on top of the file.
Below is the cover image rendered with the checkerboard texture.
![]() |
|---|
| The Neon Bathroom with Checkerboard Texture |
To use this feature, you must provide a mesh with UV coordinates. There are two ways of loading a mesh with textures. The first way is to define a textures like so in the scene file:
"Textures": {
"albedo":
{
"TYPE":"Albedo",
"TEXTURE_PATH":"{absolute_path_to_texture}"
},
"bump":
{
"TYPE":"Bump",
"TEXTURE_PATH":"{absolute_path_to_texture}"
}
}and to use a texture, you need to set it when you are defining your geometry:
{
"TYPE":"mesh",
"MESH_PATH":"{absolute_path_to_mesh}",
"MATERIAL":"mirror",
"TEXTURES": ["albedo", "bump"],
"TRANS":[0.0,5.0,0.0],
"ROTAT":[0.0,0.0, 0.0],
"SCALE":[1.0,1.0,1.0]
}And this is the default way of loading textures. The second way is simply load a .glb/gltf file and its textures will be read and used. To toggle between the features, set the USE_SELF_LOADED_TEXTURES on top of the scene.cpp file.
At 960 Triangles:
- With Texture Loading: The time per frame is 3,016.80 milliseconds.
- With Checkerboard: The time per frame is 2,988.54 milliseconds.
- Performance Improvement: The checkerboard procedural texture shows a slight improvement of about 0.94% compared to traditional texture loading. At this low complexity, the difference is minimal, suggesting that both methods handle low triangle counts with almost equal efficiency.
At 5,760 Triangles:
- With Texture Loading: The time per frame is 15,186.63 milliseconds.
- With Checkerboard: The time per frame is 14,357.98 milliseconds.
- Performance Improvement: At this intermediate level of complexity, using a procedural checkerboard texture improves performance by about 5.45%. This more noticeable improvement can be attributed to the procedural method's efficiency in memory usage and possibly less overhead in fetching and applying texture data compared to file-based textures.
At 23,040 Triangles:
- With Texture Loading: The time per frame is 35,450.50 milliseconds.
- With Checkerboard: The time per frame is 30,036.50 milliseconds.
- Performance Improvement: Here, the procedural checkerboard texture offers a significant performance enhancement of about 15.27%. As the scene complexity increases, the benefits of using a procedural texture become more pronounced. This improvement is likely due to the reduced computational overhead of procedural textures, which do not require reading from external files and can be generated on-the-fly, thereby reducing memory bandwidth and storage requirements.
The data illustrates that procedural texture generation, like a checkerboard pattern, tends to offer better performance as scene complexity increases. This is because procedural textures are computed mathematically at runtime and typically require less memory and computational resources than traditional textures, which need to be loaded from disk, stored in memory, and then mapped onto surfaces.
The performance gains are relatively small at lower triangle counts but become significantly more noticeable as the complexity of the scene increases. This trend suggests that for applications where high scene complexity coincides with intensive texture use, procedural textures can offer substantial performance benefits.
In contrast, traditional texture loading might still be preferred in scenarios where unique and complex texture details are required, and the slightly higher computational cost is justifiable by the visual output quality. However, for applications requiring high performance and scalability, especially in real-time systems, procedural textures offer a compelling advantage.
The choice between procedural texture generation and traditional texture loading should consider both the visual fidelity required and the performance implications, as demonstrated by the observed improvements in rendering times across varying levels of scene complexity.
The renderer can transport light spectrally instead of as RGB triples: each path carries radiance at 4 sampled wavelengths, enabling effects RGB transport fundamentally cannot produce — real dispersion (rainbow caustics through glass), physically meaningful light spectra (blackbody, CIE illuminants), and more accurate color mixing.
Toggle: #define SPECTRAL 0|1 in src/render/spectral.h (it is a cross-file switch, so it lives in a shared header rather than pathtrace.cu's toggle block). SPECTRAL 0 reproduces the original RGB renderer bit-identically. Rebuild after flipping it — and use a clean rebuild (cmake --build build --config Release --target cis565_path_tracer --clean-first), since the VS CUDA integration does not reliably re-track header-only macro flips.
| Technique | Source |
|---|---|
| Hero Wavelength Spectral Sampling, N=4 wavelengths/path | Wilkie et al., EGSR 2014; as in PBRT-v4 |
| Visible-wavelength importance sampling (pdf ∝ sech²) | Radziszewski et al. 2009 / PBRT-v4 SampleVisibleWavelengths |
| Sigmoid-polynomial spectral uplifting of all RGB assets | Jakob & Hanika 2019, vendored rgb2spec (BSD) in libs/rgb2spec; the sRGB coefficient table is generated once at build time |
| Illuminant uplift for unbounded emission RGB | PBRT-v4 RGBIlluminantSpectrum (scaled sigmoid × D65) |
| Dispersion via 2-term Cauchy IOR from (n_d, Abbe number), hero-only termination at dispersive interfaces | PBRT-v4 dielectric |
| Sensor: CIE 1931 CMFs → XYZ → linear sRGB per iteration | public-domain CIE data (5 nm tables shipped with rgb2spec, resampled to 1 nm) |
RGB enters spectrum-land in exactly two device functions (upliftReflectance, upliftIlluminant in pathtrace.cu) and leaves it in one (spectrumToRGB in finalGather); in RGB builds all three are identity pass-throughs, so the shading code has no #ifdef forest. Direction sampling is driven by the hero wavelength alone, and every lobe except the dispersive dielectric is wavelength-independent, so the existing scalar MIS machinery stays exact (the dispersive dielectric is a delta lobe and terminates the secondary wavelengths, PBRT-style).
Give a dielectric an Abbe number and its IOR becomes wavelength-dependent (IOR is then interpreted as n_d at the 587.6 nm d-line):
"glass_sf11": {
"TYPE": "Dielectric",
"SPEC_RGB": [1.0, 1.0, 1.0],
"IOR": 1.7847,
"ABBE": 25.68
}Reference Abbe numbers: BK7 crown glass 64.17 (n_d 1.5168), SF11 flint 25.68 (n_d 1.7847), diamond 55.3 (n_d 2.417). Lower Abbe = stronger dispersion. Demo scene: ./scenes/jsons/spectral/spectral_dispersion.json.
Emitters (and delta lights) accept an optional SPECTRUM field; the RGB stays as a tint on top of the (unit-luminance-normalized) illuminant, so EMITTANCE controls brightness consistently across illuminants:
"bulb": { "TYPE": "Emitting", "RGB": [1,1,1], "EMITTANCE": 8.0, "SPECTRUM": {"BLACKBODY": 1900} },
"sky_panel": { "TYPE": "Emitting", "RGB": [1,1,1], "EMITTANCE": 8.0, "SPECTRUM": "D65" }Supported: "D65" (daylight), "A" (incandescent, 2856 K), "E" (equal energy), {"BLACKBODY": <kelvin>} (analytic Planck radiator). In RGB builds the illuminant is folded into the light's RGB tint at scene load, so scenes stay compatible. Demo scene: ./scenes/jsons/spectral/spectral_illuminants.json (D65 vs. A vs. 1900 K candlelight panels).
![]() |
|---|
The same white room lit by three light panels: CIE D65 (daylight), CIE A (incandescent) and a 1900 K blackbody (candlelight) — all with RGB [1,1,1] and identical EMITTANCE; only the spectrum differs |
SPECTRAL 0renders are bit-identical to the pre-spectral renderer (SHA-256 compared).- The GPU port of the rgb2spec fetch/eval is checked at startup against the vendored reference C implementation (max |err| 3.5e-6 over 500 random samples), and a device-side probe verifies the CIE constant tables after upload.
- White-furnace test (gray box, flat spectra): spectral vs. RGB mean abs difference 0.16/255.
- Colored cornell box: 0.93/255 (in-gamut sRGB albedos round-trip through the uplift near-exactly; saturated multi-bounce color bleeding differs slightly — expected, and physically more correct).
- Degenerate-Abbe test (
ABBE: 1e6vs. noABBE): matches within Monte Carlo noise, confirming the hero-only termination bookkeeping is unbiased. - Blackbody at 6504 K matches D65 closely; 2700 K and Illuminant A render visibly warm (R > G > B channel means), and the RGB-build fallback tint agrees with the spectral render.
Costs: the path state grows by 32 bytes (wavelengths + pdfs), throughput/radiance become 4-wide, and each shaded hit performs up to three trilinear fetches from the 9.4 MB coefficient table — quality was deliberately prioritized over speed. Spectral noise from wavelength sampling averages out across iterations like any other Monte Carlo dimension.
The renderer supports participating media as first-class path-integral
participants, following the null-scattering formulation used by
PBRT-v4's VolPathIntegrator.
Any cube or sphere geom can be turned into a volume by giving it a Medium
material; its surface becomes an invisible null boundary and its interior can
scatter, absorb, and emit light.
The following are the current tuned showcase renders. See
TODO.md for the full diagnosis, measurements, commands, and
remaining limitations.
| Cloud | Fog |
|---|---|
![]() |
![]() |
| Smoke | Fire |
![]() |
![]() |
| Combined cloud, fog, smoke, and fire |
|---|
![]() |
"smoke":
{
"TYPE":"Medium",
"SIGMA_A":[0.58, 0.54, 0.50],
"SIGMA_S":[0.16, 0.17, 0.19],
"G":0.32,
"DENSITY":1.35,
"HETEROGENEOUS":true,
"PROFILE":"Plume",
"NOISE_SCALE":6.3,
"NOISE_OCTAVES":5,
"NOISE_SEED":29
}SIGMA_A/SIGMA_S— absorption and scattering cross-sections per unit distance (RGB; uplifted to spectra in spectral builds).G— Henyey-Greenstein phase-function asymmetry in (-1, 1): 0 is isotropic, positive scatters forward (clouds are strongly forward-scattering).DENSITY— global multiplier on both sigmas.HETEROGENEOUS—trueevaluates a procedural density field in the geom's local space;falseis a uniform medium.PROFILE— the density field's large-scale shape:"Fbm"for generic thresholded wisps,"Cloud"for a connected smooth-summed bank of anisotropic 3-D updraft fields with an irregular base and volumetric erosion,"Plume"for a continuous expanding smoke column around an advected axis,"Fog"for a soft low bank, or"Flame"for tapered and forked fire tongues.NOISE_SCALE,NOISE_OCTAVES, andNOISE_SEEDcontrol the decorating noise.EMISSION— optional scalar volumetric-emission strength.EMISSION_RGBorSPECTRUMsets its color. TheFlameprofile modulates emission spatially and by a relative temperature field.
Media must be cube or sphere geoms because shadow transmittance needs an analytic ray/interior interval. Media may emit, but nested or overlapping containers are not supported because each path currently stores one active medium. The camera must start in vacuum. Geometry type and camera placement are validated at scene load; scene authors must keep medium containers disjoint.
The implementation lives in src/render/volume.h (sampling routines) and src/render/pathtrace.cu (integration), and plugs into the same closure/MIS machinery as the surface BSDFs:
Medium tracking. Each path carries the index of the medium it is currently inside (PathSegment::mediumGeom, −1 in vacuum). Hitting a Medium geom's surface is a null interface: the ray passes straight through unchanged, the inside/outside state toggles, and no path bounce is consumed — only real scattering events count.
Distance sampling. Whenever a ray segment starts inside a medium, a free-flight distance is sampled against the segment to the next surface:
- Homogeneous media use the analytic exponential distribution
p(t) = σ_t e^(−σ_t t), sampled with the hero wavelength's σ_t. - Heterogeneous media use delta (Woodcock) tracking against a constant majorant
σ_maj = max_λ σ_t(λ): tentative collisions are sampled from the majorant exponential, classified real with probabilityσ_t(x)/σ_maj(hero-driven), and null otherwise.
If the sampled distance passes the surface, the path continues to the surface with its throughput multiplied by the transmittance weight; otherwise a real scatter event replaces the surface hit: throughput picks up the single-scattering albedo term σ_s/σ_t, next-event estimation runs at the scatter point, and a new direction is drawn from the phase function.
Spectral correctness. Sigma spectra are uplifted with the same Jakob-Hanika scheme as emission (upliftSigma), and all tracking decisions are driven by the hero wavelength while the other three wavelengths ride along with per-wavelength ratio weights — at a null collision each lane is reweighted by (σ_maj − σ_t(λ))/(σ_maj − σ_t(hero)), and the sampling pdf is averaged over the carried wavelengths (Wilkie et al. 2014 balance heuristic, PBRT-v4's rescaled path probabilities collapsed to the single-sample form). This is what makes the amber fog's colored transmittance unbiased rather than a tinted approximation.
Phase function as a closure. The Henyey-Greenstein phase function is implemented as just another lobe of the BSDF closure interface (makePhaseBSDF(g) in src/render/bxdf.h) with flags Diffuse|Reflection|Transmission — it samples/evaluates over the full sphere and is never delta. Because of that, a medium scatter event flows through exactly the same NEE + MIS code path as a surface hit (sampleDirectLighting): the environment map, area lights, and point/directional lights are all importance-sampled from inside the volume, MIS-weighted against phase sampling with the power heuristic. HG is its own perfect importance sampler (value ≡ pdf), so phase-sampled bounces carry weight 1.
Transmittance on shadow rays. Every NEE shadow ray is now transmittance-aware (shadowTransmittance): opaque geometry still blocks, but medium boundaries don't — instead the ray's overlap interval with each medium is computed analytically (slab test / quadratic in the geom's local space) and the transmittance along it is accumulated: analytically (exp(−σ_t d)) for homogeneous media, and with ratio tracking (Novák et al. 2014) for heterogeneous ones, with Russian roulette on nearly-opaque channels to bound the loop. This gives volumes correct soft, colored shadows and lets surfaces inside a volume receive properly attenuated direct light.
Volume emission. Every traveled segment of an emissive medium independently
estimates ∫ T(0,s) j(s) ds using four full-spectrum stratified samples.
Homogeneous transmittance is analytic and heterogeneous transmittance is
ratio-tracked. The flame profile supplies spatial source strength and a
relative blackbody temperature; a small point-light proxy is still used when
the flame must illuminate exterior surfaces directly.
Tracking correctness. Delta tracking and ratio tracking are unbiased
estimators of free-flight and transmittance for density fields bounded by the
majorant. Every authored profile is clamped to [0, 1] by construction (times
DENSITY), and the 10,000-event safety cap is unreachable at the showcase
scenes' optical depths. There is no ray-marching step size or step banding.
- A
Mediumgeom withSIGMA_A = SIGMA_S = 0is invisible: renders match the medium-free scene. - Scenes without media are unaffected: the plain Cornell box renders identically before and after the volumetric integration (the medium code is skipped entirely when
mediumGeomis −1). - The amber fog sphere's transmitted color deepens with path length through the sphere (Beer-Lambert), and its shadow on the floor is correspondingly tinted.
Costs: each path stores the active-medium index and accumulated real-vertex
distance used by MIS; scenes without media pay only a per-bounce branch. In
media, cost scales with σ_maj × path length (expected number of tracking
steps) plus one transmittance walk per shadow ray. Emissive media additionally
use four stratified source samples per traveled segment.
This project supports .obj/.gltf/.glb file loading. I read the data from the files using tinyobjloader and tinygltf.
Please feel free to play around with the final scene.
This feature can be turned on by setting USE_RUSSIAN_ROULETTE on top of the pathtrace.cu file.
The data is collected from the scene file ./scenes/jsons/test_performance/russianRoulette.json.
At 960 Triangles:
- Without Russian Roulette: The time per frame is 2,583.36 milliseconds.
- With Russian Roulette: The time per frame is 2,315.71 milliseconds.
- Performance Improvement: Implementing Russian Roulette shows a reduction in rendering time of about 10.36%. At lower complexities, even though the absolute time saved is modest, the proportional improvement indicates that Russian Roulette effectively reduces unnecessary computations for rays that contribute minimally to the scene.
At 5,760 Triangles:
- Without Russian Roulette: The time per frame is 13,921.02 milliseconds.
- With Russian Roulette: The time per frame is 11,564.03 milliseconds.
- Performance Improvement: The use of Russian Roulette offers a more substantial performance improvement of 16.93% at this level of complexity. This suggests that as the number of interactions (due to more triangles) increases, the potential for terminating low-contributing rays becomes more impactful, thereby saving more computational time.
At 23,040 Triangles:
- Without Russian Roulette: The time per frame is 38,008.78 milliseconds.
- With Russian Roulette: The time per frame is 24,141.30 milliseconds.
- Performance Improvement: The improvement is the most pronounced at this high complexity level, with a reduction in rendering time of 36.47%. This large improvement underscores the efficiency of Russian Roulette in managing path lifetimes in highly complex scenes, where many rays might otherwise perform unnecessary calculations.
The data highlights that Russian Roulette is particularly effective in reducing rendering times as the scene complexity increases. The technique's probabilistic termination of less significant rays becomes increasingly beneficial with the complexity of the scene because there are more opportunities to eliminate computationally expensive paths that have little impact on the final image.
- Low Complexity Scenes: At lower triangle counts, the performance gains are noticeable and beneficial for applications where even small performance enhancements are valuable.
- Moderate Complexity Scenes: As the triangle count and scene complexity increase, the benefits of Russian Roulette grow, making it highly suitable for more detailed scenes that are not at the peak of complexity but still require significant computation.
- High Complexity Scenes: In very complex scenes, Russian Roulette can drastically reduce computational load, making it an essential technique for optimizing performance in high-detail or dynamic lighting conditions where path tracing traditionally suffers from high computational costs.
The consistent increase in performance improvement across triangle counts provides a compelling case for the adoption of Russian Roulette in rendering scenarios where path optimization can lead to significant reductions in computational overhead and faster rendering times, without compromising on visual fidelity. This technique is especially relevant in real-time rendering applications and complex animation scenes where rendering speed is crucial.
- TinyObjLoader
- TinyGLTF
- PBRT
- rgb2spec (spectral uplifting coefficient tables, BSD)
- Wilkie et al., Hero Wavelength Spectral Sampling, EGSR 2014
- Jakob & Hanika, A Low-Dimensional Function Space for Efficient Spectral Upsampling, Eurographics 2019
- Miller, Georgiev & Jarosz, A Null-Scattering Path Integral Formulation of Light Transport, SIGGRAPH 2019 — the framework this implementation follows
- PBRT-v4, Chapter 14: Light Transport II — Volume Rendering and the open-source pbrt-v4
VolPathIntegrator— the reference implementation consulted for delta tracking, spectral ratio weights, and phase-function MIS - Novák, Selle & Jarosz, Residual Ratio Tracking for Estimating Attenuation in Participating Media, SIGGRAPH Asia 2014 — shadow-ray transmittance
- Woodcock et al., Techniques Used in the GEM Code (1965) — the original delta-tracking algorithm
- Henyey & Greenstein, Diffuse Radiation in the Galaxy (1941) — the phase function
- Kettunen, d'Eon, Pantaleoni & Novák, An Unbiased Ray-Marching Transmittance Estimator, SIGGRAPH 2021 — a lower-variance transmittance alternative worth adopting if dense media become a bottleneck
- NanoVDB — GPU-friendly sparse voxel grids; the natural next step for loading real cloud/smoke assets into
mediumDensity

































