PostProcessing是现代游戏中必不可少的技术之一,本文简单来总结下PostProcessing的实现原理和应用.因为详细写起来需要很大篇幅且很费时间,这里只简单介绍下原理.
1.基础部分
PostProcessing,通常在普通的场景渲染结束后对结果进行处理,将一张或数张Texture处理得到一张新的Texture.
PostProcessing的渲染Pipeline普通的模型渲染一样,不同之处在于在VertexShader中通常只是简单的拷贝,主要的逻辑写在Pixel Shader中.
拷贝图片的图元是什么样的呢?直觉来看就是一个长方形图元,由两个三角形组成.但是一般是通过一个大一点单个三角形图元来实现的,超出长方形框外的部分在光栅化时会被自动过滤掉,这样写起来更简洁,运行速度也稍快一些.
一个三角形的渲染拷贝作用和一个长方形是一样的
需要注意的是,OpenGL中Texture左下角UV点是(0,0),而DirectX中左上角UV是(0,0).
另外Direct3D 9中有Half-Pixel Offset的问题,采样像素时需要将uv点偏移半像素或者计算顶点坐标时时偏移半像素(游戏引擎里一般是直接处理编译好的shader,这样不用为不同版本写不同的shader),来看下微软文档里的几张图,就能直观地知道Half-Pixel 是什么问题:
Direct3D 10 Pixel Coordinate
Direct3D 9 Pixel Coordinate
Direct3D Texel Coordinate
2.UV的使用
根据Texture中点UV的值,施加不同的效果,比如常见的Vignette效果,就是根据到屏幕中心点的距离,混合黑色的颜色,产生边缘灰暗的效果,非常的简单.
Vignette效果
根据Texture中像素点的uv值做偏移,可以得到一些变形,扭曲,波浪,镜头变形等效果.比如下面的一个波浪的效果,FS代码大致是这样的:
波浪效果
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,长得是这样的:
Perlin Noise Texture
比如用Perlin Noise Texture来生成一个Dissolve的效果,就是指定一个阈值,动态clip掉Perlin Noise Texture中相应点的灰度值在阈值下的点:
Dissolve
随时间改变这个阈值,就可以得到逐渐出现或者消失的Dissolve效果.
还有的Texture保存一些(u,v)到某些值的映射,叫做Loopup Texture(LUT).很多张Texture组成Array还可以完成(x,y,z)到值的映射,叫做3D LUT.
LUT最常见的应用就是用来做Color Grading.
一个用来ColorGrading的LUT
4.Image Kernels
将在UV当前点周围数点采样,然后将多个点处理结果相加,这样的处理写成矩阵叫做Image Kernel,这样的操作也可以叫做Convolution,比如一个边缘检测的Kernel是这样的:
将中心点的像素值X8,然后减去周围点的像素值,就可以做边缘检测.
Edge Detection
一些常见的如Sharpen, Blur等各种各样的效果都是通过这种方式实现的.
5.Gaussian Blur
从信号和采样的角度来说,有这样几种Filtering:
box filter,tent filter,sinc filter 所有filter的面积和是1
Sinc filtering计算起来最复杂,效果也是最好的.
Filtering来重构信号的过程大致是这样:
sinc filter
Guassian Blur可以看作是Sinc Filter的一种近似,用这样一个距离相关的Kernel 函数:
实际操作时,忽略较远处的值,用预计算好的权重值,比如一个5X5的Kernel:
5X5 Guassion weight
这样每次计算时需要采样25个点才能得到最终的像素值,因此可以将Guassion blur分成两次Pass来实现,Kernel是这样的:
两次Pass的Guassion weight
这样每个点需要的采样次数就降到了10次.Box-filter(周围N*N个点权重相等)也是可以像这样分成两次Pass的,这样的Kernel是separable的.
当GPU从贴图中采样时,使用bilinear方式在某个点采样时,是根据点的位置从目标点周围4点中取值加权平均,利用这点,也可以减少需要的采样次数.比如一个3X3的Box-filter时,所有点的权重相等,将采样目标点放到4点中心,就可以得到周围4点的平均值:
9个采样点降到5个和4个
Guassion blur也可以通过这中方法来减少采样数,两种方式结合起来,可以使采样数达到最小.
6.DownSample/UpSample
一些需要Blur效果的PostProcessing通常将将TextureDownSample到一个较小的尺寸,自带Blur的效果基本上不会影响最总的效果,较小的Texure还可以加快运行速度.
Bloom的效果,就是使较亮的部分更亮点,并且扩散到附近.就是通过一系列的DownSample和UpSample,最后叠加到原来图片上来实现的.
Bloom 4次DownSample
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的效果.
再看下Depth of Field(DOF 景深)的效果,DOF先设置一个对焦的距离和范围,超出对焦范围的部分做blur处理.一个简单实现的DOF的大致过程如下:
a.将原图DownSample到1/4大小.
b.确定模糊的强度,模糊强度随距离变化的曲线大致如下图所示,根据深度值计算每个点的模糊强度,保存到一张Texture中.
dof强度和距离的关系
c.对DownSample的Texture进行Blur,每个点使用上面的Texture中的模糊强度计算模糊半径.用模糊半径在一个预计算好的Disk中随机分布的点位置进行采样,点的分布大致是下面的图片中所示.
d.将得到的Texture用9-tap Tent Filter再次Blur.
e.将得到的Texture和原图按照计算出的模糊强度混合,得到DOF处理后的图片.
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计算:
- [numthreads(HISTOGRAM_THREAD_X, HISTOGRAM_THREAD_Y, 1)]
- void KEyeHistogram(uint2 dispatchThreadId : SV_DispatchThreadID, uint2 groupThreadId : SV_GroupThreadID)
- {
- const uint localThreadId = groupThreadId.y * HISTOGRAM_THREAD_X + groupThreadId.x;
- // 使用其中部分Thread清零共享内存
- if (localThreadId < HISTOGRAM_BINS)
- {
- gs_histogram[localThreadId] = 0u;
- }
- float2 ipos = float2(dispatchThreadId) * 2.0;
- //Thread之间同步等待
- GroupMemoryBarrierWithGroupSync();
- if (ipos.x < _ScaleOffsetRes.z && ipos.y < _ScaleOffsetRes.w)
- {
- uint weight = 1u;
- float2 sspos = ipos / _ScaleOffsetRes.zw;
- // Vignette 权重,中心位置点的权重会更高
- #if USE_VIGNETTE_WEIGHTING
- {
- float2 d = abs(sspos - (0.5).xx);
- float vfactor = saturate(1.0 - dot(d, d));
- vfactor *= vfactor;
- weight = (uint)(64.0 * vfactor);
- }
- #endif
- float3 color = _Source.SampleLevel(sampler_LinearClamp, sspos, 0.0).xyz; // Bilinear downsample 2x
- //计算明亮度
- float luminance = Luminance(color);
- float logLuminance = GetHistogramBinFromLuminance(luminance, _ScaleOffsetRes.xy);
- //根据明亮度计算得到需要累加到的bin位置
- uint idx = (uint)(logLuminance * (HISTOGRAM_BINS - 1u));
- //相应bin位置的计数原子累加
- InterlockedAdd(gs_histogram[idx], weight);
- }
- //同步等待
- GroupMemoryBarrierWithGroupSync();
- //使用其中的部分Thread来将累计值从共享内存写入到RWBuffer
- if (localThreadId < HISTOGRAM_BINS)
- {
- InterlockedAdd(_HistogramBuffer[localThreadId], gs_histogram[localThreadId]);
- }
复制代码
这样我们就得到带有明亮值分布的Histogram,不仅可以用来计算平均明亮值,还可以根据分布情况作进一步优化.
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.
SSAO Sample Kernel
c.将得到的AO Buffer进行Blur.
d.根据AO值修改Diffuse的光照值,产生AO的效果.
SSAO的效果
SSR的原理和SSAO很相似,需要用到Normal Buffer和Metal Buffer(表示镜面光反照率,一般Gbuffer中有),根据需要计算反射光照的点的Normal和Metal值,计算得到一条反射光线,沿着这条线进行RayMarch,得到第一个一个与该射线相交的点,将该点的颜色当作反射的颜色进行混合.
步长不断变化的RayMarching
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
|