Project overview
This project extends the path tracer from Project 4 to render large scale scenes: San Miguel and Lumberyard Bistro. Both are built from dense triangle meshes with hundreds of thousands of faces, many distinct materials with texture maps, and emissive geometry embedded throughout the scene acting as area lights. Several interrelated problems had to be solved together to get a good image out of a scene like this.
Scene loading: the geometry has to come in from OBJ through Assimp, and parsing a multi-million triangle scene repeatedly is impractical, so a binary scene cache removes the reload stall after the first run.
Lighting: there is no longer a single hand-placed sphere light. Sky illumination comes from a full HDR environment map sampled with importance sampling, while the interior light sources are the scene’s own emissive geometry (lanterns, skylights, emissive panels) aggregated into one unified area light.
Artistic iteration: getting the camera, IBL rotation, and light balance right for a single image takes a lot of trial and error. A hot-reload system lets all of those parameters be edited in a text file and picked up on the next render pass without restarting.
The single image rendering system is extended to produce a movie flythrough: a Catmull-Rom camera path generates per-frame camera and environment parameters from a keyframe file, each frame is rendered using the same progressive render loop, and an offline denoising step via Intel OIDN cleans up the frames before they are assembled into video.
Image-based lighting (IBL)
Overview
IBL replaces the single overhead sphere light from prior projects with a full equirectangular HDR environment map that illuminates the scene from all directions. When a path escapes to infinity (no geometry hit), the ray direction is used to look up radiance from the HDR map. The environment map is also importance-sampled using a 2D cumulative distribution function (CDF) built from the luminance of each pixel, weighted by the solid angle correction factor sin(θ) to account for the equirectangular projection’s distortion near the poles.
Because the CDF concentrates samples toward the bright regions of the map (the sun disc, the sky gradient, the bright horizon), far fewer rays are wasted on dark areas than with uniform hemisphere sampling. This makes IBL converge significantly faster than a single area light, which only samples one direction, or than multiple discrete area lights, which split the sample budget across fixed emitters, since the full sky contribution is captured in a single well-directed sample per bounce.
Implementation: EnvironmentMap (include/environment_map.h)
The EnvironmentMap class handles loading, CDF construction, direction lookup, sampling, and PDF evaluation.
Loading: HDR files are loaded with stb_image as linear float RGB:
float* data = stbi_loadf(path.c_str(), &width, &height, &n, 3);
CDF construction: build() computes per-pixel luminance weighted by sin(θ):
float theta = PI * (y + 0.5f) / height;
float sinTheta = sin(theta);
float lum = 0.2126f * r + 0.7152f * g + 0.0722f * b; // Rec. 709
float weighted = lum * sinTheta;
A conditional CDF is built row-by-row (for sampling the column given a row), and a marginal CDF is built over rows. Both CDFs are normalized to [0, 1].
Importance sampling: sample(e1, e2) performs two std::lower_bound (binary search) searches, one into the marginal CDF to pick a row and one into that row’s conditional CDF to pick a column, then converts the pixel (x, y) back to a spherical direction and evaluates the sampling PDF:
float pdfUV = marginalPDF[y] * conditionalPDF[y][x] * width * height;
result.pdf = pdfUV / (2.0f * PI * PI * sinTheta); // Jacobian from (u,v) to solid angle
Lookup: lookup(direction) converts a world-space direction to (u, v) coordinates with horizontal wrapping and vertical clamping, then performs a bilinear filter over the four neighboring pixels for smooth environment evaluation.
Rotation offset: a rotationOffset field (in radians) is applied during both lookup and sample, so the environment map can be rotated about the Y axis at runtime without rebuilding the CDF.
IBL in TracePath
The IBL integrates into the path tracer through two connections:
Background radiance, when a ray escapes:
if (useIBL)
return iblIntensity * envMap->lookup(ray.direction);
Explicit IBL sampling: at each surface bounce the environment map is sampled as a virtual light source, with MIS using the power heuristic:
auto iblSample = envMap->sample(ie1, ie2);
// Cast shadow ray toward iblSample.direction
// Compute BRDF weight and IBL PDF
float w_ibl = (p_ibl * p_ibl) / (p_ibl * p_ibl + p_brdf_ibl * p_brdf_ibl);
C += W * w_ibl * (f_ibl / p_ibl) * iblIntensity * iblSample.radiance;
The implicit (BRDF-sampled) connection also applies MIS against the IBL PDF when the sampled direction misses all geometry:
float p_ibl = envMap->pdf(inDir);
float w_brdf = (p_brdf * p_brdf) / (p_brdf * p_brdf + p_ibl * p_ibl);
Environment Map Manager (include/environment_map_manager.h)
Multiple HDR maps can be registered at startup and preloaded simultaneously. The EnvironmentMapManager maintains a list of MapEntry structs, each holding an EnvironmentMap with its CDF already built:
envMapManager.add("hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr", "Noon");
envMapManager.add("hdri/dikhololo_night_4k.hdr", "Night");
envMapManager.loadAll();
envMap = envMapManager.get(0);
Hot-swapping between loaded maps is instant (pointer reassignment only, no CDF rebuild), so the hot-reload system can switch lighting environments mid-render without a load stall.
Loading Models with PBR Textures
The meshv2 scene command and AssimpLoader.cpp
OBJ scenes with PBR materials are declared with the meshv2 scene command. Unlike the original mesh command, meshv2 delegates to LoadAssimpFile in AssimpLoader.cpp, which performs object loading and produces MeshTriangle instances with per-vertex smooth normals, UVs, and full PBR material bindings, including texture maps and automatic promotion of emissive materials to area lights.
Material extraction reads the full PBR parameter set from each .mtl entry via extractObjMaterial:
Ka → AI_MATKEY_COLOR_AMBIENT (ignored)
Kd → AI_MATKEY_COLOR_DIFFUSE
Ks → AI_MATKEY_COLOR_SPECULAR
Ke → AI_MATKEY_COLOR_EMISSIVE → creates Light material
Ns → AI_MATKEY_SHININESS (Phong exponent → alpha)
Ni → AI_MATKEY_REFRACTI (index of refraction)
d → AI_MATKEY_OPACITY
Tf → AI_MATKEY_COLOR_TRANSPARENT (Kt)
Materials with non-zero Ke (emissive color) or an emissive texture map are automatically promoted to Light materials, and their triangles are registered into the scene’s AreaLight CDF during mesh ingestion.
Texture loading is attempted for all seven standard map types (diffuse, specular, alpha, bump/normal, opacity, emissive) using stb_image. Textures are deduplicated by path so that shared materials do not load the same image twice.
Extensions to Texture & Material class (include/shape.h)
Texture is extended to load RGBA (4-channel) images and exposes three sampling modes:
sample(uv): bilinear-filtered RGB, used for all color lookups during shadingsampleAlpha(uv): nearest-neighbor on the alpha channel, intentionally unfiltered for crisp cutout edgessampleFloat(uv): single R-channel value, used for roughness and shininess maps
Material gains six named texture slots (tex, texKs, texAlpha, texBump, texOpacity, texEmissive) and two methods:
resolve(uv): samples all bound maps and returns a fully baked flatMaterial. Kd is modulated by the diffuse texture, Ks by the specular (falling back to diffuse tint for metals), and alpha is scaled by the shininess map, so the BRDF can use the result without any further texture lookupsperturbNormal(N, T, uv): builds a TBN matrix from the shading normal and tangent, samplestexBumpin tangent space, and returns the perturbed world-space normal, falling back to the smooth normal if the result would flip below the surface
Intersection is extended with three new fields:
Ng/hasNg: geometric face normal, used for shadow ray offsettingtangent/hasTangent: per-hit tangent vector, consumed byperturbNormaluv: interpolated texture coordinates at the hit point
MeshTriangle (include/mesh_triangle.h)
The original Triangle stores only vertex positions and computes a flat face normal at intersection time. MeshTriangle extends it with the data needed for smooth, textured rendering:
- Per-vertex normals (
n0,n1,n2): interpolated at each hit point for smooth shading, with a front-face correction to handle back-facing hits - Per-vertex UVs (
uv0,uv1,uv2): interpolated for all texture lookups during shading - Alpha and opacity testing: the diffuse texture alpha channel and a separate opacity map are both sampled during intersection; below-threshold hits are discarded before an
Intersectionis returned, so foliage and cutout geometry work correctly without a separate material type - Tangent-space normal mapping: when the material has a
texBump, a tangent is computed from the UV derivatives viacomputeTangent()and stored on theIntersectionfor use in the shading step - Precomputed face normal
Ng: stored alongside the interpolated shading normal so subsystems that need the geometric normal (e.g. shadow ray offset) can use it without recomputing area()method: returns the triangle’s surface area, used byAreaLightto build the emissive triangle CDF
Binary Scene Cache (include/scene_cache.h)
For complex scenes with a large number of meshes, loading through Assimp can take several minutes of mesh parsing, normal generation and texture decoding. To remove that stall on repeat runs, SceneCache serializes the fully parsed scene into a single binary .cache file: all MeshTriangle geometry, flat CachedMaterial structs, and raw RGBA texture pixels.
Cache validity is checked against the OBJ file’s size and modification timestamp:
if (header.objFileSize != (uint64_t)objSize ||
header.objModTime != (uint64_t)objTime)
→ rebuild
On a cache hit, load bypasses Assimp entirely, reading all triangles and textures from the binary file in a single bulk fread:
if (SceneCache::isCacheValid(cachePath, objPath)) {
SceneCache::load(cachePath, shapes, cachedMaterials, areaLight, arlEmitScale);
} else {
LoadAssimpFile(objPath, modelTr);
SceneCache::save(cachePath, shapes, allMats, objPath);
}
Without the cache, load times are significant: San Miguel takes around 12 minutes to parse through Assimp, and the Bistro exterior around 5 minutes. With a warm cache both scenes load in under 3 seconds.
Multi-light support from scene geometry
AreaLight (include/area_light.h)
Rather than a single hardcoded sphere, the system now aggregates all emissive triangles found in the loaded scene into a unified AreaLight. As each MeshTriangle is created, its material is checked:
if (mat->isLight()) {
scene->areaLight.addTriangle(tri);
scene->areaLight.emission = mat->Kd;
}
After all triangles are registered, build() constructs an area-weighted CDF:
for (size_t i = 0; i < triangles.size(); i++) {
totalArea += triangles[i]->area();
cdf[i] = totalArea;
}
// Normalize to [0, 1]
Sampling uses std::lower_bound, which does a binary search into the CDF to select a triangle proportional to area, then draws a point uniformly on that triangle using the square-root barycentric mapping:
float su = sqrt(e2);
float bary_u = 1.0f - su;
float bary_v = e3 * su;
The resulting sample PDF is 1 / totalArea, uniform over total light surface area regardless of how many triangles contribute. This handles scenes with dozens of embedded light fixtures (lanterns, skylights, emissive panels) without any per-fixture bookkeeping.
The emitScale parameter
An arlEmitScale multiplier is applied to the emission radiance at load time, so the overall brightness of scene-embedded lights can be adjusted from the application at runtime without modifying the scene file. This is especially useful for balancing IBL sky light against interior area lights:
f >> emitScale;
arlEmitScale = emitScale;
Hot-reloadable camera and environment parameters
The render loop accepts a shouldReloadSceneParams flag on each pass. When set, it re-reads camera.txt without stopping the render, applying the new values immediately to the next batch of rays. This enables live artistic adjustment of camera, IBL, and light parameters while the progressive render accumulates.
camera.txt format:
x y z ry yaw pitch [iblIndex] [iblRot] [iblInt] [emitScale]
All parameters after pitch are optional and fall back to their previous values if absent. The reload handler:
std::ifstream f("camera.txt");
float x, y, z, ry, yaw, pitch, iblRot, iblInt, emitScale;
// ... read position and orientation ...
// Read optional iblIndex → envMapManager.get(iblIdx) → instant hot-swap
EnvironmentMap* newMap = envMapManager.get(iblIdx);
if (newMap) { envMap = newMap; activeEnvMapIndex = iblIdx; }
// Read optional iblRot → envMap->rotationOffset = iblRot * PI / 180.0f
// Read optional iblInt → iblIntensity = iblInt
// Read optional emitScale → arlEmitScale = emitScale
This workflow allows an artist to:
- Orbit the camera around a scene and see updates on the next render pass
- Rotate the HDRI to place the sun in a desired position
- Dim or brighten scene lights independently from the sky
- Switch entirely between the preloaded day and night HDRI without any load stall
Offline denoising
Path tracing at practical pass counts still leaves residual Monte Carlo noise in shadowed regions and areas lit by indirect bounce. Rather than integrating a denoiser into the C++ renderer, denoising is handled as a separate offline step using Intel Open Image Denoise (OIDN) via its oidnDenoise CLI. OIDN is a machine-learning denoiser trained on physically rendered images, and it removes Monte Carlo noise while preserving sharp edges, texture detail, and specular highlights.

