11---
22author : mos9527
3- lastmod : 2025-12-25T20:24:58.012672
3+ lastmod : 2025-12-26T17:46:49.839800
44title : Foundation 施工笔记 【6】- 路径追踪
55tags : ["CG","Vulkan","Foundation"]
66categories : ["CG","Vulkan"]
@@ -1159,7 +1159,7 @@ for (uint i = 0; i < kSamples;i++) {
11591159ggxE[dot(p, uint2(1, 32))] = float2(E / samples, Eprime / samples);
11601160```
11611161
1162- 跑出来是这样的。参照不太好找,不过 Filament [ 5.3.4.3 The DFG1 and DFG2 term visualize ] ( https://google.github.io/filament/Filament.md.html#lighting/imagebasedlights/processinglightprobes ) 做了类似的事情:事实上我们的$E\prime$和$DFG_2$是一致的,对比如下(作为对比: R 实际为(1-λ)* E, 同时这里的Y轴有翻转处理以对齐Filament结果)
1162+ 跑出来是这样的。参照不太好找,不过 Filament [ 5.3.4.3 The DFG1 and DFG2 term visualized ] ( https://google.github.io/filament/Filament.md.html#lighting/imagebasedlights/processinglightprobes ) 做了类似的事情:事实上我们的$E\prime$和$DFG_2$是一致的,对比如下(作为对比: R 实际为(1-λ)* E, 同时这里的Y轴有翻转处理以对齐Filament结果)
11631163
11641164| Filament | Ours |
11651165| -------------------------------------------- | ------------------------------------------------------------ |
@@ -1176,64 +1176,161 @@ ImageWorks也提到了对Diffuse lobe的调整(虽然这部分我们也讨论
11761176至此电介质模型调整完毕,shader节选如下:
11771177
11781178``` c++
1179- public BSDFSample Sample_f (float3 wo, float uc, float2 u, TransportMode, BxDFReflTransFlags) {
1180- float c_min_reflectance = 0.04f;
1181- // Mixed Metallic/Dielectric Fresnel F0
1182- const float3 F0 = lerp(float3(c_min_reflectance), baseColor, metallic);
1183- const float3 F90 = 1.0f; // As per glTF spec
1184- // Multi-scatter compensation
1185- // https://mos9527.com/posts/foundation/pt-6-path-tracing-adventures/#%E8%83%BD%E9%87%8F%E5%AE%88%E6%81%92%E6%94%B9%E8%BF%9B
1186- const float mu = AbsCosTheta(wo);
1187- const float2 EEp = ggxLutE.SampleLevel(lutSampler, float2(mu, roughness), 0);
1188- const float3 Epp = F0 * EEp.x + (F90 - F0) * EEp.y;
1189- float probGlossy = NEEGlossyProb(wo);
1190- if (uc < probGlossy) {
1191- // Sample Glossy
1192- if (mfDistrib.EffectivelySmooth()) {
1193- // Dirac delta case
1194- float3 wi = float3(-wo.x, -wo.y, wo.z); // = wr
1195- wi = FaceForward(wi, float3(0,0,1));
1196- float3 wm = normalize(wi + wo);
1197-
1198- float3 Fss = SchlickFresnel(F0, 1.0f, AbsDot(wo, wm));
1199- float3 Fms = F0 * (1/Epp - 1) * Fss;
1200- if (!energyCompensation)
1201- Fms = 0;
1202- float3 f = (Fss+Fms) / AbsCosTheta(wi);
1203- // Sampled PDF would be delta, but we represent them as 1s w/o weighting
1204- // With NEE this is what you get:
1205- float pdf = 1.0f * probGlossy;
1206- return BSDFSample(f, wi, pdf, BxDFFlags::SpecularReflection);
1207- } else {
1208- float3 wm = mfDistrib.Sample_wm(wo, u);
1209- float3 wi = Reflect(wo, wm);
1210- if (!SameHemisphere(wo, wi))
1211- return BSDFSample(); // Absorption
1212-
1213- float3 Fss = SchlickFresnel(F0, 1.0f, AbsDot(wo, wm));
1214- float3 Fms = F0 * (1/Epp - 1) * Fss;
1215- if (!energyCompensation)
1216- Fms = 0;
1217- float3 f = (Fms + Fss) * mfDistrib.D(wm) * mfDistrib.G(wo, wi) / (4 * AbsCosTheta(wi) * AbsCosTheta(wo));
1218-
1219- float pdf = probGlossy * mfDistrib.PDF(wo, wm) / (4 * AbsDot(wo, wm));
1220- return BSDFSample(f, wi, pdf, BxDFFlags::GlossyReflection);
1179+ import IBxDF;
1180+ import IMath;
1181+
1182+ [[vk_binding(0 ,1 )]] SamplerState lutSampler;
1183+ [[vk_binding(1 ,1 )]] Texture2D<float2> ggxLutE;
1184+
1185+ const static float MIN_ALPHA = 1e-3 ;
1186+ // Inspired by Blender's OSL implementation
1187+ // https://projects.blender.org/blender/blender/src/commit/96d715c643888c78e5dbaa8bd3c3c79ce599c0a3/intern/cycles/kernel/osl/shaders/node_principled_bsdf.osl#L15
1188+ public struct PrincipledBSDF /* glTF Core Spec ver * / : IBxDF {
1189+ float3 baseColor;
1190+ float metallic;
1191+ float roughness;
1192+ bool energyCompensation;
1193+
1194+ TrowbridgeReitzDistribution mfDistrib;
1195+
1196+ public __init(float3 baseColor, float metallic, float roughness, bool energyCompensation = true) {
1197+ this.baseColor = baseColor;
1198+ this.metallic = metallic;
1199+ this.roughness = roughness;
1200+ this.energyCompensation = energyCompensation;
1201+
1202+ float alpha = max(MIN_ALPHA, roughness * roughness);
1203+ this.mfDistrib = TrowbridgeReitzDistribution(alpha, alpha);
1204+ }
1205+
1206+ public BxDFFlags Flags () {
1207+ return BxDFFlags::Reflection | BxDFFlags::Glossy;
1208+ }
1209+
1210+ private float3 TorranceSparrowPreserveEnergy(float3 wo, float3 wi, float3 F0, float3 F90, float2 lutE) {
1211+ float3 wm = normalize(wo + wi);
1212+ float3 Fss = SchlickFresnel(F0, F90, AbsDot(wo, wm));
1213+ float3 Fms = 0.0f;
1214+ if (energyCompensation) {
1215+ float3 Epp = F0 * lutE.x + (F90 - F0) * lutE.y;
1216+ Fms = F0 * (1.0f / Epp - 1.0f) * Fss;
12211217 }
1222- } else {
1223- // Sample Diffuse
1224- float3 wi = SampleCosineHemisphere(u);
1225- wi = FaceForward(wi, float3(0,0,1));
1218+ return (Fss + Fms) * mfDistrib.D(wm) * mfDistrib.G(wo, wi) / (4.0f * AbsCosTheta(wo) * AbsCosTheta(wi));
1219+ }
1220+
1221+ public SampledSpectrum f(float3 wo, float3 wi, TransportMode mode) {
1222+ if (wo.z <= 0 || wi.z <= 0) return 0; // Reflection only
1223+ float2 lutE = ggxLutE.SampleLevel(lutSampler, float2(AbsCosTheta(wo), roughness), 0);
1224+
1225+ float3 dielF0 = float3(0.04f);
1226+ float3 dielF90 = float3(1.0f);
1227+ float3 dielBSDF = TorranceSparrowPreserveEnergy(wo, wi, dielF0, dielF90, lutE);
1228+ // 1 - R
1229+ // A helpful assumption is that the energy entering the diffuse lobe *always*
1230+ // gets out uniformly.
1231+ // A real mixing node would do a Random Walk with volume attenuation. Check LayeredBxDF in IBxDF.slang
1232+ float3 dielEpp = dielF0 * lutE.x + (dielF90 - dielF0) * lutE.y;
1233+ float3 diffuseBSDF = baseColor * InvPi;
1234+ if (energyCompensation) // Transmitted energy
1235+ diffuseBSDF *= (1.0f - dielEpp);
1236+
1237+ // Base Layer
1238+ float3 bsdf = dielBSDF + diffuseBSDF;
1239+
1240+ // Metal
1241+ // There's no diffuse lobe anymore (completely absorbed!)
1242+ // Blender has a F82 tint model for modeling F0, but for convenience's sake
1243+ // (since glTF never does that) we'll use baseColor for that.
1244+ float3 metalF0 = baseColor;
1245+ float3 metalF90 = float3(1.0f);
1246+ float3 metalBSDF = TorranceSparrowPreserveEnergy(wo, wi, metalF0, metalF90, lutE);
1247+ bsdf = lerp(bsdf, metalBSDF, metallic);
1248+
1249+ return bsdf;
1250+ }
1251+
1252+ public float PDF(float3 wo, float3 wi, TransportMode mode, BxDFReflTransFlags flags) {
12261253 float3 wm = normalize(wo + wi);
1254+ // Probability of choosing glossy vs diffuse
1255+ // This is not physically *accurate*, as this uses the single-scattering
1256+ // transmittance approximation for the dielectric layer only.
1257+ // See also previous 1-Epp for diffuse energy.
1258+ float sampleGlossy = SchlickFresnel(0.04f, 1.0f, AbsCosTheta(wo));
1259+
1260+ // Component PDFs
1261+ float pdfGlossy = mfDistrib.PDF(wo, wm) / (4.0f * AbsDot(wo, wm));
1262+ float pdfDiffuse = CosineHemispherePDF(AbsCosTheta(wi));
1263+ if (mfDistrib.EffectivelySmooth()) pdfGlossy = 0.0f; // Delta handling
1264+
1265+ // Base Layer
1266+ float pdf = sampleGlossy * pdfGlossy + (1.0f - sampleGlossy) * pdfDiffuse;
1267+
1268+ // Metal
1269+ float metal = pdfGlossy;
1270+ pdf = lerp(pdf, metal, metallic);
1271+ return pdf;
1272+ }
12271273
1228- float3 cdiff = baseColor * (1.0f - metallic);
1229- float3 f = cdiff * InvPi;
1230- if (energyCompensation)
1231- f *= 1 - Epp;
12321274
1233- float pdf = (1 - probGlossy) * CosineHemispherePDF(ClampedCosTheta(wi));
1234- return BSDFSample(f, wi, pdf, BxDFFlags::DiffuseReflection);
1275+ public BSDFSample Sample_f(float3 wo, float uc, float2 u, TransportMode mode, BxDFReflTransFlags flags) {
1276+ float3 wi;
1277+ BxDFFlags sampledFlag;
1278+
1279+ float glossy = SchlickFresnel(0.04f, 1.0f, AbsCosTheta(wo));
1280+ bool isGlossy = false;
1281+ bool isMetal = false;
1282+ // Hierarchical sampling
1283+ // Select scales the uc term as it goes - don't worry about the uniformity
1284+ if (Select(uc, metallic))
1285+ isMetal = isGlossy = true;
1286+ else {
1287+ if (Select(uc, glossy))
1288+ isGlossy = true;
1289+ }
1290+
1291+ if (isGlossy) {
1292+ // Glossy (dielectric/metal) sample
1293+ if (mfDistrib.EffectivelySmooth()) {
1294+ // Delta case. This is not possible to be generated by f() or PDF()
1295+ // and this case - in itself - is discrete.
1296+ float2 lutE = ggxLutE.SampleLevel(lutSampler, float2(AbsCosTheta(wo), roughness), 0);
1297+ wi = float3(-wo.x, -wo.y, wo.z);
1298+ wi = FaceForward(wi, float3(0,0,1));
1299+
1300+ // Mixing F0 stops making sense here as we rely on it to calculate
1301+ // energy compensation terms.
1302+ // NVPRO examples mixes F0 to express this mixture only because they're single-scattering.
1303+ // Thus we make metal/dielectric mix discrete events as well.
1304+ float3 F0 = isMetal ? baseColor : float3(0.04f);
1305+ float3 F90 = float3(1.0f);
1306+
1307+ float3 Epp = F0 * lutE.x + (F90 - F0) * lutE.y;
1308+ float3 Fss = SchlickFresnel(F0, F90, AbsDot(wo, normalize(wi+wo)));
1309+ float3 Fms = energyCompensation ? (F0 * (1.0f/Epp - 1.0f) * Fss) : float3(0);
1310+
1311+ float pdf = lerp(glossy, 1.0f, metallic);
1312+ // vvv Handle PDF like other PBRT impls. Base event is delta -> 1
1313+ pdf = 1.0f * pdf;
1314+
1315+ return BSDFSample((Fss + Fms) / AbsCosTheta(wi), wi, pdf, BxDFFlags::SpecularReflection);
1316+ }
1317+ float3 wm = mfDistrib.Sample_wm(wo, u);
1318+ wi = Reflect(wo, wm);
1319+ sampledFlag = BxDFFlags::GlossyReflection;
1320+ } else {
1321+ // Diffuse Sample
1322+ wi = SampleCosineHemisphere(u);
1323+ wi = FaceForward(wi, float3(0,0,1));
1324+ sampledFlag = BxDFFlags::DiffuseReflection;
1325+ }
1326+
1327+ if (!SameHemisphere(wo, wi)) return BSDFSample();
1328+ SampledSpectrum val = this.f(wo, wi, mode);
1329+ float pdf = this.PDF(wo, wi, mode, flags);
1330+ return BSDFSample(val, wi, pdf, sampledFlag);
12351331 }
1236- }
1332+ };
1333+
12371334```
12381335
12391336让metallic=0(全电介质)的效果如下:
@@ -1244,37 +1341,33 @@ public BSDFSample Sample_f(float3 wo, float uc, float2 u, TransportMode, BxDFRef
12441341
12451342最后,调整完能量守恒前后的该模型在白炉测试中效果如下:
12461343
1247- |  |  |
1248- | ------------------------------------------------------------ | ------------------------------------------------------------ |
1344+ ![ image-20251225180916282] ( /image-foundation/image-20251225180916282.png )
12491345
1250- ##### 遗留问题
1346+ ![ image-20251226172037447 ] ( /image-foundation/image-20251226172037447.png )
12511347
1252- - 积累地足够久图像会以某种奇怪的规律变暗? **UPD:** 解决:是精度问题。积累buffer就别省着用FP16了...换成FP32解决
1253- 
1348+ #### 样张
12541349
1255- - nvpro-samples 中见到一个[限制路径roughness消除firefly的trick](https://github.com/nvpro-samples/vk_gltf_renderer/blob/master/shaders/gltf_pathtrace.slang#L761),但是不知道为什么这样有效(限制PDF?需要考证)
1350+ Tonemap部分和上一篇一致。此外这里没有透明度检测(sponza有decal需要)——这里需要any hit,是相当昂贵的一个操作。
12561351
1257- 前后对比如下(注意左上角!)
1258-
1259- ```c++
1260- // Keep track of the maximum roughness to prevent firefly artifacts
1261- // by forcing subsequent bounces to be at least as rough
1262- maxRoughness = max(pbrMat.roughness, maxRoughness);
1263- pbrMat.roughness = maxRoughness;
1264- ```
1352+ 灯光采样用到了 MIS(虽然就太阳光+环境光),但PBRT灯光采样章节只是略过看了下。期末结束放假再来搞搞IBL或者是many light...
12651353
1266- | ![ image-20251225181555333] ( /image-foundation/image-20251225181555333.png ) | ![ image-20251225181601057] ( /image-foundation/image-20251225181601057.png ) |
1267- | ------------------------------------------------------------ | ------------------------------------------------------------ |
1354+ ##### referencePT Bathroom
12681355
1269- #### 样张
1356+ 很英伟达的浴室,来自 Ray Tracing Gems 2 提到的 https://github.com/boksajak/referencePT/tree/master/models/bathroom
1357+
1358+ ![ image-20251226174059253] ( /image-foundation/image-20251226174059253.png )
12701359
1271- 嗯..有机会在blender摸鱼了。这里等捏出来几个场景后陆续添加图片...
1360+ 为参考起见,以下是Blender Cycles在同样场景的渲染结果。后者开启降噪,Tonemapper为ACES1.3
12721361
1273- 注意没有透明度/降噪:这一篇文字已经够长了;此外,Tonemap部分和上一篇一致。
1362+ <details >
1363+ <summary >Cycles 参考</summary >
1364+ <img src =" /image-foundation/image-20251226174553084.png " ></img >
1365+ </details >
1366+ 不过这并非书上测试用的室内环境——有机会再添加后者。
12741367
12751368##### Intel Sponza
12761369
1277- ![ image-20251225202112425 ] ( /image-foundation/image-20251225202112425 .png )
1370+ ![ image-20251226173107515 ] ( /image-foundation/image-20251226173107515 .png )
12781371
12791372Link: https://github.com/mos9527/Scenes?tab=readme-ov-file#intel-gpu-research-samples---sponza
12801373
0 commit comments