Changes made
| # | Change | File | Status |
|---|---|---|---|
| 1 | PushConstantRay — temporary lights removed; frameSeed, rr, depth added | shared_structs.h | done |
| 2 | raytrace() — frame seed, Russian roulette depth, clear | vkapp_raytracing.cpp | done |
| 3 | Random seed per pixel and frame, tea / rnd | raytrace.rgen | done |
| 4 | EvalBrdf, SampleBrdf, PdfBrdf | brdf.glsl | done |
| 5 | The Monte-Carlo loop | raytrace.rgen | done |
| 6 | Running average, count in alpha | raytrace.rgen | done |
| 7 | 4b: the emitter list | vkapp_loadModel.cpp | done |
| 8 | 4b: m_lightBuff, on m_rtDesc binding 2 | vkapp_raytracing.cpp | done |
| 9 | 4b: explicit light connection with a shadow ray | raytrace.rgen | done |
The first part of this report covers the assignment as the two PDFs specify it: project 4a’s Monte-Carlo loop and project 4b’s explicit light connection. Where the implementation departs from the PDF, the section says so and why. The largest departures are a GGX BRDF that is importance sampled, in place of diffuse-only sampling, and multiple importance sampling in place of 4b’s fixed ½.
Beyond the framework covers the additions: per-path Russian roulette as an alternative to the PDF’s, sampling the sun, moon and sky dome, emissive textures, an HDRI environment, and a material override that was the outcome of chasing the Classroom’s fireflies.
The push constant
struct PushConstantRay
{
ALIGNAS(4) bool clear; // 0
ALIGNAS(4) float exposure; // 4
ALIGNAS(4) int debugView; // 8
ALIGNAS(4) int frameSeed; // 12
ALIGNAS(4) float rr; // 16
ALIGNAS(4) int depth; // 20
ALIGNAS(4) int perPathRoulette; // 24 (added)
ALIGNAS(4) int numLights; // 28 (4b)
ALIGNAS(4) int explicitLights; // 32 (4b toggle)
ALIGNAS(4) float emitScale; // 36
ALIGNAS(4) int envSampling; // 40 (added)
ALIGNAS(4) float metalRoughnessFloor; // 44 (added)
ALIGNAS(4) int alignmentTest; // 48
};
Project 3’s three vec3 lights are gone, which removes the only 16-byte members. Everything left
is 4 bytes on a 4-byte boundary, so the two languages cannot disagree about the layout. The
ray generation shader still checks alignmentTest == 1234 first, and it does not fire.
raytrace() fills it each frame:
m_pcRay.frameSeed = frameCount;
m_pcRay.rr = 0.7f;
m_pcRay.perPathRoulette = m_perPathRoulette ? 1 : 0;
if (m_perPathRoulette)
m_pcRay.depth = m_maxDepth;
else {
m_pcRay.depth = 1;
while (float(rand())/RAND_MAX < m_pcRay.rr) m_pcRay.depth++;
m_pcRay.depth = std::min(m_pcRay.depth, m_maxDepth);
}
frameSeed— the frame counter rather than the PDF’srand() % 32768. The counter never repeats. 32,768 values can, and two frames with the same seed add the same sample twice.depth— the PDF’s Russian roulette, drawn on the CPU once per frame. The cap is the PDF’s “correct way”,std::min, set from a Max depth slider (1–16, default 8). See Russian roulette.clear— see Accumulation.
Random numbers
uint seed = tea(gl_LaunchIDEXT.y*gl_LaunchSizeEXT.x + gl_LaunchIDEXT.x, uint(pcRay.frameSeed));
The seed is a local in the ray generation shader, not the payload field the PDF suggests. Only this shader draws random numbers; the closest-hit, miss and any-hit shaders never do. A payload field would send it to every one of them and back for nothing.
The first two numbers jitter the ray within the pixel:
const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + vec2(rnd(seed), rnd(seed));
Project 3 traced through the pixel centre. Jittering means the accumulated average covers the whole pixel. That anti-aliases edges and, as project 3 predicted, filters the textures the ray tracer reads at mip 0. The grain from normal maps that project 3 found in Sponza now averages out once the camera is still.
The BRDF
The PDF suggests importance sampling only the diffuse part: SampleBrdf draws a cosine-weighted
direction and PdfBrdf returns |N·L|/π. That is unbiased for any BRDF, but here it would
waste most samples. Since project 2, EvalBrdf has been Cook-Torrance GGX, and a smooth metal’s
lobe is a few degrees wide. A cosine sample almost never lands in it, so polished surfaces would
take thousands of samples to show a reflection.
So all three functions follow the same glTF material:
| Function | Does |
|---|---|
EvalBrdf(N, L, V, mat) | mix(dielectric, metal, metallic); the BRDF without N·L, as in project 3 |
SampleBrdf(u, lobe, N, V, mat) | picks the specular lobe with probability BrdfSpecularChance, else diffuse |
PdfBrdf(N, L, V, mat) | the exact density SampleBrdf draws L with |
- Diffuse lobe — the PDF’s cosine-weighted hemisphere, built in an orthonormal basis around
N rather than with
SampleLobe. - Specular lobe — a GGX microfacet normal drawn from the distribution of visible normals (Heitz 2018, in Dupuy and Benyoub’s spherical-cap form), with V reflected about it. Sampling only normals that face the viewer avoids drawing reflections that go below the surface, which plain GGX sampling does often at grazing angles.
- Choosing the lobe —
metallic + (1 − metallic) · max(F), with the dielectric’s Fresnel taken at N·V. A probability that depended on the sampled half vector would make the pdf of a given L depend on which lobe produced it. Taken at N·V, it is the same for every direction, soPdfBrdfis the mixture’s exact density:
float pSpec = BrdfSpecularChance(N, V, mat);
return (1.0 - pSpec) * NoL / BRDF_PI
+ pSpec * G1_GGX(NoV, a) * D_GGX(NoH, a) / (4.0 * NoV);
The visible-normal density is G1(V)·D(H)·(V·H)/N·V. Reflecting about H divides by 4·V·H, which cancels. The exact pdf matters twice: the estimator divides by it, and multiple importance sampling compares it against the light’s pdf.
SampleBrdf takes its random numbers as arguments rather than the seed, so that the same
function could take low-discrepancy numbers later.
The Monte-Carlo loop
The loop that ran once in project 3 now runs pcRay.depth times:
for (int i=0; i<pcRay.depth; i++) {
traceRayEXT(...);
if (!payload.hit) { /* the sky, below */ break; }
GetHitObjectData(rayDirection, mat, nrm, dbg);
vec3 P = payload.hitPos; // the payload is reused by the shadow rays
vec3 N = nrm; // normalized and flipped to the viewer
vec3 V = -rayDirection;
if (dot(mat.emission, mat.emission) > 0.0) {
C += weight * W * mat.emission * pcRay.exposure * pcRay.emitScale;
break;
}
if (nee) C += W * <explicit light connections>; // 4b
vec3 Wi = SampleBrdf(u, rnd(seed), N, V, mat);
float NoWi = dot(N, Wi);
float pdf = PdfBrdf(N, Wi, V, mat);
float p = pdf * pcRay.rr;
if (NoWi <= 0.0 || p <= 1e-6) break;
W *= NoWi * EvalBrdf(N, Wi, V, mat) / p;
brdfPdf = pdf;
rayOrigin = P;
rayDirection = Wi;
}
The loop invariant from the PDF holds at the top of every iteration: rayOrigin and
rayDirection are the next untraced segment, C is the light found so far, and W is the
product of f/p along the path.
f—N·Wi · EvalBrdf. The PDF writes the N·L intoEvalBrdf; here it stays outside, as project 3 definedEvalBrdf.- Below the surface — the specular lobe can reflect V into a direction under the surface. The
loop ends there, along with the PDF’s
p < 10⁻⁶guard. PandNare copied out of the payload straight away, because the shadow rays in 4b overwrite it.- A hit on a light adds its emission and ends the path, as the PDF has it.
weightis 1 without 4b; with it, it is the MIS weight below.
Russian roulette
The PDF draws one path length per frame on the CPU and divides by rr at every bounce. A path
reaches bounce i in a fraction rr^i of frames, and when it does, W carries 1/rr^i, so every
bounce counts at full strength on average.
| Depth drawn | Frames |
|---|---|
| 1 | 30% |
| 2 | 21% |
| 3 | 15% |
| 4 | 10% |
| 8 (draws of 8 or more, capped) | 8% |
That averages 3.2 bounces. The cap gathers the 8% of draws past 8 into 8, which is the bias the PDF warns about. Draws that long are rare and each later bounce carries less light, so the loss is small. Any cap of this kind would be bias, so it is a slider, not a constant.
The MIS weights use pdf without rr. MIS compares the densities of two ways of choosing a
direction, and rr is the chance the path continued, not part of choosing one.
One depth per frame means every pixel in a frame stops at the same bounce, which shows as dark frames while the camera moves. A Roulette combo in the GUI also offers per-path roulette by throughput; the PDF’s version is the default. See Per-path Russian roulette.
Accumulation
The PDF’s running average, with the count in alpha:
if (pcRay.clear)
imageStore(colCurr, pixel, vec4(C, 1.0));
else {
vec4 old = imageLoad(colCurr, pixel);
vec3 Ave = old.xyz;
float N = old.w;
Ave += (C - Ave)/(N + 1.0);
N += 1.0;
imageStore(colCurr, pixel, vec4(Ave, N));
}
One addition before it: a sample that is NaN or infinite is set to 0. A degenerate sample (a zero-area triangle, a pdf that underflowed) would otherwise be averaged into every later frame and never wash out.
When accumulation restarts
The PDF sets clear from myCamera.modified. Here clear is set by ResetRtAccumulation(),
which is called for every reason the accumulated image goes stale:
- The camera —
updateCameraBuffercompares the view-projection matrix with last frame’s. That covers the camera’s own movement and its animated moves between presets. - The window — the RT buffer is recreated, with undefined contents.
- A scene switch.
- The sky — a new bake (time of day, HDRI rotation) or a change in what the sky draws.
- Raster mode — the ray traced image goes stale while the scanline path draws.
- Settings — max depth, roulette, explicit sampling, emission scale, the debug view, the metal override.
The GUI shows the sample count. RTRT_DUMP_PFM=path saves the accumulated image as linear float
RGB on exit, with the sample count in the log. Converged renders were compared that way, not by
screenshot. Tone mapping and 8-bit output are far from linear: an early screenshot comparison
read a 1% difference as 4×.
Project 4b: explicit light connection
The emitter list
readModel already had the @@ loop for the list of emitting triangles. It now builds it:
for (uint i=0; i<meshdata.matIndx.size(); i++) {
const Material& mat = meshdata.materials[meshdata.matIndx[i]];
if (glm::dot(mat.emission, mat.emission) <= 0.0f) continue;
// ... the three vertices ...
Emitter e;
e.index = i;
e.v0 = a.pos; e.v1 = b.pos; e.v2 = c.pos;
e.emission = mat.emission;
e.uv0 = a.texCoord; e.uv1 = b.texCoord; e.uv2 = c.texCoord;
e.emissionTexture = mat.emissiveTextureId >= 0 ? txtBase + mat.emissiveTextureId : -1;
lightList.push_back(e);
}
- Normal and area are not stored. The shader computes both from the vertices, which it
needs anyway, so
Emitterholds the vertices, emission, and the UVs and texture of a textured emission. - Scenes made of several files — a scene can be several
readModelcalls, so each file’s list is appended tom_lightListandcreateLightBuffer()uploads it once, at the end ofloadScene. - San Miguel keeps only its last light, as the framework intended. The framework’s
lightList.end()-1erase is now guarded for an empty list, where it gave an inverted range. - Emission scale — the PDF suggests scaling emission by 4 or 5. Here the emission is stored as
authored and each scene’s
emit_scaleinscenes.tomlscales it, with a slider for the rest. The framework’sm_pcRay.exposure = 2, which doubled ray traced emission only, is now 1, so emitters are as bright in both paths.
| Scene | Emitting triangles |
|---|---|
| Sponza | 8,276 |
| Bistro | 65,560 |
The light buffer
std::vector<Emitter> lights = m_lightList;
if (lights.empty()) lights.push_back(Emitter{}); // a buffer cannot be empty
VkCommandBuffer cmdBuf = createTempCmdBuffer();
initBufferWrapFromData(m_lightBuff, cmdBuf, lights, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
submitTempCmdBuffer(cmdBuf);
initBufferWrapFromData uses a staging buffer rather than the PDF’s vkCmdUpdateBuffer, which
is limited to 65,536 bytes: Bistro’s list is 5.2 MB, at 80 bytes per Emitter. A scene with no emitters gets one zeroed
placeholder, since Vulkan has no empty buffers. numLights stays 0 there, so the shader never
reads it.
| Binding | Type | Stages | Holds |
|---|---|---|---|
| 2 | storage buffer | raygen | m_lightBuff, the Emitter[] |
layout(set=0, binding=2, scalar) buffer Lights_ { Emitter e[]; } lights; // emitting triangles
The light count comes from pcRay.numLights rather than lights.e.length(), because the
placeholder makes the buffer one entry long when there are no lights.
SampleLight, PdfLight, GeometryFactor
One triangle is chosen uniformly, then one point uniformly on it, as the PDF specifies:
int k = min(int(rnd(seed) * float(pcRay.numLights)), pcRay.numLights - 1);
float s = sqrt(rnd(seed));
float r2 = rnd(seed);
vec3 b = vec3(1.0 - s, s * (1.0 - r2), s * r2); // barycentrics of the point
The square-root mapping is uniform over the triangle like the PDF’s fold-over, and uses the same two numbers.
The PDF converts the area pdf with GeometryFactor, which carries both cosines:
|(D·Na)(D·Nb)| / (D·D)². Combined with an EvalBrdf that already includes N·L, as 4a defines it,
the cosine at the shaded point would count twice. Here PdfLight converts the area density
1/(A·n) to a density over directions using only the light’s cosine:
float PdfLight(float area, float d2, float cosL)
{
return d2 / (cosL * area * float(pcRay.numLights));
}
The shaded point’s cosine then appears once, in f = N·L · EvalBrdf. Working per direction, not
per area, is also what lets the light sample and the BRDF sample be compared, below.
cosL takes the absolute value, so emitters light from both sides, like the PDF’s
GeometryFactor.
The shadow ray
bool Visible(vec3 P, vec3 L, float tMax)
{
payload.hit = true;
traceRayEXT(topLevelAS,
gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsSkipClosestHitShaderEXT,
0xFF, 0, 0, 0, P, 0.001, L, tMax, 0);
return !payload.hit;
}
The PDF’s flags, except gl_RayFlagsOpaqueEXT. Project 3 made the geometry non-opaque so the
any-hit shader could cut out foliage, and noted that shadow rays would need the same treatment or
leaves would cast solid shadows. Without the opaque flag, any-hit runs and a cutout texel lets the
shadow ray through. tMax is 0.999 · d, so the ray stops just short of the light’s own triangle.
Combining the two with MIS
With 4b, a light can be found two ways: the explicit connection picks it, or a BRDF ray hits it. Counting both in full would count it twice. The PDF halves both. Here each is weighted by Veach’s power heuristic:
float PowerHeuristic(float a, float b)
{
float a2 = a * a;
return a2 / max(a2 + b * b, 1e-20);
}
- The explicit sample is weighted
PowerHeuristic(pLight, PdfBrdf(N, L, V, mat)). - A BRDF ray that hits a light is weighted
PowerHeuristic(brdfPdf, PdfLight(...)), with the light’s pdf computed from the hit triangle (HitTriangleCross) and the distance travelled. - A camera ray has no previous hit to have sampled the light from, so it keeps its full weight.
For any direction the two weights sum to 1, so like ½ it is unbiased. Unlike ½, it follows whichever method is better at that point. A small light seen from a rough surface comes almost entirely from the explicit sample, and its reflection in a polished one almost entirely from the BRDF ray. With ½, each half carries the full noise of the worse method.
Result
Fireflies in the Classroom
The Classroom scene, lit only by sun and sky through its windows, came out full of fireflies: isolated pixels many times brighter than their neighbours. They were still there after several thousand samples. The path tracer is meant to be the unbiased reference for later work, so the easy fix, clamping bright samples, was not an option until the cause was known.
Each test ran for 60 s at 15:00, compared as linear float images. A firefly counts as a pixel over 4× the median of its 5 × 5 neighbourhood. These measurements were made with the per-pixel roulette described above, before the switch to the PDF’s.
| Test | Fireflies |
|---|---|
| As authored | 2,597 |
Sky dome sampling, sun MIS, KHR_materials_specular, NDF vs VNDF, depth 5 vs 8 | no real change |
| Without the sun’s explicit sample (screenshot metric) | 11,300 → 583 |
| Metals forced to roughness ≥ 0.5 | 603, and the brightest pixel 168 → 4.7 |
- The sun’s explicit sample is unbiased. With it on and off (BRDF rays only), the image means are 0.0942 and 0.0931, 1.2% apart.
- The fireflies are sun caustics off chrome. The chair frames (27,360 triangles at roughness 0.1, a stainless part at 0.0) and window frames are mirror-like metal. A diffuse bounce that happens to land on one, and happens to reflect toward the sun, carries the sun’s full radiance. That path is rare and correct, which is exactly what a firefly is.
- Why only two scenes sparkle. The Classroom and Living room 2 have 35,568 and 32,548 triangles of metal at roughness 0–0.1. Sponza and San Miguel have none.
- The reference renderer has them too. NVIDIA’s vk_gltf_renderer shows the same sparkle with its firefly clamp at 0 and its roughness carry-over off. It looks cleaner by default because of those two biased tools.
Sampling the sun better cannot fix this. A diffuse bounce that reaches the sun by way of a mirror is a caustic, and no direct-light strategy finds it. The fixes are bidirectional methods or biased ones. The outcome here is a Metal rough. slider that edits the material instead, and leaves the estimator unbiased.
Beyond the framework
Per-path Russian roulette
The PDF’s roulette draws one depth per frame, so every pixel in a frame runs the same number of bounces, unless its path escapes, hits a light or samples below the surface. That has two effects:
- No idle threads. A GPU runs threads in groups of 32 and a group takes as long as its longest path. With one depth per frame, no thread waits on another’s long path.
- Noise shared across the whole frame. On a 1-bounce frame, every pixel loses its indirect light at once. The average still converges, but while it does, indirect light varies frame to frame across the whole image rather than pixel by pixel.
In practice the shared noise is easy to see. Indoors, where most light arrives by bouncing, every depth-1 frame is much darker than the rest, so while the camera moves the image flickers dark. Frame times jump around too, since a depth-1 frame costs one bounce and a capped one costs eight.
So a Roulette combo in the GUI offers the common alternative, which ends each path by its own throughput. The PDF’s version is the default.
float p = perPath ? pdf : pdf * pcRay.rr;
...
if (perPath && i >= 2) {
float survive = clamp(max(W.r, max(W.g, W.b)), 0.05, 0.95);
if (rnd(seed) > survive) break;
W /= survive;
}
Per path, depth is set to the cap and the shader decides when each path ends. It decides per
pixel, never cuts a path before bounce 3, and keeps paths that are still carrying light. Every
frame then has its indirect light and roughly the same cost, and the noise in neighbouring pixels
is independent, which a denoiser handles far better than noise shared by the whole frame. The cost
is threads finishing at different bounces. pbrt (Pharr, Jakob and Humphreys, 4th ed.) uses the
same rule.
The sun, moon and sky in the path tracer
Project 3 lit ray traced hits with the sky the raster way: no visibility, so interiors were lit as if roofless. The path tracer treats the sky as three more lights, each sampled explicitly and each shared with BRDF sampling by MIS:
| Light | Explicit sample | |
|---|---|---|
| Sun, moon | uniform in their angular cones | 1 / (2π(1 − cos r)) |
| Sky dome | a texel from a luminance × solid-angle CDF over the environment cube, then a point in it | the texel’s probability over its solid angle |
- One light, two ways to find it. A bounce ray that escapes sees each disc as the same uniform cone the explicit sample draws from. If it saw the limb-darkened disc the camera sees, the two strategies would be estimating different lights.
- Precision. The sun’s radius is 0.0046 rad, where
1 − cos rin single precision is off by about half a percent. It is computed as2 sin²(r/2). - The dome’s CDF is rebuilt with every sky bake, uploaded beside the cube (
m_scDescbinding 5). Each texel is weighted by its solid angle, since a cube’s corner texels cover less of the sphere than its centre ones. A small floor keeps every texel samplable, because bilinear lookups spread light into texels that are black on their own.
Emissive textures
glTF emission is emissiveFactor × emissiveTexture. The loader read only the factor. Bistro’s
lights all have factor (1, 1, 1) and carry their colour and shape in the texture, so every string
light glowed white and whole lamp housings emitted. Both paths now multiply by the texel. The
explicit sample reads the texture at the point it chose, which is why Emitter carries UVs, so
a sampled light and a hit light see the same emission.
Emission of 0.02 or less in its brightest channel is treated as a surface, not a light: a dimly glowing fixture, not a lamp.
KHR_materials_specular and ior
The dielectric part of the BRDF reads glTF’s specular factor, colour and texture, and the index of
refraction. Several scenes depend on it: the Classroom and Living room 2 set specularFactor to 0,
and San Miguel uses it throughout. It feeds BrdfSpecularChance too, so the lobe choice follows
the material.
Metal roughness override
A Metal rough. slider raises every metal’s roughness to at least its value, scaled by
metallic, on every ray. It changes the scene, not the estimator: the image is an unbiased render
of a scene whose metals are rougher. 0, the default, renders the scene as authored.
HDRI lighting
An equirectangular .hdr can replace the physical sky. It is resampled into a 512² cube with the
same mips, irradiance and sampling CDF, so neither the raster path nor the path tracer needed
changes. A rotation slider turns it about the vertical. The cube is 32-bit float where the GPU can
filter it: an HDRI’s sun reaches about 10⁵, past the 65,504 a half float can hold, and the
overflowed texels had silently dropped the sun.
Auto exposure from the frame
Project 2’s auto exposure metered the sky, which is wrong indoors. It now meters the rendered frame: a 256-bin log-luminance histogram on the GPU, its 10–90% trimmed mean brought to 0.12 and adapted over time. While the ray tracer has fewer than 16 samples it holds, so the noise of the first frames does not pump the exposure.
Cleanup and resize
| Resource | Created | Destroyed |
|---|---|---|
m_lightBuff | createLightBuffer(), at the end of loadScene | destroyModel(), at shutdown and on a scene switch |
m_envCdfBuff | with the environment cube; recreated when a bake’s face size differs | with the cube |
Neither depends on the window, so recreateSizedVulkanResources is unchanged from project 3.
m_rtDesc, which it already recreates, writes binding 2 from m_lightBuff again.
On a scene switch, destroyModel() destroys the old light buffer with the rest of the model, and
the new scene’s loadScene builds it before createRtDescriptorSet() runs.
Validation-layer verification
VK_LAYER_KHRONOS_validation stays on unconditionally. Sponza ran with no validation-layer
messages through startup, path tracing to 240 samples per pixel, and shutdown.
[19:47:55.664] info ---- Scene: Sponza -----------------------------------------------------
[19:47:58.230] warni Skipped 8140 triangles with alphaMode BLEND (dirt_decal); there is no blended pass
[19:48:13.482] info 3 textures decoded and uploaded in 647 ms
[19:48:13.498] info Scene totals: 3 objects, 3 instances, 82 textures
[19:48:13.502] info Light list: 8276 emitting triangles
[19:48:14.291] info Acceleration structures built: 3 BLAS, TLAS of 3 instances
[19:48:14.851] info ---- Draw loop ---------------------------------------------------------
[19:48:19.594] info ---- Cleanup -----------------------------------------------------------
The warning is the loader’s own: Sponza’s dirt decal is alpha-blended and there is no blended pass.
Appendix A — where each @@ comment is addressed
@@ request | File | Answered in |
|---|---|---|
| Pathtracing: remove the 3 temporary light values | shared_structs.h, vkapp_raytracing.cpp | The push constant |
Pathtracing: write SampleBrdf, PdfBrdf, PdfLight | raytrace.rgen | The BRDF; SampleLight, PdfLight, GeometryFactor |
| Pathtracing: initialize the pixel’s seed | raytrace.rgen | Random numbers |
| Pathtracing: the Monte-Carlo loop | raytrace.rgen | The Monte-Carlo loop |
| Pathtracing: break on a miss | raytrace.rgen | The Monte-Carlo loop |
| Pathtracing: accumulate a light’s emission times W | raytrace.rgen | The Monte-Carlo loop |
| Explicit: the same with a factor of ½ | raytrace.rgen | Combining the two with MIS |
| Explicit light connection goes here | raytrace.rgen | Project 4b |
| Pathtracing: accumulate f/p into W | raytrace.rgen | The Monte-Carlo loop |
| Pathtracing: accumulate C into the output pixel | raytrace.rgen | Accumulation |
| Build the list of lights | vkapp_loadModel.cpp | The emitter list |
| 4b: send the light list to the shader | vkapp_loadModel.cpp | The light buffer |
frameSeed, rr, depth, clear | 4a-path-tracing.pdf | The push constant; Accumulation |
m_lightBuff, binding 2, Emitter[] in raygen | 4b-explicit.pdf | The light buffer |
Appendix B — destroy / resize registry
Project 4’s resources, added to project 3’s registry.
| Resource | Destroyed | Resized | Rebuilt on scene switch |
|---|---|---|---|
m_lightBuff | yes | — | yes |
m_envCdfBuff | yes | — | with the sky bake |