游戏开发技术论坛

 找回密码
 立即注册
搜索
查看: 1401|回复: 0

游戏场景渲染中的PostProcessing(后处理)

[复制链接]

1万

主题

1万

帖子

3万

积分

版主

Rank: 7Rank: 7Rank: 7

积分
36572
发表于 2020-3-13 11:35:42 | 显示全部楼层 |阅读模式
PostProcessing是现代游戏中必不可少的技术之一,本文简单来总结下PostProcessing的实现原理和应用.因为详细写起来需要很大篇幅且很费时间,这里只简单介绍下原理.

1.基础部分

PostProcessing,通常在普通的场景渲染结束后对结果进行处理,将一张或数张Texture处理得到一张新的Texture.

PostProcessing的渲染Pipeline普通的模型渲染一样,不同之处在于在VertexShader中通常只是简单的拷贝,主要的逻辑写在Pixel Shader中.

拷贝图片的图元是什么样的呢?直觉来看就是一个长方形图元,由两个三角形组成.但是一般是通过一个大一点单个三角形图元来实现的,超出长方形框外的部分在光栅化时会被自动过滤掉,这样写起来更简洁,运行速度也稍快一些.

image001.jpg
一个三角形的渲染拷贝作用和一个长方形是一样的

需要注意的是,OpenGL中Texture左下角UV点是(0,0),而DirectX中左上角UV是(0,0).

另外Direct3D 9中有Half-Pixel Offset的问题,采样像素时需要将uv点偏移半像素或者计算顶点坐标时时偏移半像素(游戏引擎里一般是直接处理编译好的shader,这样不用为不同版本写不同的shader),来看下微软文档里的几张图,就能直观地知道Half-Pixel 是什么问题:

image003.jpg
Direct3D 10 Pixel Coordinate

image004.jpg
Direct3D 9 Pixel Coordinate

image005.jpg
Direct3D Texel Coordinate

2.UV的使用

根据Texture中点UV的值,施加不同的效果,比如常见的Vignette效果,就是根据到屏幕中心点的距离,混合黑色的颜色,产生边缘灰暗的效果,非常的简单.

image006.jpg
Vignette效果

根据Texture中像素点的uv值做偏移,可以得到一些变形,扭曲,波浪,镜头变形等效果.比如下面的一个波浪的效果,FS代码大致是这样的:

image007.jpg
波浪效果

vec2 v = UV - vec2(0.5, 0.5);

vec2 offset = sin((length(v) * 5 + TIME) * PI * 2) * normalize(v) * 0.1;

COLOR = texture(TEXTURE, UV + offset);

根据到中心点的距离,用正弦函数计算沿着中心方向UV的偏移量,就可以得到波浪的效果.然后在相位上加上时间,可以得到随时间变化的动态波浪.

3.Noise Texture/Lookup Texture

噪声图也是非常常用的一种非常常用的Texture,通常我们见到的都是Perlin Noise Texture,长得是这样的:

image008.png
Perlin Noise Texture

比如用Perlin Noise Texture来生成一个Dissolve的效果,就是指定一个阈值,动态clip掉Perlin Noise Texture中相应点的灰度值在阈值下的点:

image009.png
Dissolve

随时间改变这个阈值,就可以得到逐渐出现或者消失的Dissolve效果.

还有的Texture保存一些(u,v)到某些值的映射,叫做Loopup Texture(LUT).很多张Texture组成Array还可以完成(x,y,z)到值的映射,叫做3D LUT.

LUT最常见的应用就是用来做Color Grading.

image010.png
一个用来ColorGrading的LUT

4.Image Kernels

将在UV当前点周围数点采样,然后将多个点处理结果相加,这样的处理写成矩阵叫做Image Kernel,这样的操作也可以叫做Convolution,比如一个边缘检测的Kernel是这样的:

QQ截图20200313113322.png

将中心点的像素值X8,然后减去周围点的像素值,就可以做边缘检测.

image011.jpg
Edge Detection

一些常见的如Sharpen, Blur等各种各样的效果都是通过这种方式实现的.

5.Gaussian Blur

从信号和采样的角度来说,有这样几种Filtering:

image012.jpg
box filter,tent filter,sinc filter 所有filter的面积和是1

Sinc filtering计算起来最复杂,效果也是最好的.

Filtering来重构信号的过程大致是这样:

image014.jpg
sinc filter

Guassian Blur可以看作是Sinc Filter的一种近似,用这样一个距离相关的Kernel 函数:

QQ截图20200313113250.png

实际操作时,忽略较远处的值,用预计算好的权重值,比如一个5X5的Kernel:

image016.jpg
5X5 Guassion weight

这样每次计算时需要采样25个点才能得到最终的像素值,因此可以将Guassion blur分成两次Pass来实现,Kernel是这样的:

image017.jpg
两次Pass的Guassion weight

