什么是色调映射/以及tanh的使用(glsl)

551 阅读4分钟

写在开头

最近还是在学习glsl,发现我的基础确实太差了,有很多数学知识都不清楚,这篇博客是我在学习tanh(双曲正切函数)时机缘巧合也了解到了tanh在色调映射相关的运用,自我吸收总结的一篇关于tanh色调映射的文章,希望各位斧正!

01.png

什么是色调映射(Tonemapping)

色调映射(Tonemapping) 是一种图形渲染技术,用于将 高动态范围(HDR)的颜色和亮度值映射到低动态范围(LDR) 的显示设备上(如屏幕,通常限制在 0 到 1 的亮度范围),以在保留视觉细节和颜色的同时,呈现更自然、逼真的画面。

核心概念

  • 高动态范围(HDR):在渲染中,HDR 允许亮度值超过 1.0,模拟现实世界中更广泛的亮度范围(如阳光的强烈亮度到阴影的低亮度)。

  • 问题:显示器通常只能显示有限的亮度范围(0 到 1)。如果直接将 HDR 值“钳制”(限制)到这个范围,高亮区域会丢失细节,颜色可能失真,出现过曝(washed out)或颜色条带(color banding)。

  • 色调映射的作用:通过数学函数将 HDR 值平滑地压缩到 LDR 范围,保留颜色比例和细节,避免突兀的亮度截断。

没有色调映射会出现的问题是,当一个色值的亮度超过了255或者1.0时,由于显示器存在亮度范围,所以此时颜色就会失真出现过曝

正式开始!

首先看一段glsl代码

void main() {

    vec2 p = (2.0 * gl_FragCoord.xy - u_resolution.xy ) / u_resolution.y;
   
    float l = length(p);

    vec3 sun_color = vec3(1.0, 0.5, 0.2);

    float brightness = 1.0 / length(l);

    gl_FragColor = vec4(sun_color * brightness, 1.0);

}

02.png

学习一个马斯克转发的Grok效果(glsl)中我们学到了一个数据基础知识

数学规律:当分母越接近0,结果越接近无穷大! 这就是为什么圆环会发光 - 在半径0.5的位置,分母最小,亮度最大!

ring值brightness = 0.1 / ring效果
0.50.1 / 0.5 = 0.2暗灰色
0.20.1 / 0.2 = 0.5中等亮度
0.10.1 / 0.1 = 1.0比较亮
0.050.1 / 0.05 = 2.0很亮!
0.010.1 / 0.01 = 10.0超亮!!
0.0010.1 / 0.001 = 100.0极亮!!!

所以我们能从上面的图上看到圆心中间非常非常亮,并且圆环之间的过渡并不平滑,这就是因为我们的glsl代码的颜色是0.0 ~ 1.0,超过1.0的颜色会被自动钳制(clamped)到 1.0!上述的例子是模仿太阳光,可能最开始觉得效果还不错,再来看两个例子!

vec3 sun_color = vec3(1.0, 0.5, 0.2);

03.png

vec3 sun_color = vec3(0.1,0.4,0.2);

04.png

可以看到圆心中间都已经产生了过曝现象!此时就需要我们用色调映射技术把超过显示范围的颜色合理过渡到可以显示的范围中!

色调映射🎨

由于过曝/失真的现象是从HDR开始以来就伴随的现象,所以有许多能人义士开发了许多方法来让HDR自然的过渡到LDR,下面展示几个方法

ACES

// Narkowicz 2015, "ACES Filmic Tone Mapping Curve"
vec3 Tonemap_ACES(vec3 x)
{
    const float a = 2.51;
    const float b = 0.03;
    const float c = 2.43;
    const float d = 0.59;
    const float e = 0.14;
    return (x * (a * x + b)) / (x * (c * x + d) + e);
}

Hable

// Hable 2010, "Filmic Tonemapping Operators"
vec3 Tonemap_Uncharted2(vec3 x)
{
    x *= 16.0;
    const float A = 0.15;
    const float B = 0.50;
    const float C = 0.10;
    const float D = 0.20;
    const float E = 0.02;
    const float F = 0.30;
    
    return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}

Unreal

vec3 Tonemap_Unreal(vec3 x)
{
    // Unreal 3, Documentation: "Color Grading"
    // Adapted to be close to Tonemap_ACES, with similar range
    // Gamma 2.2 correction is baked in, don't use with sRGB conversion!
    return x / (x + 0.155) * 1.019;
}

tanh

vec3 Tonemap_tanh(vec3 x)
{
    x = clamp(x, -40.0, 40.0);
    vec3 exp_neg_2x = exp(-2.0 * x);
    return -1.0 + 2.0 / (1.0 + exp_neg_2x)
}

tanh🔑

上面介绍了很多种色调映射的办法,我想着重介绍一下tanh (双曲正切函数),我最开始就是想学习tanh结果误打误撞了解了很多关于色调映射的知识,色调映射的核心就是让数值的曲线变得不能超过限值并且有比较平滑的过渡,我们看看tanh的数学曲线!

图片来自ml-science

从图上可以看出来tanh输出的范围是[-1.0,1.0]之间!正好也可以用来做我们的色调映射方法

双曲正切函数的数学定义为:

tanh(x)=sinh(x)cosh(x)=exexex+ex\tanh(x) = \frac{\sinh(x)}{\cosh(x)} = \frac{e^x - e^{-x}}{e^x + e^{-x}}

  • 其中,sinh(x) 是双曲正弦,cosh(x) 是双曲余弦。
  • 范围:输出值始终在 [-1.0, 1.0] 之间。
  • 曲线形状:tanh(x) 是一个 S 形(sigmoid-like)曲线,类似于逻辑函数(sigmoid),但基于双曲函数。
  • 当 x 接近正无穷大时,tanh(x) 趋向 1.0。
  • 当 x 接近负无穷大时,tanh(x) 趋向 -1.0。
  • 当 x = 0 时,tanh(0) = 0。
  • 平滑性:tanh 是连续且平滑的,适合需要渐进过渡的场景。

参考资料

Mini: Tonemaps

Functions: Tanh

双曲函数