后处理:渲染完整个场景得到屏幕图像后,对这个图像进行操作。
过程:摄像中添加屏幕后处理脚本,OnRenderImage(RenderTexture src, RenderTexture dest)会将当前渲染的图像存储在src的纹理中,使用Graphics.Blit(RenderTexture src, RenderTexture dest, Material material)和特定的Shader对当前图像进行处理,再把dest显示在屏幕上。注:OnRenderImage是Mono的函数。Graphics.Blit会将第一个参数传给Shader中_MainTex属性。
渲染状态:关闭深度写入。
1、简单的调整亮度、饱和度、对比度
fixed4 frag(v2f i) : SV_Target
{
fixed4 renderTex = tex2D(_MainTex, i.uv);
fixed3 finalColor = renderTex.rgb * _Brightness;
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
finalColor = lerp(luminanceColor, finalColor, _Saturation);
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor, renderTex.a);
}
2、边缘检测
卷积操作:使用卷积核对图像的每个像素进行操作。常见的边缘卷积算子(卷积核)有Sobel,Roberts等。
由于卷积需要对相邻区域内的纹理进行采样,需要利用_MainTex_TexelSize(可以访问纹理对应的每个纹素的大小)计算相邻区域的纹理坐标。下面是使用Sobel算子实现边缘检测。
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;//存储了邻域的纹理坐标,从片元放在顶点着色器,减少运算,提高性能。
}
片元着色器:
fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i); //计算当前像素的梯度值
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
简单解释一下lerp:
float lerp(float a, float b, float w) {
return a + w*(b-a);
}
Sobel函数如下:
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
half Sobel(v2f i) {
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
const half Gy[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;
for (int it = 0; it < 9; it++) {
texColor = luminance(tex2D(_MainTex, i.uv[it]));
edgeX += texColor * Gx[it];
edgeY += texColor * Gy[it];
}
half edge = 1 - abs(edgeX) - abs(edgeY);
return edge;
}
对9个像素进行采样,计算亮度值,再与水平和竖直方向的卷积核中对应的权重相乘,最后叠加到各自的梯度值上。edge越小越是边缘。
注:上述我们是对屏幕的颜色信息进行的边缘检测,由于实际中纹理、阴影等很多信息会影响这个结果,因此,会对深度纹理和法线纹理进行边缘检测。
3、高斯模糊
均值模糊也是使用了卷积操作,且卷积核中的每个元素值相等,且相加为1,卷积后得到的像素值是其相邻域中各个像素的平均值。中值模糊是邻域中所有像素排序后取中值为像素颜色。
高斯模糊使用的卷积核叫高斯核。高斯核是正方形的,每个元素按下面这个方程计算,x,y对应当前到中心的距离。
高斯核的维数越高,模糊程度越大。N*N的高斯核对W*H的图像要采样N*N*W*H次。但是,我们可以将二维的拆成两个一维的,使用两个一维的先后滤波,采样次数2*N*W*H。
4、bloom效果
让画面中较亮的部分扩散到周围,朦胧感。实现方法:根据一个阈值将图像中较亮的部分存在一张纹理中,再对这张纹理高斯模糊,最后将它和原图像混合。
提取较亮的部分:
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(c) - _Luminance, 0.0, 1.0);
return c * val;
5、SSAO(屏幕空间环境光遮蔽)
用来模拟环境光,被遮蔽的物体环境光要稍微暗一些。
原理:对铺屏四边形上的每片段,根据周边深度值计算遮蔽因子。这个遮蔽因子用来减少或者抵消片段的环境光照分量。高于片段深度值样本的个数就是遮蔽因子。遮蔽因子是通过采集片段周围球型核心(Kernel)的多个深度样本,并和当前片段深度值对比而得到的。
实现方式(有很多种):
基于法线的半球积分:计算P的遮挡值,需要把它周围的像素都当成一个小球,然后计算这个小球对它遮挡值的总和。影响因素有:周围像素点到P的距离,周围像素点到P的向量V和P点的法线的夹角。
基于视线方向的球积分。
据不同的需求给SSAO采样点分级,比如要好点的效果可以采样32次以上,要性能高点可以采样8次就够了。有较高要求的SSAO给结果进行模糊处理 降噪 会使明暗程度更加平滑。在较低次数采样下基于View方向的球积分效果表现更好,在较高次数采样下基于normal方向的半球积分表现效果更好。总的来说基于normal方向的计算效果更佳逼真。
6、DOF(景深)
现实世界中,相机只能聚焦到一个特定距离的焦平面,焦平面内侧和外侧较远的物体都会被虚化模糊。景深效果可以提高场景深度感,突出重点物体,比如人物摄影的时候虚化背景。焦点上的清晰度是最高的,其余的影像清晰度随着它与焦点的距离成正比例下降。
原理:通过两张图片,一张清晰的,一张经过高斯模糊的,然后根据图片中每个像素的深度值在两张图片之间差值,就可以达到景深的效果了。模糊的方法可以用前面介绍的高斯模糊。
Camera Depth Texture记录了屏幕空间所有物体距离相机的距离。
参考:https://blog.csdn.net/puppet_master/article/details/52819874,写的很不错,实现也有。
7、SSR
8、AA
会增加内存消耗,低内存机型上关掉。
(持续更新中)