这样每个点需要的采样次数就降到了10次.Box-filter(周围N*N个点权重相等)也是可以像这样分成两次Pass的,这样的Kernel是separable的.

当GPU从贴图中采样时,使用bilinear方式在某个点采样时,是根据点的位置从目标点周围4点中取值加权平均,利用这点,也可以减少需要的采样次数.比如一个3X3的Box-filter时,所有点的权重相等,将采样目标点放到4点中心,就可以得到周围4点的平均值:

image018.jpg
9个采样点降到5个和4个

Guassion blur也可以通过这中方法来减少采样数,两种方式结合起来,可以使采样数达到最小.

6.DownSample/UpSample

一些需要Blur效果的PostProcessing通常将将TextureDownSample到一个较小的尺寸,自带Blur的效果基本上不会影响最总的效果,较小的Texure还可以加快运行速度.

Bloom的效果,就是使较亮的部分更亮点,并且扩散到附近.就是通过一系列的DownSample和UpSample,最后叠加到原来图片上来实现的.

image019.jpg
Bloom 4次DownSample

image020.jpg
Bloom 4次UpSample和最终颜色叠加的效果

7.Depth Buffer

正常地渲染场景后,可以得到Depth Buffer.PostProcessing时,从当前的UV值,和Depth Buffer中读取到的深度值,可以得到裁剪空间中的坐标,再从裁剪空间可以反推出当前像素点在世界空间中的位置.

需要注意的是,OpenGL中是将-1~1的深度值映射到0~1的Depth Buffer的深度值,从中读取时需要将得到的值应用d * 2 - 1,得到在裁剪空间中的深度值.

得到世界空间中的坐标后,就可以应用一些效果,比如Depth Fog效果,就是计算点到摄影机的距离,根据距离用对数函数计算出一个雾的强度值,将雾的颜色叠加到原始颜色上,就可以得到Depth Fog效果.也可以指定一个地面的高度,根据到地面的距离得到Height Fog的效果.

image022.jpg

再看下Depth of Field(DOF 景深)的效果,DOF先设置一个对焦的距离和范围,超出对焦范围的部分做blur处理.一个简单实现的DOF的大致过程如下:

a.将原图DownSample到1/4大小.

b.确定模糊的强度,模糊强度随距离变化的曲线大致如下图所示,根据深度值计算每个点的模糊强度,保存到一张Texture中.

image023.jpg
dof强度和距离的关系

c.对DownSample的Texture进行Blur,每个点使用上面的Texture中的模糊强度计算模糊半径.用模糊半径在一个预计算好的Disk中随机分布的点位置进行采样,点的分布大致是下面的图片中所示.

image024.jpg

d.将得到的Texture用9-tap Tent Filter再次Blur.

e.将得到的Texture和原图按照计算出的模糊强度混合,得到DOF处理后的图片.

image025.jpg
DOF

8.Motion Blur/Temporal AA

Motion Blur的实现:每个物体记录上一帧的位置,计算出物体相对上一帧的位置偏移,算出相对镜头的速度值,渲染到一个Velocity Buffer上,再根据Velocity Buffer做Blur处理.

TAA:将每个物体的投影矩阵加上一点不同的Jittering,每帧的jittering值不断变化,将连续两帧或数帧的结果进行插值,以此来实现快速的AA.

实际的游戏中有的会将Motion Blur和TAA结合起来实现更好的效果.

这里仅仅简单介绍下这两种处理基本的原理,不再深入探究.

9.Compute Shader

Computer Shader非常适用于PostProcessing,因为不需要Rasterize,OutputMerge等阶段,Computer Shader可以运行地比Pixel Shader更快.大部分Post Processing都可以直接切换成CS的写法来获得速度提升.不仅如此,CS的GroupShaderdMemory实现执行单元间共享内存,让CS可以用来实现一些PS无法实现的功能.

来看下Auto Exposure的实现,Auto Exposure的原理,就是将所有Texture中的像素的明亮度统计,计算得到一个合适的曝光度,再应用到Texture上.

使用Pixel Shader来实现的话,将需要计算的Texture上每个点的明亮度,并DownSample到一个合适大小的Texture上,再将该Texture连续按照大小的1/2 DownSample 直到一个1X1的Texture上,该Texture上的值就是所有点的平均明亮度,就可以改变Texture的曝光度.

Pixel Shader的缺点就是只能得到所有点亮度的平均值或者加权平均值.使用CS则可以统计所有明亮度范围并进行计数,实现更加强大的Auto Exposure.

来看下一个简单的AutoExposure CS的实现:

