Skip to content

Commit 091ccb4

Browse files
committed
upd
1 parent 1546b88 commit 091ccb4

1 file changed

Lines changed: 4 additions & 187 deletions

File tree

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

Lines changed: 4 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
author: mos9527
3-
lastmod: 2025-12-26T17:46:49.839800
3+
lastmod: 2025-12-26T17:53:11.131525
44
title: Foundation 施工笔记 【6】- 路径追踪
55
tags: ["CG","Vulkan","Foundation"]
66
categories: ["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

11741149
ImageWorks也提到了对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

13501167
Tonemap部分和上一篇一致。此外这里没有透明度检测(sponza有decal需要)——这里需要any hit,是相当昂贵的一个操作。

0 commit comments

Comments
 (0)