11---
22author : mos9527
3- lastmod : 2025-12-26T17:46:49.839800
3+ lastmod : 2025-12-26T17:53:11.131525
44title : Foundation 施工笔记 【6】- 路径追踪
55tags : ["CG","Vulkan","Foundation"]
66categories : ["CG","Vulkan"]
@@ -704,31 +704,6 @@ glTF的该模型可以认为是和PBRT中的`DieletricBxDF`与`DiffuseBxDF`做
704704
705705这既是[ LayeredBxDF中用到的NEE/Next Event Estimation(次事件估计)的思想] ( https://pbr-book.org/4ed/Light_Transport_II_Volume_Rendering/Scattering_from_Layered_Materials#fragment-SamplenexteventforlayeredBSDFevaluationrandomwalk-0 ) 。而回顾我们之间讨论过的菲涅耳方程:我们很清楚有** 「多少」** 能量会到达下一层(然后反射),又有多少会被直接反射:_ 反射率_准确地表达了这样的比例!
706706
707- 接下来采样中对两个Lobe的混合也将这么做。在此之前还有一个问题:导体/电介质二者的混合应该怎么做?再次根据` metallic ` 值NEE可取,但其实不必如此...
708-
709- #### 导体/电介质合并
710-
711- 不难发现,这两者都的光泽BRDF仅依赖一个** 一样的** $\alpha$粗糙度:这意味着他们的不同,** 在且仅在于他们的菲涅耳值** ——从采样到PDF都是一样的。既然是比例混合,不妨直接** 线性混合他们最终的菲涅耳项** ,丢给同样的BRDF计算?
712-
713- 这正是诸多glTF实现中的做法——如此,我们将电介质的lobe和导体lobe等效地合并成一个glossy lobe。这也是为什么在接下来的实现中,你只能看到一次NEE的原因。NEE概率采样如下:
714-
715- ``` c++
716- // Fresnel sampling approximation of layered materials
717- // This is glTFFresnelMix in a statistical form.
718- public float glTFFresnelNEE (float NdotV, float ior = 1.5f)
719- {
720- float F0 = pow((ior - 1) / (ior + 1), 2); // IOR=1.5->0.04
721- return SchlickFresnel(F0, 1.0f, NdotV);
722- }
723- ```
724-
725- 但是这还不够:设想metallic=1的情况,完全金属——diffuse lobe会消失。毕竟是线性组合,我们对概率加权metallic即可:
726-
727- ```c++
728- float probGlossy = glTFFresnelNEE(ClampedCosTheta(wo));
729- probGlossy = lerp(probGlossy, 1.0f, metallic); // Fully metallic means there's no diffuse lobe
730- ```
731-
732707#### 菲涅耳项估计
733708
734709计算菲涅耳本身在之前介绍过——而前面用了` ShlickFresnel ` 。当然,mix ` FrDieletric ` 和` FrConductor ` 在这里是正确的...但用到的三角函数是不是有些多?
@@ -1173,167 +1148,7 @@ ggxE[dot(p, uint2(1, 32))] = float2(E / samples, Eprime / samples);
11731148
11741149ImageWorks也提到了对Diffuse lobe的调整(虽然这部分我们也讨论过了):$E\prime\prime$是补偿过的反射量,那么真正能到达底层diffuse lobe的能量即为$1-E\prime\prime$(回顾反射率关系),刚好允许我们进行正确的能量调整:漫反射一定有入射=出射,$1-E\prime\prime$则是混合glossy lobe后其正确的反照率。
11751150
1176- 至此电介质模型调整完毕,shader节选如下:
1177-
1178- ``` c++
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;
1217- }
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) {
1253- 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- }
1273-
1274-
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);
1331- }
1332- };
1333-
1334- ```
1335-
1336- 让metallic=0(全电介质)的效果如下:
1151+ 至此电介质模型调整完毕。让metallic=0(全电介质)的效果如下:
13371152
13381153![ image-20251225163746262] ( /image-foundation/image-20251225163746262.png )
13391154
@@ -1345,6 +1160,8 @@ public struct PrincipledBSDF /* glTF Core Spec ver */ : IBxDF {
13451160
13461161![ image-20251226172037447] ( /image-foundation/image-20251226172037447.png )
13471162
1163+ 实现部分还有很多细节,尽力也在注释中标注。这里就不贴出来了——有兴趣还请看仓库链接:https://github.com/mos9527/Foundation/blob/vulkan/Editor/Shaders/IBSDF.slang (可能有死链...届时请在在仓库搜索 [ PrincipledBSDF] ( https://github.com/search?q=repo%3Amos9527%2FFoundation%20PrincipledBSDF&type=code ) 然后..留个言提醒下? )
1164+
13481165#### 样张
13491166
13501167Tonemap部分和上一篇一致。此外这里没有透明度检测(sponza有decal需要)——这里需要any hit,是相当昂贵的一个操作。
0 commit comments