先将Texture DownSample到某个合适的大小,再用CS计算:

  1. [numthreads(HISTOGRAM_THREAD_X, HISTOGRAM_THREAD_Y, 1)]
  2. void KEyeHistogram(uint2 dispatchThreadId : SV_DispatchThreadID, uint2 groupThreadId : SV_GroupThreadID)
  3. {
  4.     const uint localThreadId = groupThreadId.y * HISTOGRAM_THREAD_X + groupThreadId.x;

  5.     // 使用其中部分Thread清零共享内存
  6.     if (localThreadId < HISTOGRAM_BINS)
  7.     {
  8.         gs_histogram[localThreadId] = 0u;
  9.     }

  10.     float2 ipos = float2(dispatchThreadId) * 2.0;

  11.     //Thread之间同步等待
  12.     GroupMemoryBarrierWithGroupSync();
  13.     if (ipos.x < _ScaleOffsetRes.z && ipos.y < _ScaleOffsetRes.w)
  14.     {
  15.         uint weight = 1u;
  16.         float2 sspos = ipos / _ScaleOffsetRes.zw;
  17.         // Vignette 权重,中心位置点的权重会更高
  18.         #if USE_VIGNETTE_WEIGHTING
  19.         {
  20.             float2 d = abs(sspos - (0.5).xx);
  21.             float vfactor = saturate(1.0 - dot(d, d));
  22.             vfactor *= vfactor;
  23.             weight = (uint)(64.0 * vfactor);
  24.         }
  25.         #endif

  26.         float3 color = _Source.SampleLevel(sampler_LinearClamp, sspos, 0.0).xyz; // Bilinear downsample 2x
  27.         //计算明亮度
  28.         float luminance = Luminance(color);
  29.         float logLuminance = GetHistogramBinFromLuminance(luminance, _ScaleOffsetRes.xy);
  30.         //根据明亮度计算得到需要累加到的bin位置
  31.         uint idx = (uint)(logLuminance * (HISTOGRAM_BINS - 1u));
  32.         //相应bin位置的计数原子累加
  33.         InterlockedAdd(gs_histogram[idx], weight);
  34.     }
  35.     //同步等待
  36.     GroupMemoryBarrierWithGroupSync();
  37.     //使用其中的部分Thread来将累计值从共享内存写入到RWBuffer
  38.     if (localThreadId < HISTOGRAM_BINS)
  39.     {
  40.         InterlockedAdd(_HistogramBuffer[localThreadId], gs_histogram[localThreadId]);
  41.     }
复制代码


这样我们就得到带有明亮值分布的Histogram,不仅可以用来计算平均明亮值,还可以根据分布情况作进一步优化.

image027.jpg
UE4中Eye Adaption的Debug

10.Screen-Space Methods

有了Depth Buffer来获取当前点再世界坐标的位置,就可以更进一步,在目标点周围取点,判断可见性,进行RayMarching等操作.这类方法叫做Screen-Space Method,常见的有SSAO(Screen Space Abbient Occlusion), SSR(Screen Space Reflecttion)等.

这里介绍下一个简单的SSAO的实现.

a.准备Depth Buffer和Normal Buffer,如果是Deferred Rendering,直接在GBUFFER中就能取到,如果是Forward Rendering,可以通过Depth Buffer生成.

b.在一个半球形的区域中随机取点,并且加上随机的旋转值.将随机到的点通过Normal Buffer计算得到世界空间中的坐标,再计算得到在裁剪空间中的值,并和DepthTexture中的深度值进行比较来判断是否被遮挡,将结果累加,得到一个保存AO值的buffer.

image029.jpg
SSAO Sample Kernel

c.将得到的AO Buffer进行Blur.

d.根据AO值修改Diffuse的光照值,产生AO的效果.

image030.jpg
SSAO的效果

SSR的原理和SSAO很相似,需要用到Normal Buffer和Metal Buffer(表示镜面光反照率,一般Gbuffer中有),根据需要计算反射光照的点的Normal和Metal值,计算得到一条反射光线,沿着这条线进行RayMarch,得到第一个一个与该射线相交的点,将该点的颜色当作反射的颜色进行混合.

image031.jpg
步长不断变化的RayMarching

image033.jpg
SSR

因为SSR是Screen Space的,所以无法反射屏幕之外的场景.

11.小结

(1)本文基本上介绍到了大部分比较常见的PostProcessing,其他的效果也都是通过这些方法的扩展和延伸.

(2) 各个PostProcessing的顺序非常重要,比如SSR SSAO一般在渲染透明物体前执行,DOF Bloom等一般在所有物体渲染结束后,ToneMapping之前,一些自定义的变形效果通常在ToneMapping之后.

(3)各个效果之间可能会互相干扰,比如TAA就会影响很多效果的计算,需要在计算时考虑到这些因素.

(4)各个PostProcessing之间有的可以共用一些Texture,以及在一次Pass中合并执行多个操作,来降低性能消耗.

(5)实际项目中的应用实现一般比文中提到的要复杂得多,需要细心地调节参数,不断测试改进.本文只是从原理方面简单描述下,具体的应用还需要参考更加详细的文档.

作者:TC130
专栏地址:https://zhuanlan.zhihu.com/p/105909416

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2023-2-4 10:14

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表