HPWater水体散射模型
【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息! 实时水体渲染BSDF模型,支持体积散射、薄层SSS和背光透射。 本模型的结构设计参考了经典的GGX微表面BRDF,两者都将光照分解为几个物理项的乘积: GGX镜面公式: 水体散射公式: 两者的共同点: 主要区别: 相比GGX BRDF(只需要roughness和fresnel0),水体BSDF需要额外的体积散射参数。 材质参数(BSDFData): 体积散射参数(WaterLightLoopData): 派生量: 水体的光照与普通材质不同。光进入水中后会被吸收和散射,部分光在水体内部多次弹射后从表面出射回到摄像机。这个过程涉及:入射折射、体积散射、出射透射三个阶段。 本模型将水体散射分解为两个输出通道(遵循HDRP的CBSDF结构)。 关于diffR/diffT的命名: 对于水体: diffT(薄层散射):光从背面/侧面穿过薄层出射。光路穿过表面,属于“透射方向”。包含: 三个组件通过入射几何项自然分离,避免能量重复计算。 Step 1:入射计算 Step 2:体积散射(diffR) Step 3:薄层SSS(diffT的一部分) Step 4:背光透射(diffT的一部分) Step 5:出射透射 最终输出: 1.1 菲涅尔透射(Fresnel Transmission) 数学公式: 水的f₀ ≈ 0.02,即垂直入射时只有2%的光被反射。 代码实现: 1.2 入射几何项(Incident Geometry Terms) 数学公式: 代码实现: 分工表: 解释: 深水区域使用Ray Marching沿视线采样,累加每一步的散射光量。循环次数默认6次。 物理过程: 2.1 Beer-Lambert透射 公式: 代码实现: 2.2 散射光计算 公式: L:入射光 代码实现: 2.3 Henyey-Greenstein相位函数 数学公式: 代码实现: 2.4 混合相位函数(瑞利 + 米氏) 数学公式: 代码实现: 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。步长为导数: 代码实现优化: 解释: 水下场景散射公式: 这是一个简化近似(避免对场景光再做完整Ray Marching): 物理效果:让远处的水下物体看起来更模糊、更偏向水的散射色(雾化)。 薄层(如波峰、浪花)厚度无法直接得知,只能用高度或法线近似。关键是正确估算光程,并处理与深水散射的过渡。 输入参数thickness: 通常由高度场、法线或其他几何信息近似得到。代码中乘以HPWATER_SSS_PATH_SCALE(默认20米)转换为等效光程。 薄层与深水的几何关系: 薄层的底部与深水区域连接。当thickness接近1时,薄层实际上“看到”的是深水区域累积的散射光。因此: 为什么相机与光源角度相近时,水面背面会稍亮? 典型场景:夕阳下,人以较低视角看水面。此时相机方向V和光源方向L角度相近(都是低角度),水面波峰的背面会呈现微微透亮的效果。 这是相位函数、入射几何项和出射菲涅尔透射三者共同作用的结果: 1. 相位函数P(cosθ)的前向散射 2. 入射几何项G_sss在背面有值 3. 出射菲涅尔透射fresnelTransmissionExit 三者的平衡: 这就是为什么波峰薄处在这种视角下会呈现柔和的透光效果。 3.1 非线性光程修正(Nonlinear Path Length) 物理直觉: 数学公式: 代码实现: 3.2 薄层散射计算与深水混合 数学公式: 代码实现: 解释: 当对准光源观察时,光从背后穿透薄水层产生辉光。这是对准太阳时水面发亮的原因。 与薄层SSS的区别: 数学公式: 其中: 代码实现: 解释: 散射光从水内出射时,再次经过菲涅尔透射。 数学公式: 代码实现: 数学公式: 代码实现: 薄层SSS参数 背光透射参数 体积散射参数 这是侑虎科技第1971篇文章,感谢作者hulalalala供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859) 作者主页:https://www.zhihu.com/people/ou-yang-xuan-58-47 再次感谢hulalalala的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)HPWater BSDF - 稍微基于物理的水体散射模型

本模型的散射模拟

直射光5°恒定厚度
直射光15°恒定厚度
直射光30°恒定厚度
第一张(厚度近似)后三张(不同直射光角度)
海浪厚度近似(法线)这个近似并不准确,但将就着用
可以看到不同厚度下,薄层SSS和透射经过不同厚度介质时,前向散射的分散性导致的散射形状
不同光向下的厚度近似模拟与GGX BRDF的类比



输入参数


整体概述
在HDRP的CBSDF中,R = Reflection(反射方向),T = Transmission(透射方向):// ============================================================================
// 水体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 末尾统一应用
//
// ============================================================================渲染流程总结
计算三个入射几何项(G_entry、G_sss、G_backlit)和入射菲涅尔透射(T_entry),决定多少光进入水体、进入哪个散射路径。
对深水区域进行少量Ray Marching,累加多次散射的光量,输出S_volume。
对薄层区域计算散射光量S_sss,使用非线性光程修正处理不同厚度,并与深水散射混合。
计算光从背面穿透薄层的透射光,使用极强前向相位函数。
所有散射光经过出射菲涅尔透射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:入射计算
光在界面上一部分被反射,剩余部分透射进入水中。透射比例由菲涅尔方程决定。
// HPWaterBSDFLibary.hlsl: Line 200
float3 T_entry = 1.0 - F_Schlick(bsdfData.fresnel0, clampedNdotL);
三个组件各自有独立的入射几何项,根据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
Step 2:体积散射(Volume Scattering)
光在介质中传播时按指数衰减。
// HPWaterVolumetrics.hlsl: Line 202-205
float3 extinctionCoeff = absorptionCoeff + scatteringCoeff;
transmittance = exp(-extinctionCoeff * crossDistance);
被消光的光中,一部分被吸收(变成热量),另一部分被散射(改变方向)。
(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;
}
相位函数描述光散射后的方向分布。水体主要是前向散射。
// 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;
}
真实水体同时存在瑞利散射(分子级,波长相关,产生蓝色)和米氏散射(颗粒级,前向散射)。
// 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;
}
辐射传输方程的数值积分:
理论上,沿视线累加的散射光是一个积分:

近处对视觉贡献大(细节可见),远处贡献小(被衰减)。用指数步进可以在采样数相同时获得更好的精度。

令currentExp=e^(k·t)=EXP_FACTOR^t,则:// 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;
Step 3:薄层SSS(Thin Layer Subsurface Scattering)
thickness是归一化的厚度值,范围 [0, 1]:波峰(薄层顶部)thickness ≈ 0
/\
/ \
/ \ ← 薄层区域
/ \
/────────\ ← thickness = 1(薄层底部 = 深水顶部)
│ │
│ 深水区 │ ← S_volume(ray marching 累积)
│__________│夕阳低角度 + 人高视角看水面:
入射:G_sss ≈ 0.6~1(背面/侧面入射)
↓
散射:P_sss 中等偏高(V、L 角度相近但非正对)
↓
出射:T_exit ≈ 0.7~0.9(低视角有所衰减)
三者相乘 → 背面稍亮(自然的透光效果)
薄层SSS用单次计算近似深水散射的延续。光程需要随thickness增加而增长,以匹配深水Ray Marching的累积效果。
// 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);
薄层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);Step 4:背光透射(Backlit Transmission)

// 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;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;参数说明



视觉效果分区

物理参考