At 250 passes the denoiser handles the scene well overall, but its effect is most visible in areas lit purely by indirect bounce. The regions beneath the archways receive no direct light and need far more passes to converge on their own, leaving noisy patches that OIDN fills in cleanly.
Pass count directly affects denoising quality: the more converged the input, the less the denoiser has to extrapolate. This shows up most in night scenes, where the tight falloff around each light source needs enough samples to be defined before OIDN can reconstruct it cleanly rather than smearing it.

The difference between 25 and 100 passes is subtle at a glance, but visible in the light-to-dark transition zones around the lanterns: at 25 passes the denoiser softens the falloff, while at 100 passes the boundary is better defined.


In the Bistro scene the effect is clearest around the menu signage and glass surfaces, which accumulate fireflies at low pass counts. At 25 passes the denoiser smears these regions, losing legibility; at 100 passes the fireflies are sufficiently reduced that denoising preserves the detail instead of erasing it.

While OIDN works well for static images, it produces visible flickering when applied frame-by-frame to a movie, since it has no temporal context and processes each frame independently.
Material adjustments for large-scale scenes
The scene files obtained from Casual Effects for both San Miguel and Lumberyard Bistro use OBJ/MTL formats authored for real-time renderers, and several material properties did not translate directly to our offline path tracer. Adjustments were made during material loading to get physically plausible results within the current renderer’s capabilities.
San Miguel
The lanterns and chandeliers in San Miguel enclose their light sources behind glass geometry. Explicit light sampling in the renderer does not refract shadow rays through transmissive surfaces to reach the emitter behind them, so sampling through the glass would fail to find the light. To work around this, the glass enclosures of the lantern fixtures and chandeliers are overridden to emissive triangles, which the area light CDF can sample directly.
The scene’s MTL file also sets Tf (1,1,1) on most surfaces regardless of whether they are actually transmissive. To avoid unintended transmission on opaque geometry, transmission is only enabled when Tf deviates from (1,1,1). The water fountain is the one explicit exception: Kt and ior are overridden manually, though the effect is subtle since a flat transmissive surface is a rough approximation of water.
Lumberyard Bistro
The Bistro’s glass surfaces carry both Ke (emissive) and Kt (transmission) values in their MTL entries. Since the renderer treats any non-zero Ke as a Light material, and that is mutually exclusive with transmission, Ke takes precedence and Kt is ignored on these surfaces.
The area light CDF is weighted purely by triangle surface area, which means small bright emitters like string lamps are undersampled relative to large low-emission surfaces like the café window panels.
The Bistro MTL files also have texture mapping issues across both the exterior and interior variants: some surfaces reference missing textures entirely, which leaves minor visual inconsistencies visible on closer inspection.
Render performance
All images are rendered at 1544×1080 on a 16-core AMD Ryzen 9 7950. San Miguel and the Bistro scenes render at approximately 4 seconds per pass at this resolution. Images at 250 passes take roughly 25 minutes; higher pass counts for the harder night interiors take proportionally longer. Movie frames are rendered at 858×600 at approximately 2 seconds per pass, with 50 passes per frame completing in under 2 minutes each.
Rendered results
For faster iteration, all images have been run for 250 passes at least (higher if more convergence required) and run through the OIDN denoiser for presentation.
Scene 1: San Miguel
San Miguel is a dense courtyard interior/exterior scene with approximately 10 million triangles across 287 materials, with stone arches, hanging lanterns, and heavy texture work throughout. The embedded lanterns are the area lights; the sky HDR is the dominant outdoor illumination. The day render uses a partly-cloudy midday HDRI, which casts directional sunlight through the arches and onto the stone floor. The night render switches to a dark sky HDRI, shifting the scene to be lit almost entirely by the warm lantern light.
Day

