伪镜头光晕

哈Ha! 我向您介绍约翰·查普曼(John Chapman)撰写的文章“ Pseudo Lens Flare”的翻译。

图片

镜头光晕 (lens flare)是由光在镜头系统中的散射和折射引起的摄影伪影。 尽管这是伪影,但在计算机图形学中使用镜头眩光有很多原因:

  • 它增加了图像的感知亮度和可见动态范围。
  • 镜头光斑经常出现在照片中,因此可能会令人惊讶
  • 它可以在风格或戏剧中扮演重要角色,也可以在游戏中扮演游戏的一部分(想象眩光使玩家蒙蔽)

传统上,使用基于子画面的技术实时实现镜头光晕 。 尽管精灵会提供易于控制且非常逼真的结果,但必须将其明确放置并需要遮挡数据才能正确显示。 在这里,我将描述一个简单且相对便宜的屏幕空间效果,该效果从输入颜色缓冲区中创建伪镜头光晕 。 它不是基于物理学的,因此其结果与逼真的效果略有不同,但是可以与传统的基于精灵的效果结合使用(或替代)。

演算法


包括四个阶段:

  1. 下采样/阈值。
  2. 产生镜头眩光元素。
  3. 模糊
  4. 高档/与原始图像融合。

1.下采样/阈值


下采样 -优化以减少后续步骤的成本。 另外,我们要选择原始图像中最亮像素的子集。 使用规模/偏差 (规模/偏差)可提供一种灵活的方法来实现此目标:

uniform sampler2D uInputTex; uniform vec4 uScale; uniform vec4 uBias; noperspective in vec2 vTexcoord; out vec4 fResult; void main() { fResult = max(vec4(0.0), texture(uInputTex, vTexcoord) + uBias) * uScale; } 

图片

缩放/偏差调整是调整效果的主要方法; 最佳设置将取决于颜色缓冲区的动态范围,以及要查看结果的厚度。 由于该技术是一种近似的事实,因此细微之处看起来更好。

2.产生镜头眩光元素


镜头光晕元素倾向于围绕图像中心旋转。 通过模拟这种效果,我们可以水平/垂直扩展前一阶段的结果。 通过扩展纹理坐标,在元素生成阶段很容易做到这一点:

 vec2 texcoord = -vTexcoords + vec2(1.0); 

这是没有必要的。 有无元素生成都可以正常工作。 但是,更改纹理坐标的结果有助于在视觉上将镜头眩光效果与原始图像区分开。

鬼魂


“重影”(重影)是重复的高光,反映了色彩缓冲区中的明亮区域,相对于图像中心展开。 我选择生成的方法是从当前像素到屏幕中心获取一个矢量,然后沿着该矢量进行多个选择。

图片

 uniform sampler2D uInputTex; uniform int uGhosts; // number of ghost samples uniform float uGhostDispersal; // dispersion factor noperspective in vec2 vTexcoord; out vec4 fResult; void main() { vec2 texcoord = -vTexcoord + vec2(1.0); vec2 texelSize = 1.0 / vec2(textureSize(uInputTex, 0)); // ghost vector to image centre: vec2 ghostVec = (vec2(0.5) - texcoord) * uGhostDispersal; // sample ghosts: vec4 result = vec4(0.0); for (int i = 0; i < uGhosts; ++i) { vec2 offset = fract(texcoord + ghostVec * float(i)); result += texture(uInputTex, offset); } fResult = result; } 

注意,我使用fract()确保纹理坐标环绕; 等效地,您可以将环绕模式GL_REPEAT用于纹理。

结果如下:

图片

您可以通过仅允许靠近图像中心的明亮区域生成重影来改善结果。 我们可以通过增加权重来实现这一点,权重将从样本的中心开始递减:

 vec4 result = vec4(0.0); for (int i = 0; i < uGhosts; ++i) { vec2 offset = fract(texcoord + ghostVec * float(i)); float weight = length(vec2(0.5) - offset) / length(vec2(0.5)); weight = pow(1.0 - weight, 10.0); result += texture(uInputTex, offset) * weight; } 

权重函数尽可能简单-线性。 我们在循环内计算权重的原因是,输入图像中心的明亮区域可以将重影“投射”到边界,但是边界处的明亮区域无法将重影投射到中心。

图片

最终的改进是根据1D纹理,重影的径向颜色变化:

图片

在循环之后应用它,以影响幻影的最终颜色:

 result *= texture(uLensColor, length(vec2(0.5) - texcoord) / length(vec2(0.5))); 

光环(光环)


如果像在重影计算中那样将矢量移到图像的中心,但是固定矢量的长度,则会得到不同的效果:原始图像会发生径向变形:

图片
我们可以使用它来通过将权重乘以样本来创建“光晕”,从而限制变形图像对半径受uHaloWidth控制的环的贡献

 // sample halo: vec2 haloVec = normalize(ghostVec) * uHaloWidth; float weight = length(vec2(0.5) - fract(texcoord + haloVec)) / length(vec2(0.5)); weight = pow(1.0 - weight, 5.0); result += texture(uInputTex, texcoord + haloVec) * weight; 

图片

色度失真(色彩失真)


