HPWater水体散射模型
- 作者:admin
- /
- 时间:2小时前
- /
- 浏览:13 次
- /
- 分类:厚积薄发
【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!
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_exit
Step 1:入射计算
1.1 菲涅尔透射(Fresnel Transmission)
光在界面上一部分被反射,剩余部分透射进入水中。透射比例由菲涅尔方程决定。
数学公式:

水的f₀ ≈ 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次。
物理过程:
- 光进入水后沿视线传播
- 每走一小步,部分光被吸收、部分被散射
- 散射的光中,朝向摄像机的部分被累加
- 重复直到到达水底或视线结束
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;
// 非线性部分:厚层区域,d² 让光程在 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):体积散射的理论基础
这是侑虎科技第1971篇文章,感谢作者hulalalala供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/ou-yang-xuan-58-47
再次感谢hulalalala的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