The main courtyard viewed from beneath the tree canopy, lit by a partly cloudy afternoon HDRI. Directional sunlight rakes across the stone floor and table settings while the arched colonnade falls into soft shadow. The tree’s alpha-cutout foliage filters the light into dappled patterns on the ground, and the stone fountain anchors the composition at center. Wall-mounted lanterns are faintly visible in the shaded arcade; they are the emissive geometry that drives the night renders.

An alternate angle of the courtyard, closer to ground level, showing the textured place settings and wrought-iron furniture in the foreground. The afternoon sun catches the upper balcony with its hanging planters and flowering vines, casting long shadows across the white stucco facade. The colonnade recedes into shade on the left while the fountain and tree fill the right half of the frame, giving a sense of the courtyard’s depth and scale.

A closer view of the dining area beneath the courtyard tree. Sunlight breaks through the dense canopy and scatters across the stone paving and white tablecloths, while a flowering cherry tree and ivy-covered wall frame the arched doorway in the background. The colorful hand-painted plates and blue cushion details are clearly resolved; the texture detail holds up across the scene’s thousands of unique material assignments.

A wide view from the shaded arcade looking out into the courtyard, framed by the stone arches and columns. The midday sun floods the open courtyard, illuminating the full spread of dining tables beneath the tree canopy, while the covered walkway on the right remains in cool shadow with a warm lantern glowing against the column. Purple flowers in the foreground and a potted palm on the upper balcony add color depth across the scene’s multiple layers.

