Skip to content

Commit 1546b88

Browse files
committed
upd
1 parent 9d05bfb commit 1546b88

9 files changed

Lines changed: 168 additions & 75 deletions

content/posts/foundation/pt-6-path-tracing-adventures.md

Lines changed: 168 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
author: mos9527
3-
lastmod: 2025-12-25T20:24:58.012672
3+
lastmod: 2025-12-26T17:46:49.839800
44
title: Foundation 施工笔记 【6】- 路径追踪
55
tags: ["CG","Vulkan","Foundation"]
66
categories: ["CG","Vulkan"]
@@ -1159,7 +1159,7 @@ for (uint i = 0; i < kSamples;i++) {
11591159
ggxE[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-
| ![image-20251225180916282](/image-foundation/image-20251225180916282.png) | ![image-20251225180846683](/image-foundation/image-20251225180846683.png) |
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-
![image-20251225181106479](/image-foundation/image-20251225181106479.png)
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

12791372
Link: https://github.com/mos9527/Scenes?tab=readme-ov-file#intel-gpu-research-samples---sponza
12801373

-157 KB
Binary file not shown.
-1.85 MB
Binary file not shown.
-1.85 MB
Binary file not shown.
-1.81 MB
Binary file not shown.
143 KB
Loading
1.57 MB
Loading
1.77 MB
Loading
2.25 MB
Loading

0 commit comments

Comments
 (0)