Unity Shader exp 函数的算法与渲染应用

从指数函数的数学本质出发,深入它在 URP 渲染管线中的六个实战场景

一、数学基础:exponential 是什么

exp(x) 是以自然常数 e ≈ 2.71828 为底的指数函数,即 ex。 它是微积分中唯一一个"导函数等于自身"的函数,这个性质直接决定了它在物理衰减、概率分布、信号处理中的核心地位。

关键数学性质

  • **恒正性:**exp(x) > 0 对所有实数 x 成立------在渲染中天然适合做权重和衰减因子。
  • **导函数不变:**d/dx eˣ = eˣ,意味着指数衰减的变化率正比于当前值。
  • **幂等性质:**exp(a)·exp(b) = exp(a+b),这使得多次指数衰减可复合为单次。
  • **逆关系:**exp 与 log(自然对数)互为反函数:exp(log(x)) = x。

**直觉理解:**exp(x) 描述的是"每前进一个单位,量值乘以固定因子"的过程。 在物理世界中,光的吸收、声音的衰减、放射性衰变都遵循指数规律------所以 exp 函数在渲染中有大量直接对应物理模型的用途。

二、GPU 上的 exp:指令级实现

在 HLSL/ShaderLab 中,exp(x) 看起来只是一个函数调用,但它在 GPU 硬件上对应的是专门的超越函数单元(Transcendental Function Unit)。 理解其底层实现有助于做出正确的精度与性能权衡。

exp 与 exp2 的区别

GPU 硬件原生支持的是 exp2(x)(以 2 为底),而非 exp(x)(以 e 为底)。 exp(x) 在编译时被转换为 exp2(x * log2(e)),即 exp2(x * 1.442695...)。 这意味着调用 exp()exp2() 多一次乘法。

Half Precision 下的精度问题

在移动端或使用 half 类型时,exp() 的数值稳定性需要特别关注:

  • **下溢:**exp(x) 在 x < -11.1 (half) 或 x < -88.7 (float) 时结果为零(下溢到 0)。
  • **上溢:**exp(x) 在 x > 11.1 (half) 或 x > 88.7 (float) 时溢出到无穷大。
  • **安全区间:**在 half precision 下,输入应保持在 -11, 11 之内。

实践建议: 对 half precision,优先使用 exp2() 而非 exp()。 因为它省去了一次 log2(e) 的乘法,且许多 GPU 的 exp2 指令比 exp 指令延迟更低。 在人眼感知不敏感的场合(如雾效、辉光衰减),half 精度完全够用。

函数 原生指令 额外开销 推荐场景
exp(x) exp2(x * log2e) 一次乘法 物理光照模型
exp2(x) 原生 exp2 指令 雾效、衰减、Bloom
rcp(exp(x)) 两次指令 可改用 exp(-x) 代替
pow(e,x) exp2(x * log2e) 比 exp(x) 多一步 不推荐替代 exp

三、应用案例一:指数雾(Exponential Fog)

这是 exp 在渲染中最经典的应用。光线在介质中传播时,被散射和吸收的比例随距离呈指数增长, 因此到达相机的光强按 exp(-density * distance) 衰减。

数学模型

Visibility = exp(-density · distance)

URP 内置了两类指数雾公式:

  • exp(-density * d) ------ 简单指数雾
  • exp2(-density * d²) ------ 指数平方雾(Exponential Squared),近距离更通透

URP 中的 Shader 实现

cs 复制代码
// URP 指数雾(简化自 ShaderLibrary/Fog.hlsl) half ComputeFogFactor(float coord) { float fogIntensity = exp(-_FogDensity * coord); return saturate(fogIntensity); } half4 MixFog(half4 color, float fogFactor) { return lerp(_FogColor, color, fogFactor); }

性能提示: 指数平方雾 exp2(-density * d * d) 本质上是用平方项 替代 d, 使近距离衰减更平缓、远距离衰减更剧烈。在移动端可用 half 精度直接计算,无需额外开销。

四、应用案例二:HDR 色调映射(Tone Mapping)

渲染管线输出的 HDR 颜色值范围可能远超 0, 1,需要通过色调映射将其压缩到 LDR 显示范围。 指数色调映射是 最简洁且视觉效果自然的方案之一。

指数色调映射公式

Lout = 1 − exp(−exposure · Lin)

与 Reinhard 和 ACES 相比,指数映射的特点是在高光区域有更柔和的肩部(shoulder)曲线。

Shader 代码