The interior stairwell connecting the ground floor to the upper balcony. A shaft of sunlight enters through the ornate wrought-iron window on the left, casting intricate scroll-work shadows onto the steps and wall below. The courtyard is glimpsed through the archway at the bottom, while the rest of the stairwell falls into deep shadow: a hard lighting case, since nearly all illumination arrives through a single small opening.

The upper walkway balcony overlooking the courtyard, with afternoon light streaming through the arches onto the weathered wooden table and tile floor. Wrought-iron chandeliers hang from the beamed ceiling, framed paintings line the interior wall, and potted flowers along the railing add bursts of red and orange. The corridor’s strong one-point perspective draws the eye deep into the scene, and the fine geometric detail (railings, chair spindles, leaf geometry) stays resolved across a long depth range.

An elevated view from the upper balcony looking down into the courtyard, with the stone fountain and tree canopy visible below. Potted plants and red flowers cascade over the railing in the foreground, while the two-story colonnade wraps around the far side of the courtyard with its sunlit arches and shadowed walkways. This angle shows the scene’s vertical complexity, from the ground-level dining tables to the upper balcony planters, along with the interaction between direct sunlight on the facade and the ambient sky light filling the shaded lower arcade.
Night

The same courtyard view at night, now lit entirely by the wall-mounted lanterns along the colonnade. Their warm glow pools across the stone columns and arched doorways, while the open courtyard and tree canopy fall into deep shadow against a faint starlit sky. The lighting is driven solely by the scene’s emissive geometry, with the IBL contributing only a subtle ambient fill. This is the area light system handling multiple distributed light sources in an enclosed space.