某些镜头光斑会因不同波长的光折射变化而导致色彩失真。 我们可以通过创建一个函数来模拟此情况,该函数分别选择沿样本矢量偏移量稍有不同的红色,绿色和蓝色通道:

 vec3 textureDistorted( in sampler2D tex, in vec2 texcoord, in vec2 direction, // direction of distortion in vec3 distortion // per-channel distortion factor ) { return vec3( texture(tex, texcoord + direction * distortion.r).r, texture(tex, texcoord + direction * distortion.g).g, texture(tex, texcoord + direction * distortion.b).b ); } 

它可以用作上一个清单中调用texture()的直接替换。 我计算方向变形如下:

 vec2 texelSize = 1.0 / vec2(textureSize(uInputTex, 0)); vec3 distortion = vec3(-texelSize.x * uDistortion, 0.0, texelSize.x * uDistortion); vec3 direction = normalize(ghostVec); 

尽管获取功能很简单,但它会从纹理中提取x3个样本,尽管它们都应该是缓存友好的,除非您将uDistortion设置为某个巨大的值。

随着元素的产生,一切。 结果如下:

图片

3.模糊


在没有模糊的情况下, 镜头光晕元素(尤其是重影)倾向于保留图像的外观。 通过为镜头光晕元素添加模糊效果,我们可以减弱高频,从而降低与输入图像的对比度,这有助于我们出售效果。

图片

我不会告诉你如何模糊。 您可以在各种Internet资源上阅读它(高斯模糊)。

4.高档/与原始图像融合


因此,我们有模糊的镜头光晕元素。 我们如何将它们与原始源图像结合在一起? 关于整个渲染管道,有几个重要的注意事项:

  • 在与镜头光晕组合之前必须先应用任何后续的运动模糊景深 ,以使镜头光晕元素不会参与这些效果。
  • 在进行任何色调映射之前,应先应用镜头光晕 。 这具有物理意义,因为色调映射模拟了胶片/ CMOS对入射光的响应,而镜头眩光是必不可少的一部分。

考虑到这一点,我们现阶段可以做一些事情来改善结果:

镜头污垢


首先,您需要以肮脏的纹理以全分辨率修改镜头光晕元素(在《战地风云3》中广泛使用):

图片

 uniform sampler2D uInputTex; // source image uniform sampler2D uLensFlareTex; // input from the blur stage uniform sampler2D uLensDirtTex; // full resolution dirt texture noperspective in vec2 vTexcoord; out vec4 fResult; void main() { vec4 lensMod = texture(uLensDirtTex, vTexcoord); vec4 lensFlare = texture(uLensFlareTex, vTexcoord) * lensMod; fResult = texture(uInputTex, vTexcoord) + lensflare; } 

关键在于镜片上非常脏的质地。 如果对比度低,则镜头光晕形状倾向于主导结果。 随着对比度的增加, 镜头光晕元素被消隐,这赋予了不同的美学外观,并且还隐藏了一些缺陷。

衍射星爆


作为一项额外的改进,我们可以通过将starburst纹理添加到镜头污垢中来使用它:

图片
作为纹理, 爆炸形看起来不太好。 不过,我们可以将转换矩阵传递给着色器,这将使我们能够在每帧中旋转/变形爆炸,并获得所需的动态效果:

 uniform sampler2D uInputTex; // source image uniform sampler2D uLensFlareTex; // input from the blur stage uniform sampler2D uLensDirtTex; // full resolution dirt texture uniform sampler2D uLensStarTex; // diffraction starburst texture uniform mat3 uLensStarMatrix; // transforms texcoords noperspective in vec2 vTexcoord; out vec4 fResult; void main() { vec4 lensMod = texture(uLensDirtTex, vTexcoord); vec2 lensStarTexcoord = (uLensStarMatrix * vec3(vTexcoord, 1.0)).xy; lensMod += texture(uLensStarTex, lensStarTexcoord); vec4 lensFlare = texture(uLensFlareTex, vTexcoord) * lensMod; fResult = texture(uInputTex, vTexcoord) + lensflare; } 

uLensStarMatrix转换矩阵基于从摄影机方向获得的值,如下所示:

 vec3 camx = cam.getViewMatrix().col(0); // camera x (left) vector vec3 camz = cam.getViewMatrix().col(1); // camera z (forward) vector float camrot = dot(camx, vec3(0,0,1)) + dot(camz, vec3(0,1,0)); 

还有其他方法可以获取凸轮值。 最重要的是,当旋转相机时,它应该连续变化。 矩阵本身的构造如下:

 mat3 scaleBias1 = ( 2.0f, 0.0f, -1.0f, 0.0f, 2.0f, -1.0f, 0.0f, 0.0f, 1.0f, ); mat3 rotation = ( cos(camrot), -sin(camrot), 0.0f, sin(camrot), cos(camrot), 0.0f, 0.0f, 0.0f, 1.0f ); mat3 scaleBias2 = ( 0.5f, 0.0f, 0.5f, 0.0f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, ); mat3 uLensStarMatrix = scaleBias2 * rotation * scaleBias1; 

比例偏置矩阵需要纹理原点偏移,因此我们可以相对于图像中心旋转爆炸形

结论


所以现在一切! 此方法演示了相对简化的后期处理如何使镜头光晕看起来不错。 它不是完全真实的照片,但是如果使用正确,它可以产生出色的效果。

图片



UPD
作者还发表了一篇文章 ,进行了一些小的优化。
源代码可以在这里这里看到。

Source: https://habr.com/ru/post/zh-CN439408/


All Articles