cs 复制代码
// 指数色调映射 ------ 无分支、性能友好 half3 ExponentialToneMap(half3 hdrColor, half exposure) { return 1.0 - exp(-exposure * hdrColor); } // 带白点(white point)调节的版本 half3 ExpToneMapWP(half3 color, half exposure, half whitePoint) { return (1.0 - exp(-exposure * color)) / (1.0 - exp(-exposure * whitePoint)); }

五、应用案例三:Bloom 与柔和衰减

Bloom 后处理的核心是多级降采样 + 高斯模糊。但标准高斯核在 shader 中需要多次采样。 指数衰减核 (类似拉普拉斯分布 exp(-|x|/σ))可以近似高斯模糊的视觉效果, 同时显著减少采样次数。

分离指数模糊(Separable Exponential Blur)

cs 复制代码
// 水平方向单次指数模糊(配合垂直 pass 实现分离卷积) half4 ExpBlurHorizontal(Varyings input) : SV_Target { const half sigma = _BlurSigma; half4 color = 0.0; half totalWeight = 0.0; for (int i = -_SampleCount; i <= _SampleCount; i++) { half offset = i * _BlitTexture_TexelSize.x * _KernelScale; half weight = exp(-abs(i) / sigma); color += SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, input.uv + float2(offset, 0)) * weight; totalWeight += weight; } return color / totalWeight; }

技巧: 如果 σ ≥ 1,甚至可以用 Kawase Blur------它是迭代式指数模糊, 每次迭代只采样 4 个偏移位置,在移动端性能极好。每一轮 Kawase pass 本质上是多次 exp 衰减的复合。

六、应用案例四:基于 exp 的菲涅尔效果

标准 Schlick 菲涅尔使用 pow(1 - NdotV, 5),而 指数菲涅尔 使用 exp(-k · NdotV)。后者在掠射角(grazing angle)附近过渡更自然, 且在某些移动 GPU 上性能更好(exp 指令比 pow 指令延迟低)。

两版代码对比

七、应用案例五:SDF 平滑最小运算

在基于有符号距离场(SDF)的渲染中------比如 UI 圆角、粒子软边缘、VFX Graph------需要将多个形状的 SDF 平滑地融合在一起。基础的 min(a, b) 产生硬边,而基于 exp 的平滑最小运算能产生自然的有机过渡。

核心公式

smin(a, b, k) = −(1/k) · log(exp(−k·a) + exp(−k·b))

其中 k 控制融合强度:k 越大边缘越锐利,k 越小过渡越柔和。 这个公式本质上是 LogSumExp 技巧在 SDF 领域的应用。

HLSL 实现

cs 复制代码
// 基于 exp 的 SDF 平滑最小运算 float SmoothMinExp(float a, float b, float k) { return -log(exp(-k * a) + exp(-k * b)) / k; } // 优化版:避免数值溢出 float SmoothMinExpSafe(float a, float b, float k) { float h = max(k - abs(a - b), 0.0) / k; return min(a, b) - h * h * k * (1.0 / 4.0); }

**注意:**上述 Safe 版本是原 exp-log 公式的二次近似,虽然省去了 exp 和 log 的开销, 但在极端 k 值下会偏离精确结果。对于 UI 粒子效果等对精度要求不高的场景推荐使用; 对关键视觉效果建议保留精确的 exp-log 版本。

八、应用案例六:自定义光照衰减

URP 默认的光照衰减由 DistanceAttenuation 纹理查找驱动,但在需要 物理准确的点光源 / 聚光灯衰减时,exp 能提供更逼真的结果。 物理上,光强遵循平方反比定律,配合指数项可以模拟大气散射造成的额外衰减。

物理衰减模型

Attenuation = exp(-scatter · distance) / (distance² + 1)

cs 复制代码
// URP 中替代默认衰减的物理衰减函数 half PhysicalAttenuation(float3 lightPos, float3 worldPos, half range, half scattering) { float dist = distance(lightPos, worldPos); // 范围裁剪(smooth 过渡) half rangeAtten = saturate(1.0 - exp(-5.0 * (1.0 - dist / range))); // 平方反比 + 指数散射衰减 half invSqr = 1.0 / (dot(dist, dist) + 1.0); half scatterAtten = exp(-scattering * dist); return rangeAtten * invSqr * scatterAtten; }

与默认衰减的对比

