![]()
【USparkle專欄】如果你深懷絕技,愛“搞點研究”,樂于分享也博采眾長,我們期待你的加入,讓智慧的火花碰撞交織,讓知識的傳遞生生不息!
這是侑虎科技第1971篇文章,感謝作者hulalalala供稿。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯系我們,一起探討。(QQ群:793972859)
作者主頁:
https://www.zhihu.com/people/ou-yang-xuan-58-47
HPWater BSDF - 稍微基于物理的水體散射模型
實時水體渲染BSDF模型,支持體積散射、薄層SSS和背光透射。
![]()
本模型的散射模擬
![]()
直射光5°恒定厚度
![]()
直射光15°恒定厚度
![]()
直射光30°恒定厚度
![]()
第一張(厚度近似)后三張(不同直射光角度)
![]()
海浪厚度近似(法線)這個近似并不準確,但將就著用
![]()
可以看到不同厚度下,薄層SSS和透射經過不同厚度介質時,前向散射的分散性導致的散射形狀
![]()
不同光向下的厚度近似模擬
與GGX BRDF的類比
本模型的結構設計參考了經典的GGX微表面BRDF,兩者都將光照分解為幾個物理項的乘積:
![]()
GGX鏡面公式:
![]()
水體散射公式:
![]()
兩者的共同點:
都用幾何項G表示入射光的有效投影面積
都用菲涅爾項F/T處理界面的能量分配
都將復雜光照分解為可獨立計算的物理項相乘
主要區別:
GGX處理的是表面微觀幾何的反射
水體BSDF處理的是體積介質的散射和透射
水體需要區分反射方向(diffR)和透射方向(diffT)
輸入參數
相比GGX BRDF(只需要roughness和fresnel0),水體BSDF需要額外的體積散射參數。
材質參數(BSDFData):
![]()
體積散射參數(WaterLightLoopData):
![]()
派生量:
消光系數:μt = μa + μs(absorptionColor + scatterColor)
散射反照率:ω = μs / μt(決定吸收/散射比例)
光學深度:γ = μt · d(決定介質的“不透明度”)
整體概述
水體的光照與普通材質不同。光進入水中后會被吸收和散射,部分光在水體內部多次彈射后從表面出射回到攝像機。這個過程涉及:入射折射、體積散射、出射透射三個階段。
本模型將水體散射分解為兩個輸出通道(遵循HDRP的CBSDF結構)。
關于diffR/diffT的命名:
在HDRP的CBSDF中,R = Reflection(反射方向),T = Transmission(透射方向):
diffR:光從表面的“同一側”出射(入射和出射在法線同側)
diffT:光從表面的“另一側”穿過來(入射和出射在法線兩側)
對于水體:
diffR(宏觀體積散射):光從正面入射 → 水下散射 → 正面出射。光路在法線同側,屬于“反射方向”。適用于深水區域,通過Ray Marching累加散射。
diffT(薄層散射):光從背面/側面穿過薄層出射。光路穿過表面,屬于“透射方向”。包含:
thinLayerSSS:近似深水散射在薄層區域的延續,側面和背面可見
backlitTransmission:光從背面直接穿透薄水層,看向光源時產生輝光
三個組件通過入射幾何項自然分離,避免能量重復計算。
// ============================================================================
// 水體BSDF模型概述
// ============================================================================
//
// 【物理場景:圓錐形薄層 + 深水區域】
//
// ? 光源
// |
// 波峰(薄層) thickness ≈ 0
// /\
// / \ ← 薄層:透射率高,fallback到深水
// / \
// / \ thickness漸變
// /________\ ← 圓錐底部,thickness = 1(薄層與深水邊界)
// | |
// | 深水區 | ← ray marching 區域(macroScattering)
// |__________|
// ↓
// 水底
//
// 【散射模型分解】
//
// 1. 宏觀體積散射 (diffR):
// - 正面入射 → 水下 ray marching → 正面出射
// - 處理深水區域的體積散射
//
// 2. 薄層散射 (diffT):
// - 薄層 SSS:近似深水散射在薄層區域的延續
// - 背光透射:光直接穿透薄層,強前向散射
// - 處理波峰、浪花等薄水區域
//
// 【薄層與深水的過渡】
//
// 薄層(thickness 小)→ 透射率高 → 直接使用深水散射顏色(S_volume)
// 厚層(thickness 大)→ 透射率低 → 使用薄層 SSS(近似深水累積)
//
// 通過 sss_transmittance 自動混合兩者
//
// 【出射菲涅爾】
//
// T_exit = 1 - F(NdotV) 在 PostEvaluateBSDF 末尾統一應用
//
// ============================================================================渲染流程總結
Step 1:入射計算
計算三個入射幾何項(G_entry、G_sss、G_backlit)和入射菲涅爾透射(T_entry),決定多少光進入水體、進入哪個散射路徑。
Step 2:體積散射(diffR)
對深水區域進行少量Ray Marching,累加多次散射的光量,輸出S_volume。
Step 3:薄層SSS(diffT的一部分)
對薄層區域計算散射光量S_sss,使用非線性光程修正處理不同厚度,并與深水散射混合。
Step 4:背光透射(diffT的一部分)
計算光從背面穿透薄層的透射光,使用極強前向相位函數。
Step 5:出射透射
所有散射光經過出射菲涅爾透射T_exit后到達攝像機。
最終輸出:
diffR = G_entry × T_entry × S_volume
diffT = (G_sss × S_sss) + (G_backlit × T_backlit × P_backlit)
output = (diffR + diffT) × T_exitStep 1:入射計算
1.1 菲涅爾透射(Fresnel Transmission)
光在界面上一部分被反射,剩余部分透射進入水中。透射比例由菲涅爾方程決定。
數學公式:
![]()
水的f0≈0.02,即垂直入射時只有2%的光被反射。
代碼實現:
// HPWaterBSDFLibary.hlsl: Line 200
float3 T_entry = 1.0 - F_Schlick(bsdfData.fresnel0, clampedNdotL);1.2 入射幾何項(Incident Geometry Terms)
三個組件各自有獨立的入射幾何項,根據NdotL的正負自然分配能量,避免重復計算。
數學公式:
![]()
代碼實現:
// HPWaterBSDFLibary.hlsl
float G_entry = clampedNdotL; // Line 204
float G_sss = 1.0 - G_entry; // Line 309
float G_backlit = saturate(-NdotL); // Line 375分工表:
![]()
解釋:
正面(NdotL>0):大部分光穿過薄層進入深水,由diffR處理
側面(NdotL≈0):diffR無貢獻,薄層散射thinLayerSSS主導
背面(NdotL<0):光從背后入射,thinLayerSSS和backlitTransmission共同作用
Step 2:體積散射(Volume Scattering)
深水區域使用Ray Marching沿視線采樣,累加每一步的散射光量。循環次數默認6次。
物理過程:
1. 光進入水后沿視線傳播
2. 每走一小步,部分光被吸收、部分被散射
3. 散射的光中,朝向攝像機的部分被累加
4. 重復直到到達水底或視線結束
2.1 Beer-Lambert透射
光在介質中傳播時按指數衰減。
公式:
![]()
代碼實現:
// HPWaterVolumetrics.hlsl: Line 202-205
float3 extinctionCoeff = absorptionCoeff + scatteringCoeff;
transmittance = exp(-extinctionCoeff * crossDistance);2.2 散射光計算
被消光的光中,一部分被吸收(變成熱量),另一部分被散射(改變方向)。
公式:
![]()
L:入射光
(1?T): 被消光的光的比例(積分結果)
μs / μt:散射反照率(被消光的光中有多少是散射而非吸收)
P(θ):相位函數(散射光中朝向攝像機的比例)
代碼實現:
// HPWaterVolumetrics.hlsl: Line 199-218
float3 CaculateScatteredLight(
float3 originLight, // L: 入射光
float3 absorptionCoeff, // μa: 吸收系數
float3 scatteringCoeff, // μs: 散射系數
float crossDistance, // d: 光程
float3 phase, // P(θ): 相位函數
out float3 transmittance)
{
// μt = μa + μs
float3 extinctionCoeff = absorptionCoeff + scatteringCoeff;
// T = exp(-μt × d)
transmittance = exp(-extinctionCoeff * crossDistance);
// (1 - T): 被消光的光量
float3 extinguishedLight = originLight * (1.0 - transmittance);
// μs / μt: 散射反照率
float3 scatteringAlbedo = scatteringCoeff * rcp(extinctionCoeff);// S = L × (1-T) × (μs/μt) × P(θ)
float3 scatteredLight = extinguishedLight * scatteringAlbedo * phase;
return scatteredLight;
}
2.3 Henyey-Greenstein相位函數
相位函數描述光散射后的方向分布。水體主要是前向散射。
數學公式:
![]()
g = 0.8:強前向散射(水體典型值)
g = 0:各向同性
g < 0:后向散射
代碼實現:
// HPWaterVolumetrics.hlsl: Line 171-177
float HenyeyPhase(float cosTheta, float phaseG)
{
float g2 = phaseG * phaseG;
float denom = 1.0 + g2 - 2.0 * phaseG * cosTheta;
float mieScatter = (1.0 - g2) * rcp(pow(abs(denom), 1.5));
return mieScatter;
}2.4 混合相位函數(瑞利 + 米氏)
真實水體同時存在瑞利散射(分子級,波長相關,產生藍色)和米氏散射(顆粒級,前向散射)。
數學公式:
![]()
代碼實現:
// HPWaterVolumetrics.hlsl: Line 187-196
float3 CaculateScatterPhase(float cosTheta, float phaseG)
{
// 瑞利散射(5%):βR ∝ 1/λ?,短波長(藍光)散射更強
static const float3 betaRayleigh = float3(5.8e-6, 13.5e-6, 33.1e-6);
float rayleighPhase = (1.0 + cosTheta * cosTheta) * (3 / (16 * PI));
float3 rayleighScatter = betaRayleigh * rayleighPhase * 1e6;
// 米氏散射(95%)
float mieScatter = HenyeyPhase(cosTheta, phaseG);// 混合
float3 scatterPhase = rayleighScatter * 0.05 + mieScatter * 0.95;
return scatterPhase;
}
2.5 Ray Marching主循環
輻射傳輸方程的數值積分:
理論上,沿視線累加的散射光是一個積分:
![]()
其中S(x)是位置x處的散射光源項,T(x → eye)是從x到眼睛的透射率。
這個積分沒有解析解,所以用Ray Marching做數值近似(黎曼求和):
![]()
指數步進的數學推導:
近處對視覺貢獻大(細節可見),遠處貢獻小(被衰減)。用指數步進可以在采樣數相同時獲得更好的精度。
設t∈[0,1] 是歸一化采樣索引(t=i/N),k=ln(EXP_FACTOR):
![]()
當t=0時d=0,當t=1時d=0。步長為導數:
![]()
代碼實現優化:
令currentExp=e^(k·t)=EXP_FACTOR^t,則:
d=(currentExp-1)*kDenom
dd=currentExp*kDD
每次迭代currentExp*=expStep(其中expStep=EXP_FACTOR^(1/N))
// HPWaterVolumetrics.hlsl: Line 294-345
// 指數步進參數預計算
float rcpCount = rcp(float(WATER_SAMPLE_COUNT)); // 1/N
float kDenom = rcp(EXP_FACTOR - 1.0); // 1/(e^k - 1)
float kDD = log(EXP_FACTOR) * rcpCount * kDenom; // k/(N·(e^k-1))
float expStep = pow(EXP_FACTOR, rcpCount); // e^(k/N)
float currentExp = pow(EXP_FACTOR, Dither * rcpCount); // 抖動起始點
//WATER_SAMPLE_COUNT = 6
for (int i = 0; i < WATER_SAMPLE_COUNT; i++)
{
// d: 歸一化采樣位置 [0,1],dd: 當前步的歸一化步長
float d = (currentExp - 1.0) * kDenom;
float dd = currentExp * kDD;
float3 samplePos = RayStart + NoLinearRayDirection * d;
float3 samplePosDynamic = RayStart + DynamicRayDirection * d;
// 計算陰影
shadowValue = ComputeShadowValue(samplePosDynamic, featureFlags, lightLoopContext, posInput, bsdfData);
if(i == 0) lastShadowValue = shadowValue;
// ===== 一次散射:直接光照 =====
float cosTheta = dot(safeNormalize(samplePos), safeNormalize(LightDir));
float3 scatterPhase = CaculateScatterPhase(cosTheta, _PhaseG);
float3 directLighting = LightColor * shadowValue;
float directCrossDistance = dd * NoLinearRayLength + SunDepth * dd;
// 先計算不帶相位的基礎散射(phase = 1)
float3 baseScatter = CaculateScatteredLight(
directLighting, AbsorptionCoefficient, ScatterCoefficient,
directCrossDistance, 1, transmittance, scatteringAlbedo).rgb;
// 多次散射效果:根據散射反照率決定相位的各向同性程度
// scatterAlbedo 高 -> 多次散射充分 -> 相位趨于各向同性
// scatterAlbedo 低 -> 單次散射主導 -> 保留方向性相位
float scatterAlbedo = Luminance(scatteringAlbedo) * GetInverseCurrentExposureMultiplier();
float3 effectivePhase = lerp(scatterPhase, 1.0, saturate(smoothstep(0, 0.5, scatterAlbedo)));
float3 directScatteredLight = baseScatter * effectivePhase;
// 累加
accumTransmittance *= transmittance;
scatteredLight += directScatteredLight;
currentExp *= expStep;
}// ===== 水下場景散射(Scene In-Scattering)=====
// 水下物體反射的光,穿過水體時也會被散射,產生「霧化」效果
float3 sceneLight = SceneColor * lerp(shadowValue, 1, 0.3);
float lightIntensity = Luminance(LightColor);
float3 sceneScatteredLight = sceneLight * accumTransmittance * ScatterCoefficient * NoLinearRayLength * lightIntensity;
scatteredLight += sceneScatteredLight;
解釋:
EXP_FACTOR控制指數步進的疏密程度,近處采樣密集以捕捉細節
Dither用于抖動起始點,減少帶狀Artifact
effectivePhase在厚介質中趨于各向同性,模擬多次散射后方向性丟失
水下場景散射公式:
![]()
這是一個簡化近似(避免對場景光再做完整Ray Marching):
Lscene:水下場景光(SceneColor,考慮30%陰影穿透)
Taccum:累積透射率(場景光穿過水體的衰減)
μs:散射系數(決定多少光被散射出來)
d:光程長度(路徑越長散射越多)
Isun:光源亮度(調制系數,使場景散射與直接光散射亮度一致)
物理效果:讓遠處的水下物體看起來更模糊、更偏向水的散射色(霧化)。
Step 3:薄層SSS(Thin Layer Subsurface Scattering)
薄層(如波峰、浪花)厚度無法直接得知,只能用高度或法線近似。關鍵是正確估算光程,并處理與深水散射的過渡。
輸入參數thickness:
thickness是歸一化的厚度值,范圍 [0, 1]:
0=極薄(波峰頂部、浪花邊緣)
1=最厚(薄層與深水區域的邊界)
通常由高度場、法線或其他幾何信息近似得到。代碼中乘以HPWATER_SSS_PATH_SCALE(默認20米)轉換為等效光程。
薄層與深水的幾何關系:
波峰(薄層頂部)thickness ≈ 0
/\
/ \
/ \ ← 薄層區域
/ \
/────────\ ← thickness = 1(薄層底部 = 深水頂部)
│ │
│ 深水區 │ ← S_volume(ray marching 累積)
│__________│薄層的底部與深水區域連接。當thickness接近1時,薄層實際上“看到”的是深水區域累積的散射光。因此:
薄層SSS不是獨立計算,而是近似深水散射在薄層區域的延續
光程縮放(20米)較大,是因為要匹配深水Ray Marching的累積效果
Fallback機制:當thickness很小(透射率高)時,直接使用S_volume作為薄層的顏色
為什么相機與光源角度相近時,水面背面會稍亮?
典型場景:夕陽下,人以較低視角看水面。此時相機方向V和光源方向L角度相近(都是低角度),水面波峰的背面會呈現微微透亮的效果。
這是相位函數、入射幾何項和出射菲涅爾透射三者共同作用的結果:
1. 相位函數P(cosθ)的前向散射
cosθ=dot(-V, L),V和L角度越近,cosθ越大
HG相位函數隨cosθ增大而增大(前向散射特性)
散射光更多地朝向攝像機方向
2. 入射幾何項G_sss在背面有值
背面入射時NdotL<0,導致G_entry=0
因此G_sss=1-G_entry=1,薄層SSS獲得入射能量
正面入射時G_sss減小,薄層貢獻減少
3. 出射菲涅爾透射fresnelTransmissionExit
T_exit=1-F_Schlick(f0, NdotV)
法線朝向相機(NdotV大)→ T_exit大 → 壓制小,光容易出射
法線不朝向相機(NdotV小)→ T_exit小 → 壓制大,光難以出射
波峰背面的法線相對更朝向相機,所以T_exit較大,散射光能出射
三者的平衡:
夕陽低角度 + 人高視角看水面:
入射:G_sss ≈ 0.6~1(背面/側面入射)
↓
散射:P_sss 中等偏高(V、L 角度相近但非正對)
↓
出射:T_exit ≈ 0.7~0.9(低視角有所衰減)三者相乘 → 背面稍亮(自然的透光效果)
這就是為什么波峰薄處在這種視角下會呈現柔和的透光效果。
3.1 非線性光程修正(Nonlinear Path Length)
薄層SSS用單次計算近似深水散射的延續。光程需要隨thickness增加而增長,以匹配深水Ray Marching的累積效果。
物理直覺:
薄層(d→0):透射率高,fallback到S_volume,光程影響小
厚層(d→1):需要更長光程來匹配深水累積散射的視覺效果
數學公式:
![]()
代碼實現:
// HPWaterBSDFLibary.hlsl: Line 264-280
// 線性部分:薄層區域
float L_linear = thickness * HPWATER_SSS_PATH_SCALE;
// 非線性部分:厚層區域,d2 讓光程在 thickness→1 時增長更快
float scatterStrength = Luminance(bsdfData.scatterColor);
float L_nonlinear = thickness * thickness * HPWATER_SSS_PATH_SCALE
* (1.0 + scatterStrength);
// 光學深度決定混合比例,τ 大時使用非線性
float opticalDepth = sss_extinctionScalar * thickness * HPWATER_SSS_PATH_SCALE;
float nonlinearWeight = saturate(opticalDepth * HPWATER_SSS_NONLINEAR_STRENGTH);// 有效光程
float sssPathLength = lerp(L_linear, L_nonlinear, nonlinearWeight);
3.2 薄層散射計算與深水混合
薄層SSS是深水散射在薄層區域的延續。根據透射率與深水散射混合:透射率高說明水很薄,應該直接看到深水的散射顏色。
數學公式:
![]()
代碼實現:
// HPWaterBSDFLibary.hlsl: Line 282-296
// P_sss:相位函數
float sss_cosTheta = dot(-V, L);
float3 P_sss = CaculateScatterPhase(sss_cosTheta, _PhaseG);
// S_sss:散射光計算
float3 sss_transmittance;
float3 S_sss = WaterVolumeLightLoop::CaculateScatteredLight(
LightColor, bsdfData.absorptionColor, bsdfData.scatterColor,
sssPathLength, P_sss, sss_transmittance);
// HPWaterBSDFLibary.hlsl: Line 309-329
// G_sss:薄層入射項 = 1 - 正面入射項
float G_sss = 1.0 - G_entry;
// 薄層 SSS 輸出 = 入射項 × 散射 × 陰影 × 補償
float3 thinLayerSSS = S_sss * G_sss * lastShadowValue * HPWATER_SSS_SCATTER_BOOST;// --------------------------------
// 薄層與深水的混合(Fallback 機制)
// --------------------------------
//
// 薄層區域(thickness 小):
// └─ transmittance 高 → sssWeight 低
// └─ 直接使用深水散射顏色(S_volume)
//
// 厚層區域(thickness 大):
// └─ transmittance 低 → sssWeight 高
// └─ 使用薄層 SSS(近似深水累積散射的延續)
//
// 注意:兩個分支都乘以 G_sss,保持入射分工一致
//
float sssWeight = saturate(1.0 - Luminance(sss_transmittance));
thinLayerSSS = lerp(S_volume * G_sss, thinLayerSSS, sssWeight);
解釋:
G_sss=1-G_entry確保正面入射時薄層SSS為 0,能量由diffR處理
HPWATER_SSS_SCATTER_BOOST補償單次計算與深水Ray Marching的亮度差異
Fallback機制讓波峰頂部(極薄)直接看到深水散射顏色
兩個分支都乘以G_sss,保證入射分工一致
Step 4:背光透射(Backlit Transmission)
當對準光源觀察時,光從背后穿透薄水層產生輝光。這是對準太陽時水面發亮的原因。
與薄層SSS的區別:
薄層SSS:光散射后出射,方向較分散(g≈0.8)
背光透射:光幾乎直穿,極強方向性(g≈0.998)
數學公式:
![]()
其中:
Gbacklit=clamp(?N?L,0,1):背面入射投影
Tbacklit=e^(?μt?d):Beer-Lambert透射
Pbacklit=PHG(cosθ,0.998):極強前向相位
代碼實現:
// HPWaterBSDFLibary.hlsl: Line 370-392
// G_backlit:背面入射投影
float G_backlit = saturate(-NdotL);
// 背光專用光程(比 SSS 短,純穿透路徑)
float backlitPathLength = thickness * HPWATER_BACKLIT_PATH_SCALE;
// T_backlit:Beer-Lambert 透射
float3 T_backlit = exp(-sss_extinctionCoeff * backlitPathLength);
// P_backlit:極強前向相位(g = 0.998)
float backlit_cosTheta = dot(V, -L);
float P_backlit = HenyeyPhase(backlit_cosTheta, 0.998);// 輸出
float3 backlitTransmission = LightColor * G_backlit * T_backlit * P_backlit * lastShadowValue;
解釋:
G_backlit只在背面(NdotL<0)有值
backlit_cosTheta=dot(V, -L)是攝像機看向光源穿透方向的夾角
g=0.998意味著只有幾乎正對光源時才能看到背光透射
HPWATER_BACKLIT_PATH_SCALE比SSS的scale小,因為背光是純穿透
Step 5:出射透射(Exit Transmission)
散射光從水內出射時,再次經過菲涅爾透射。
數學公式:
![]()
代碼實現:
// HPWaterBSDFLibary.hlsl: Line 98 (PostEvaluateBSDF)
float3 fresnelTransmissionExit = 1.0 - F_Schlick(bsdfData.fresnel0, clampedNdotV);
lightLoopOutput.diffuseLighting *= fresnelTransmissionExit;最終輸出組合
數學公式:
![]()
代碼實現:
// HPWaterBSDFLibary.hlsl: Line 207, 220
float3 underwaterLight = G_entry * T_entry;
float3 macroScattering = S_volume * underwaterLight;
// HPWaterBSDFLibary.hlsl: Line 312
float3 thinLayerSSS = S_sss * G_sss * lastShadowValue * HPWATER_SSS_SCATTER_BOOST;
// HPWaterBSDFLibary.hlsl: Line 392
float3 backlitTransmission = LightColor * G_backlit * T_backlit * P_backlit * lastShadowValue;// HPWaterBSDFLibary.hlsl: Line 440-441
cbsdf.diffR = macroScattering;
cbsdf.diffT = thinLayerSSS + backlitTransmission;
參數說明
薄層SSS參數
![]()
背光透射參數
![]()
體積散射參數
![]()
視覺效果分區
![]()
物理參考
Beer-Lambert定律:光在介質中的指數衰減
Henyey-Greenstein相位函數:描述散射的角度分布
菲涅爾方程:界面反射/透射比例
輻射傳輸方程(RTE):體積散射的理論基礎
文末,再次感謝hulalalala的分享, 作者主頁:https://www.zhihu.com/people/ou-yang-xuan-58-47, 如果您有任何獨到的見解或者發現也歡迎聯系我們,一起探討。(QQ群: 793972859 )。
近期精彩回顧
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.