The alternate courtyard angle at night, looking along the colonnade toward the far corner. Lantern light spills warm pools across the stone pillars and into the dining area, while the upper balcony chandeliers cast a softer glow onto the facade above. The warm-to-cool gradient between the lantern-lit arcade and the dark open courtyard creates natural depth, and the tree canopy silhouettes against the starry sky reinforce the transition from artificial to ambient light.

Inside the ground-floor arcade at night, looking down the corridor beneath the arched ceiling. The pendant lanterns recede in perspective, each casting a warm cone of light onto the dining tables, checkered tablecloths, and framed paintings along the wall. The light falls off rapidly between fixtures, creating alternating pools of warmth and shadow that emphasize the corridor’s depth. Through the archway on the left, the dark courtyard is barely visible, a stark contrast to the intimate, lantern-lit interior.

A wide view from the shaded arcade looking out into the courtyard at night, matching the daytime companion angle. A wall-mounted lantern on the right column casts warm light across the stone floor and nearest dining tables, while the open courtyard and tree canopy fall into deep shadow against the starlit sky. The upper balcony window glows faintly in the background. The scene’s illumination has shifted entirely from the IBL-flooded daytime view to localized pools of lantern light framing the dark courtyard beyond.

The same stairwell at night, now lit by the overhead chandelier and a wall-mounted lantern visible through the archway. The warm light reveals the staircase, framed paintings, and the ornamental arch at the landing that was hidden in shadow during the day. Through the window on the left, the lantern-lit colonnade is faintly visible. The scene’s light sources have shifted completely from a single IBL-driven sunbeam to multiple small emissive fixtures filling the enclosed space with soft, ambient warmth.

The upper walkway at night, matching the daytime view of the same corridor. The wrought-iron chandeliers are now the primary light source, their warm glow illuminating the beamed ceiling and casting soft light onto the wooden table, paintings, and tile floor. Through the arches on the left, the starlit sky and silhouetted foliage replace the bright daylight of the companion image: a direct day-to-night comparison of the scene going from IBL-dominated to area-light-dominated illumination.

The elevated balcony view looking down into the courtyard at night. Lanterns on the upper and lower levels cast warm light across the two-story facade, picking out the stone column details and window frames while the courtyard floor below is dimly lit by their combined spill. The balcony railing and cascading plants fall into silhouette in the foreground, and the fountain is barely visible in the darkness below, a dramatic contrast to the sunlit version of this same viewpoint, where every surface was fully resolved.
Day - Night toggle
Here we try to compare the images between Day - Night for the same reference frame. The images have been captured with same set of camera parameters, only varying the IBL between Day vs Night and increasing emitScale for night IBL.



