理解高动态范围光

Table of Contents

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

现在大部分图形程序员(包括我)可能是从directxsdk处接触到这个概念并开始学习。DirectX SDK中的示例demo较为清晰地展示了一个完整的HDR技术所要采用的流程,如下:

  1. float render target 上去渲染当前的场景。
  2. 使用 RGBMLogLuv 等编码方式来节省所需的内存和带宽
  3. 通过 down sample 去计算场景亮度
  4. 根据亮度对场景做一个 矫正(tone mapping) 最后输出到一个 rgb8render target
  5. 上面的流程的四个步骤就展示了四个高大上的概念,我们先一一弄懂这些基础概念,再进一步分析。

1 float render target

首先是float render target。render target(以下简称RT)这个概念就不在此详细描述了,这个概念都还没弄懂的话需要先放下这篇文章,先夯实基础再说。RT可以理解为一系列的像素点的集合。在计算机中自然需要用一个一个字节去表示像素点的RGBA信息。最常见的是使用一个字节去表示像素点的一个颜色分量。这样子表示一个像素点则需要四个字节共32位。但一个字节表示一个颜色分量,比如Red分量的话,最多就只能表示256阶的信息。在很多时候,尤其是在处理我们的HDR信息的时候,256阶是远远不够用。所以我们要采用32位或者更高精度的浮点数去表示每一位颜色分量。float render target正是表示这一个概念。

1.1 RGBM

RGBM是一种颜色编码方式,如上所述,为了解决这个精度不足以存储亮度范围信息的问题,我们可以创建一个精度更高的buffer,以扩展之前的那个限定在[0,1]取值域的取值范围,从而使得我们可以在RT上渲染一个更高的亮度范围的画面效果。但使用高精度的buffer则带来另一个问题:即需要更高的内存存储空间和更高的带宽,并且有些渲染硬件无法以操作8位精度的buffer的速度去操作16位浮点数的buffer。因此为了解决这个问题,我们需要采用一种编码方法将这些颜色数据编码成一个能以8位颜色分量的存储的数据。编码方式有多种,例如RGBM编码,LogLuv编码,等等。假如有一个给定的包含了RGB颜色分量的颜色值C,将其编码成一个含有RGBM四个分量的颜色值的步骤是:

  1. 定义一个"最大范围值",假定为变量MaxRange
  2. 取得C的RGB分量中最大的那个值。将这个最大值赋值给变量maxRGB
  3. maxRGB除以MaxRange,得到商,将这个商赋值给变量M
  4. 用M乘以255得到结果值之后,取得大于这个结果值的最小整数,然后再将这个最小值除以255之后,再赋值给变量M
  5. 最后,用C的各个颜色分量,除以MMaxRange的乘积,作为最终结果颜色的RGB分量,M就作为最后一个分量。

这些步骤写成shader代码如下:

float MaxRange = 8; 
float4 EncodeRGBM(float3 rgb)
{
  float maxRGB = max(rgb.x,max(rgb.g,rgb.b));
  float M = maxRGB / MaxRange;
  M = ceil(M * 255.0) / 255.0;
  return float4(rgb / (M * MaxRange), M);
}

1.2 LogLuv

LogLuv编码格式是由Greg Ward在其论文The LogLuv Encoding for Full Gamut, High Dynamic Range Images中提出的。该编码格式最初是被设计来处理静态图像数据。后来经过Ninja Theory公司的前程序员Marco salvi的改进,此算法能使用在实时游戏中了。通过将一个32位的像素点的其中16位空间用来存储亮度信息,该编码格式可以存储一个非常大的范围的亮度数据值以适应于HDR渲染。LogLuv的编解码算法可以通过一段非常简单但高效的像素着色器函数去完成,这些函数由Christer Ericcson所提供,如下:

// 用来进行编码的参数矩阵M
const static float3x3 M = float3x3(
0.2209, 0.3390, 0.4184,
0.1138, 0.6780, 0.7319,
0.0102, 0.1130, 0.2969);

// M矩阵的逆矩阵,用来进行解码
const static float3x3 InverseM = float3x3( 
6.0013,-2.700,-1.7995, 
-1.332, 3.1029,-5.7720, 
3007,-1.088,5.6268);

float4 LogLuvEncode(in float3 vRGB) 
{
    float4 vResult; 
    float3 Xp_Y_XYZp = mul(vRGB, M); 
    Xp_Y_XYZp = max(Xp_Y_XYZp, float3(1e-6, 1e-6, 1e-6)); 
    vResult.xy = Xp_Y_XYZp.xy / Xp_Y_XYZp.z; 
    float Le = 2 * log2(Xp_Y_XYZp.y) + 127; 
    vResult.w = frac(Le); 
    vResult.z = (Le - (floor(vResult.w*255.0f))/255.0f)/255.0f; 
    return vResult; 
} 

float3 LogLuvDecode(in float4 vLogLuv) 
{
    float Le = vLogLuv.z * 255 + vLogLuv.w; 
    float3 Xp_Y_XYZp; 
    Xp_Y_XYZp.y = exp2((Le - 127) / 2); 
    Xp_Y_XYZp.z = Xp_Y_XYZp.y / vLogLuv.y; 
    Xp_Y_XYZp.x = vLogLuv.x * Xp_Y_XYZp.z; 
    float3 vRGB = mul(Xp_Y_XYZp, InverseM); 
    return max(vRGB, 0); 
}

2 down sample

down sample即是低向采样。可以大致地理解为,将原来一副宽高为WH的纹理,通过一定的采样算法,采用纹理中的一些关键的像素信息点,将其“缩小”到一个宽高为MN大小(M<W & N < H)的新缓冲区中去。可以近似类比使用看图软件缩小原始图的大小,所看到的缩小了的图就是对原图做了down sample的效果。

3 tone mapping

tone mapping 即是色调映射, 色调映射 是在有限动态范围的媒介上近似显示高动态范围图像的一项计算机图形学技术。打印结果、CRT 或者LCD显示器以及投影仪等都只有有限的动态范围。从本质上来讲,色调映射是要解决的问题是进行大幅度的对比度衰减将场景亮度变换到可以显示的范围,同时要保持图像细节与颜色等对于表现原始场景非常重要的信息。打个很直白的比方,假定我们对“白色”的定义,从高到低分为:“超级白”,“特别白”,“一般白”,“苍白”,“灰白”,因为计算机颜色缓冲区的精度问题,没法表示太多的白色,可能只能显示“特别白”,“一般白”,“苍白”这一范围内的白色。超过“特别白”的“超级白”只能被截断为“特别白”,低于“苍白”的“灰白”也只能被提升为“苍白”。根据应用的不同,色调映射的目标可以有不同的表述。在有些场合,生成“好看”的图像是主要目的,而在其它一些场合可能会强调生成尽可能多的细节或者最大的图像对比度。在实际的渲染应用中可能是要在真实场景与显示图像中达到匹配,尽管显示设备可能并不能够显示整个的亮度范围。

用一张很形象的图可以描述实现HDR效果的流程,如下:

kumakoko avatar
kumakoko
pure coder
comments powered by Disqus