要点: exp 衰减在近距离比默认纹理查找更"亮",远距离更"暗"------这更符合物理直觉。 如果项目追求写实风格,建议用此方案替换默认衰减。注意 exp(-scattering * dist) 中的 scattering 参数需要美术调节,典型值在 0.1 ~ 2.0 之间。

九、性能考量与最佳实践

指令延迟参考

操作 移动 GPU 延迟(周期) 桌面 GPU 延迟(周期) 备注
exp2() ~8-16 ~4-8 原生指令,最快
exp() ~10-20 ~4-8 多一次 mul
log() ~8-16 ~4-8 与 exp2 同级
pow(x,5) ~20-40 ~8-16 通用 pow 开销大
纹理采样 ~100-400 ~80-200 带宽瓶颈

实践清单

  1. 能用 exp2 就别用 exp。 大多数渲染场景(雾效、衰减、Bloom)只需要衰减形状,不要求精确的自然指数底。 将公式重写为 exp2(-k*d) 只需调节 k 的数值即可。
  2. half 精度下做输入钳位。 在调用 exp 前用 clamp(x, -10, 10) 包裹输入,防止下溢 / 上溢产生 NaN 或无穷大, 后者在移动 GPU 上可能引发渲染异常。
  3. 用查表替代高频 exp 调用。 如果一个 shader 中 exp 调用超过 3 次且输入范围可预测, 考虑预计算一张 64 或 128 像素的 1D 纹理做查找------纹理采样虽然延迟高,但在大量重复调用时总吞吐更好。
  4. 注意 exp 的乘法结合律。 exp(a) * exp(b) = exp(a+b) 不仅数学上成立,在 shader 中也是重要的优化手段------合并多次 exp 为一次可以显著减少指令数。
  5. Kawase / 迭代 exp 优于大核 exp 模糊。 对 Bloom 等全屏后处理,4 轮 Kawase pass(每轮 4 次采样)比单轮 13-tap 指数模糊效果更好且带宽更低。

快速参考:常见公式的 exp2 改写

exp 版本 等价的 exp2 版本 参数转换
exp(-density * d) exp2(-k * d) k = density / ln(2) ≈ density * 1.4427
exp(-x² / 2σ²) exp2(-k * x²) k = 1 / (2σ² * ln(2))
1 - exp(-exposure * L) 1 - exp2(-k * L) k = exposure / ln(2)
exp(-k · NdotV) exp2(-k' · NdotV) k' = k / ln(2)

十、总结

exp 是 shader 中最简洁而强大的数学函数之一。它把自然界的衰减规律直接映射到 GPU 指令, 让你用一行代码就能描述从雾效到菲涅尔的连续视觉现象。

应用场景 核心公式 GPU 友
指数雾 exp(-density · d) ★★★★★
HDR 色调映射 1 − exp(−exposure · L) ★★★★★
Bloom 指数模糊 exp(−|x| / σ) ★★★★☆
菲涅尔效果 exp(−k · NdotV) ★★★★★
SDF 平滑融合 −log(Σ exp(−k·dᵢ)) / k ★★★☆☆
物理光照衰减 exp(−scatter · d) / (d² + 1) ★★★★☆

一言以蔽之:**在 URP shader 中,凡是需要"逐渐变弱""平滑过渡""物理衰减"的地方, exp 都应该是你的首选。**理解它的数学本质和 GPU 实现细节,能让你在写 shader 时多一份从容、少一次试错。

相关推荐
“码”力全开1 小时前
AI视频分析误报优化完整流程
算法·架构·边缘计算
深盾科技_Virbox1 小时前
深盾科技·Virbox产品体系全景解读:软件安全如何从加密锁走向全生命周期
java·大数据·算法·安全·软件需求
可编程芯片开发2 小时前
基于VSG虚拟同步发电机控制的三相并网逆变器带多组可变负载Simulink建模与仿真
算法
AI服务老曹2 小时前
国产NPU视觉算法参数配置说明
算法·性能优化·边缘计算
彦为君2 小时前
Redis最新版本特性
java·数据库·redis·算法·bootstrap
触底反弹3 小时前
🔥 字符串算法面试三连击:反转、回文、回文变种,搞懂这三题稳了!
前端·javascript·算法
aaaameliaaa3 小时前
计算斐波那契数(递归、迭代)(1,1,2,3,5.....)
c语言·开发语言·笔记·算法·排序算法
Jerry3 小时前
LeetCode 977. 有序数组的平方
算法
Turbo正则3 小时前
群论学习入门 | 群论与李群的基本概念
人工智能·学习·算法·抽象代数