The courtyard from beneath the tree canopy. Daytime IBL floods the stone floor and colonnade, while at night the lanterns along the arcade become the sole light source against the starlit sky.



Alternate courtyard angle along the colonnade. Afternoon sun illuminates the two-story facade and balcony planters, while at night the lanterns and upper chandeliers cast warm pools across the columns and dining area.



Wide view from the arcade looking out into the courtyard. Daylight floods the open space through the arches, while at night a single wall lantern on the right column provides the only warm light against the dark courtyard and starlit sky.



Upper walkway corridor. Afternoon sunlight streams through the arches onto the tile floor and furniture, while at night the wrought-iron chandeliers take over as the sole light source, illuminating the beamed ceiling and paintings along the wall.



Elevated balcony view looking down into the courtyard. Sunlight picks out the two-story colonnade and cascading plants by day, while at night lanterns on both levels cast warm light across the facade with the courtyard floor dimly lit below.
Scene 2: Lumberyard Bistro (Exterior)
The Lumberyard Bistro is a street-level scene with a café facade, outdoor seating, and decorative string lights. The exterior carries approximately 2.8 million triangles across 126 materials, and the interior a further 1.1 million triangles across 67 materials. The emissive string lights and interior window glow act as area lights scattered across the scene. The day render uses the same partly-cloudy HDRI to illuminate the scene from above; the night render isolates the warm string lights and window emission against a dark sky, which is the area light system handling many small, distributed emitters at once.
Day

The Bistro street corner viewed from the cobblestone plaza, lit by a partly cloudy afternoon HDRI. The “Le Petit Coin” café facade anchors the scene with its green shopfront, red awnings, and wrought-iron balcony, while colored string lights arc across to the neighboring buildings. A blue Vespa, outdoor menu boards, and potted plants populate the street level, and the stone archway on the right opens into a tree-lined passage: small-scale props and alpha-cutout foliage distributed across a dense urban environment.

A closer street-level view of the café corner in daylight, with the camera near the Vespa and menu boards. The afternoon sun catches the upper facade and balcony, casting shadows from the wrought-iron railing and ivy onto the yellow stucco, while the green shopfront and red awning below sit in softer indirect light. The cobblestone detail, hanging planters, and string light bulbs are clearly resolved at close range.
Night

The same street corner at night, now lit by the café’s glowing window panels and the colored string lights strung between buildings. The emissive windows cast warm light onto the cobblestones and outdoor seating, while the upper floors and neighboring facades fall into shadow against a starlit milky-way sky: a direct day-to-night comparison of the transition from IBL-dominated to area-light-dominated illumination across many small distributed emitters.

A closer street-level view of the café corner at night, with the camera low near the Vespa. The emissive window panels dominate the lighting, casting bright spill onto the sidewalk, outdoor furniture, and menu boards flanking the entrance. The colored string lights and a single bright bulb overhead punctuate the dark street, while the upper floors and neighboring buildings receive only faint indirect light from the windows below, so the tight falloff around each emitter is visible across the scene.
Day - Night toggle
Here we try to compare the images between Day - Night for the same reference frame. The images have been captured with same set of camera parameters, only varying the IBL between Day vs Night and increasing emitScale for night IBL.



Bistro street corner. Afternoon IBL lights the full facade and cobblestones, while at night the emissive café windows and colored string lights become the dominant illumination against the milky-way sky.



Closer street-level view near the Vespa. Afternoon sun catches the upper facade and balcony shadows, while at night the glowing window panels and string lights spill warm light onto the cobblestones and outdoor seating.
Scene 3: Lumberyard Bistro (Interior)
The Bistro interior is rendered in two configurations: with the exterior building geometry disabled, allowing the HDRI to illuminate the space directly through the open window frames, and with the full exterior enabled, where light must enter through the window openings in the surrounding facade. Skipping the glass surfaces in both cases lets IBL light pass into the interior unobstructed. The fully loaded exterior proved significantly harder to converge: the surrounding buildings occlude most of the sky, reducing the interior to narrow shafts of indirect light that need far more passes to resolve cleanly.
Day

The Bistro dining room from the bar with full exterior geometry loaded. Indirect IBL enters through the door openings on the left where the outdoor terrace and surrounding buildings are visible, while the interior fixtures glow faintly against the ambient light. The exterior buildings occlude most of the sky, noticeably darkening the room compared to the without-exterior configuration.

