如何转换为 HDR 渲染器?

How to convert to a HDR renderer?

我正在将我的 webgl 延迟渲染器转换为使用高动态范围的渲染器。我从网上的各种来源阅读了很多关于这个主题的文章,我有几个问题希望得到澄清。我阅读的大部分内容都涉及 HDR 图像渲染,但我的问题与渲染器可能必须如何更改才能支持 HDR 有关。

据我了解,HDR 本质上是在尝试捕捉更高的光照范围,以便我们可以在极亮或极暗的场景中看到细节。通常在游戏中,我们使用强度 1 表示白光,0 表示黑色。但在 HDR / 现实世界中,范围要多样化得多。 IE。引擎中的太阳可能比 10 的灯泡亮 10000 倍。

为了应对这些更大的范围,您必须将渲染器转换为使用浮点渲染目标(或者理想情况下是半浮点数,因为它们使用更少的内存)用于其光传递。

我的第一个问题是关于照明的。除了浮点渲染目标之外,这是否仅仅意味着如果之前我有一个代表太阳的光,其强度为 1,它 could/should 现在被表示为 10000?即

float spec = calcSpec();
vec4 diff = texture2D( sampler, uv );
vec4 color = diff * max(0.0, dot( N, L )) * lightIntensity + spec; //Where lightIntensity  is now 10000?
return color;

照明系统是否有任何其他根本性变化(浮动纹理和更高范围除外)?

在此基础上,我们现在有了一个浮动渲染目标,它累加了所有的光照值(在所描述的较高范围内)。在这一点上,我可能会对渲染目标进行一些 post 处理,比如 bloom。完成后,现在需要对其进行色调映射,然后才能将其发送到屏幕。这是因为光线范围必须转换回我们显示器的范围。

因此对于色调映射阶段,我大概会使用 post 过程,然后使用色调映射公式将 HDR 照明转换为低动态范围。我选择的技术是来自神秘海域 2 的 John Hables:

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;
const float W = 11.2;

vec3 Uncharted2Tonemap(vec3 x)
{
    return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}

... // in main pixel shader

vec4 texColor = texture2D(lightSample, texCoord );
texColor *= 16;  // Hardcoded Exposure Adjustment 
float ExposureBias = 2.0;
vec3 curr = Uncharted2Tonemap( ExposureBias * texColor.xyz );
vec3 whiteScale = 1.0 / Uncharted2Tonemap(W);
vec3 color = curr * whiteScale;
 // Gama correction
color.x = pow( color.x, 1.0 /2.2 );
color.y = pow( color.y, 1.0 /2.2 );
color.z = pow( color.z, 1.0 /2.2 );
return vec4( color, 1.0 );

Tone mapping article

我的第二个问题与色调映射阶段有关。除了这种技术之外,还有更多的东西吗?是简单地使用更高的光强度并调整曝光所有这些都需要被视为 HDR - 或者还有更多?我知道有些游戏具有自动曝光功能来计算平均发光度,但在最基本的层面上是否需要这样做?大概你可以只使用手动调整曝光?

许多文档中讨论的其他内容是 gama 校正。伽马校正似乎在两个方面进行。首先在读取纹理时,然后在将它们发送到屏幕时再次读取。读取纹理时,必须将它们简单地更改为如下所示:

vec4 diff = pow( texture2D( sampler, uv), 2.2 );

然后在上述色调映射技术中,输出校正通过以下方式完成:

pow(color,1/2.2);

在 John Hables 的演讲中,他说并非所有纹理都必须像这样校正。漫反射纹理必须是,但法线贴图之类的东西不一定必须。

我的第三个问题是关于这个伽玛校正。为了让它工作,这是必要的吗?这是否意味着我必须在所有读取漫反射贴图的地方更换我的引擎?

这就是我目前对该转换涉及的内容的理解。是否正确,是否有任何我误解或错误的地方?

光计算/累积 是的,你通常能够保持你的闪电计算相同,并且增加超过 1.0 的定向光强度肯定没问题。值可以超过一个的另一种方法是简单地将几个灯的贡献加在一起。

色调映射

你当然明白这个概念。有很多不同的方法可以进行实际映射,从更简单/天真的方法 color = clamp(hdrColor * exposure) 到更复杂(更好)的方法 posted。

自适应色调映射很快就会变得更加复杂。同样,天真的方法是通过使用最亮的像素来简单地归一化颜色,这肯定会使 hard/impossible 感知图像较暗部分的细节。您还可以平均亮度和钳位。或者您可以保存最后几帧的整个直方图并在您的映射中使用它们。

另一种方法是仅使用相邻像素的值对每个像素进行归一化,即"local tone mapping"。这个在实时渲染中一般不会做。

虽然听起来可能很复杂,但您 post 编辑的公式会产生非常好的结果,因此可以使用它。一旦你有了一个有效的实现,就可以在这里随意试验。还有很棒的论文:)

伽玛 现在 gamma-correction 很重要,即使你不使用 hdr 渲染。不过别担心,这并不难。

最重要的是始终注意您正在使用什么颜色space。就像没有单位的数字一样,没有颜色 space 的颜色也没什么意义。现在我们喜欢在着色器中使用线性 (rgb) 颜色 space,这意味着具有两倍 rgb 值的颜色应该是两倍的亮度。然而,这不是监视器的工作方式。

相机和照片编辑软件通常只是简单地向我们隐藏所有这些,并简单地以显示器喜欢的格式(称为 sRGB)保存图片。

sRGB 还有一个优势,那就是压缩。我们通常以每通道每像素 8/16/32 位保存图像。如果您以线性 space 保存图片并且图像中有小但非常亮的点,则您的 8/16/32 位可能不够精确,无法保存图像较暗部分的亮度差异,如果您正在显示他们再次(当然伽玛校正)细节可能会在黑暗中丢失。

您可以更改颜色 space 您的图像保存在许多相机和程序中,即使它有时有点隐藏。因此,如果您告诉您的艺术家以线性 (rgb) 颜色 space 保存所有图像,您根本不需要对图像进行伽玛校正。由于大多数程序(如 sRGB 和 sRGB)提供更好的压缩,通常最好将描述 color 的图像保存在 sRGB 中,因此需要对这些图像进行伽马校正。描述 values/data 的图像,如法线贴图或凹凸贴图,通常以线性颜色 space 保存(如果您的法线 [1.0, 0.5, 0.0] 只是没有45 度角每个人都会感到困惑;非颜色的压缩优势也毫无意义。

如果您想使用 sRGB 纹理,只需告诉 OpenGL,它会为您将其转换为线性颜色 space,而不会影响性能。

void glTexImage2D(  GLenum target,
  GLint level,
  GLint internalFormat,  // Use **GL_SRGB** here
  GLsizei width,
  GLsizei height,
  GLint border,
  GLenum format,
  GLenum type,
  const GLvoid * data);

哦,当然,您必须对发送到显示器的所有内容进行伽马校正(因此从线性更改为 sRGB 或伽马 2.2)。您可以在色调映射或其他 post 处理步骤中执行此操作。或者让 OpenGL 为您做;参见 glEnable(GL_FRAMEBUFFER_SRGB)