The same room from the wider angle with full exterior loaded. The bar counter and back wall on the left fall into deep shadow, while the tables nearest the open doors receive the strongest light, with the red awning and cobblestone street visible through them. Wall sconces and pendant lamps glow faintly but contribute minimal fill against the IBL.

The Bistro dining room viewed from the bar, with exterior frames loaded and glass surfaces skipped to allow direct IBL illumination. Low-angle sunrise light enters through the framed windows on the left, casting warm light across the dining tables and place settings. The bar’s pendant lamps and wall sconces glow faintly but contribute little against the dominant IBL. The arched doorway and curtained windows along the back wall add depth, and the glassware and table settings are clearly resolved across the full room.

A wider view from the opposite end, with the sunset horizon visible through the framed door openings and arched window on the right. Low-angle sunlight enters from the right, catching the bar counter and floor, while the center of the dining room sits in softer indirect light. The window frames and mullions provide the enclosed feel that the full exterior geometry would normally supply.
Night

The bar-side view at night with full exterior geometry. The wall sconces, bar pendants, and globe lights become the primary illumination, with only faint exterior light visible through the doorways. The surrounding buildings block nearly all ambient sky light, leaving the interior fixtures to drive the scene entirely.

The wider angle at night with full exterior. The interior fixtures fill the room with warm light while the exterior through the doors is barely visible. The same space that received indirect IBL in the daytime render is now lit entirely by emissive surfaces.

The same bar-side view at night with increased emission from the interior fixtures. The wall sconces and bar pendants are now the primary light source, casting warm light across the ceiling, walls, and dining tables, while cool blue light from the waterfront reflects through the framed windows on the left. The warm-to-cool gradient across the floor shows the interior emitters competing with the ambient night light entering through the openings: a reversal of the daytime render, where the IBL dominated and the fixtures contributed little.

The wider interior view at night, again with boosted interior emission. The bar pendant lamps and wall sconces fill the room with warm light, while cool exterior light from street lamps casts shadows from the window columns across the hardwood floor. The same space seen in the day render, where sunrise light flooded through the windows, is now lit primarily by the scene’s emissive geometry.
Day - Night toggle
Here we try to compare the images between Day - Night for the same reference frame. The images have been captured with same set of camera parameters, only varying the IBL between Day vs Night and increasing emitScale for night IBL.



Bistro interior from the bar with full exterior. Indirect IBL supplements the faint fixtures by day, while at night the wall sconces and pendants become the primary light source with the exterior barely visible through the doorways.



Wider interior view with full exterior. The red awning and street are visible through the doors by day, while at night the fixtures fill the room with warm light and the exterior fades to dark shapes through the openings.
For the interior shots where exterior geometry is disabled, the day-to-night transition is more abrupt than expected. Without the surrounding exterior buildings to occlude the sky gradually, the HDRI is directly visible through the windows and door openings, which makes the lighting shift more stark than it would be in a fully enclosed scene.



Bar-side view without exterior. Sunrise IBL floods through the open windows by day, while at night the interior fixtures take over with cool blue waterfront reflections visible through the framed openings.



Wider interior without exterior. Sunset light enters through the framed doors and arched window by day, while at night the bar pendants and wall sconces warm the room with cool street light reflections visible through the openings.
Extension: movie rendering
With a working single-image pipeline (progressive rendering, hot-reloadable camera and environment parameters, IBL, and scene-embedded lights), extending to a movie flythrough is largely a matter of automating what was already done by hand: computing a series of camera states and triggering a render for each one. The additions needed are a curve system to interpolate between authored keyframes smoothly, and offline denoising to clean up frames rendered at a limited pass budget.
Since rendering hundreds of frames to convergence is impractical, movies are rendered at 50 passes per frame and each frame is denoised with OIDN before assembly.
CameraPath (include/camera_path.h)
Keyframes are defined in camera_path.txt, one per line, each specifying time (seconds), camera position (x, y, z), orientation (yaw, pitch), IBL rotation, IBL intensity, and area light emission scale:
# time x y z yaw pitch iblRot iblInt emitScale
0.0 13.610 1.838 11.396 -1.3 10.0 -40.0 1.0 2.0
5.0 14.390 1.319 0.013 0.3 -0.9 -40.0 1.0 2.0
8.0 8.553 1.435 -2.019 160.0 -0.0 -40.0 1.0 2.0
CameraPath::evaluate(frame, fps) converts a frame number to a time t, locates the enclosing keyframe segment, and interpolates using Catmull-Rom splines for position and orientation. Catmull-Rom is chosen because it guarantees C1 continuity (smooth velocity at keyframes) while passing exactly through each keyframe.
Catmull-Rom formula:
static float catmullRom(float p0, float p1, float p2, float p3, float t) {
float t2 = t * t, t3 = t2 * t;
return 0.5f * (
(2.0f * p1) +
(-p0 + p2) * t +
(2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 +
(-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3
);
}
Boundary segments clamp p0 or p3 to the nearest keyframe to avoid needing phantom control points at the ends of the path.
Angle interpolation: yaw is interpolated with catmullRomAngle, which unwraps each neighbor angle relative to the current keyframe before calling catmullRom, preventing the spline from taking the long way around when crossing the ±180° wraparound:
a0 = a1 + wrapAngle(a0 - a1);
a2 = a1 + wrapAngle(a2 - a1);
a3 = a1 + wrapAngle(a3 - a1);
IBL rotation, intensity, and emission scale use simple linear interpolation between the bounding keyframes; these parameters rarely benefit from the higher-order smoothness of Catmull-Rom.
RenderMovie (main.cpp)
RenderMovie drives the frame loop: it loads the keyframe file to construct the Catmull-Rom spline, then for each frame evaluates the path, applies the resulting camera and IBL parameters directly to the scene, and renders with adaptive stopping, checking noise every 25 passes and exiting early if the frame has converged. Both .hdr and .png outputs are written per frame.
Denoising artifacts in movie rendering
Rendering 200 to 300 frames to convergence is impractical, so each frame is rendered at 50 passes and denoised with OIDN. Individual frames look acceptable in isolation, but when assembled into a video the temporal inconsistency of per-frame denoising becomes apparent. OIDN has no frame-to-frame context, so small differences in how noise is resolved between adjacent frames produce visible flickering artifacts in motion.
Scene: San Miguel
San Miguel was chosen over the Bistro for the flythrough primarily because it offers more varied lighting conditions to move through: the mix of open sunlit courtyards, shaded colonnades, and covered walkways means the camera naturally transitions between distinct lit and shadowed regions. The multiple levels and foliage also add structural depth that makes camera movement more interesting. The Bistro works well as a static composition but navigating it is comparatively flat, and the texture inconsistencies noted in the material adjustments section make it less suitable for a close-up moving camera.
Day
The camera moves through the courtyard at ground level, passing from open sunlight into the shaded colonnade. The IBL-driven sunlight rakes across the cobblestones and dining tables, casting hard shadows from the tree canopy and wrought-iron furniture. As the camera enters the arcade, the lighting transitions from direct sun to indirect bounce off the stone columns and floor, and the warm sky fill through the arches competes with the cooler ambient light deep under the ceiling.
The upper walkway segment under afternoon light. Sunlight streams through the open arches onto the tile floor and wooden furniture, while the beamed ceiling and interior wall remain in shadow lit by indirect bounce. The chandeliers glow faintly but contribute no visible illumination against the dominant IBL. The camera ends looking down over the balcony railing into the shaded courtyard below, where the dining tables and fountain are lit only by indirect sky light bouncing off the surrounding walls.
Night
The ground-level arcade at night, lit entirely by the wall-mounted lanterns. Each lantern casts a warm pool of light onto the nearest columns and floor tiles, with rapid falloff between fixtures creating alternating bright and dark zones along the corridor. The courtyard beyond the arches is nearly black: only faint starlight from the night HDRI fills the open space, with the area light system driving all the visible illumination.
The upper walkway at night, where the wrought-iron chandeliers are the sole light sources. Their warm glow illuminates the beamed ceiling and spills onto the paintings and furniture below, while the arches frame a dark sky with visible stars. The camera pans down into the courtyard at the end, revealing the lantern-lit ground floor far below, the full scene lit by dozens of small emissive triangles with no visible IBL contribution.
Summary
- Image-based lighting: sky illumination is driven by a full HDR environment map importance-sampled using a luminance-weighted 2D CDF, combined with MIS against the BRDF at every bounce for faster convergence than a single light source.
- PBR scene loading: OBJ scenes with PBR extensions are loaded via the
meshv2command intoMeshTriangleinstances carrying per-vertex smooth normals, UVs, alpha cutouts, and tangent-space normal mapping. A binary scene cache serializes the fully parsed scene to disk, reducing repeat load times from several minutes to under 3 seconds. - Multi-light from scene geometry: all emissive triangles in the loaded scene are automatically detected and aggregated into a unified
AreaLightCDF. AnemitScalemultiplier allows the balance between sky IBL and interior area lights to be tuned without modifying the scene file.