unity shader入门精要——第12章
来源:互联网 发布:布尔玛和孙悟空 知乎 编辑:程序博客网 时间:2024/05/16 17:18
屏幕后处理效果是游戏中实现屏幕特效的常用方法。这章学习如何在unity中利用渲染纹理实现各种常见的屏幕后处理效果。第一部分解释实现屏幕后处理效果的原理,并建立一个基本的屏幕后处理脚本系统。第二部分,实现一个简单的调整画面亮度、饱和度、对比度的屏幕特效。第三部分,接触到图像滤波的概念,利用Sobel算子在屏幕空间中对图像进行边缘检测,实现描边效果。第四部分,实现高斯模糊的特效。第五部分,实现bloom。第六部分,运动模糊。
建立一个基本的屏幕后处理脚本系统
屏幕后处理发生在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现屏幕特效。使用这个技术可以为游戏画面添加更多的艺术效果,例如景深、运动模糊等。
因此,想要实现屏幕后处理必须先得到渲染后的屏幕图像,即抓取屏幕。unity的接口OnRenderImage,在渲染完所有物体后发生。OnRenderImage(RenderTexture src, RenderTexture dest);
调用该函数,unity把当前渲染得到的图像存储到第一个参数对应的源渲染纹理中,通过函数的一系列操作后,再把目标渲染纹理即第二个参数显示到屏幕上。在OnRenderImage函数中,我们可以用Graphics.BLit(Texture src, RenderTexture dest, material mat, int pass = -1);
src是源纹理,dest为目标纹理,若为空,则直接输出到屏幕上。mat是我们用的材质,该材质会进行一系列后期处理。而src纹理会传递给shader中名为_MainTex的纹理属性。pass为-1,表示依次调用shader内的所有pass。否则,只调用给定索引的pass。
默认情况下,OnRenderImage会在透明和不透明物体执行完后调用,以便对所有物体都有影响。但有时,我们希望在不透明的pass执行完后调用,这是要在OnRenderImage前加上ImageEffectOpaque属性。
在unity中实现屏幕后处理效果一般步骤:
1. 添加一个后处理的脚本;
2. 在脚本中,实现OnRenderImage来获取当前屏幕的渲染纹理;
3. 在OnRenderImage中调用Graphics。Blit使用特定的shader对当前图像进行处理。
4. 把返回的图像显示到屏幕上。
5. 一些复杂的屏幕后处理效果可能需要多次调用Graphics.Blit。
在进行屏幕后处理前,需要检查平台是否支持渲染纹理和屏幕特效,是否支持当前shader等。所以我们创建了一个基类。实现各种屏幕后处理特效时,继承该基类,并实现派生类。
using UnityEngine;using System.Collections;[ExecuteInEditMode][RequireComponent (typeof(Camera))]public class PostEffectsBase : MonoBehaviour { // Called when start protected void CheckResources() { bool isSupported = CheckSupport(); if (isSupported == false) { NotSupported(); } } // Called in CheckResources to check support on this platform protected bool CheckSupport() { if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) { Debug.LogWarning("This platform does not support image effects or render textures."); return false; } return true; } // Called when the platform doesn't support this effect protected void NotSupported() { enabled = false; } protected void Start() { CheckResources(); } // Called when need to create the material used by this effect protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) { if (shader == null) { return null; } if (shader.isSupported && material && material.shader == shader) return material; if (!shader.isSupported) { return null; } else { material = new Material(shader); material.hideFlags = HideFlags.DontSave; if (material) return material; else return null; } }}
一些屏幕特效可能需要更多的设置,例如设置一些默认值等,可以重载Start、CheckResources等。
一般都需要一个shader,需要checkShaderANdCreateMaterial。
亮度、对比度、饱和度
- Graphics。Blit(src,dest, mat)会把第一个参数传递给shader的_MainTex属性,所以shader中必须有该属性。
- 屏幕后处理实际上是在场景中渲染了一个与屏幕同宽同高的四边形面片,为了防止它对其他物体产生影响,我们需要设置相关状态。我们关闭了深度写入,防止它挡住后面渲染的物体。例如,如果当前的OnRenderImage是在所有不透明的pass之后被调用的,那么不关闭深度写入会影响后面透明的pass 的渲染。这些状态设置是屏幕后处理的标配。
ZTest Always Cull Off ZWrite Off - _Brightness 参数乘以原颜色调整亮度。
得到该像素的亮度值,用该亮度创建一个饱和度为0的颜色,并使用_Saturation属性在其和上面调整亮度后的颜色直接插值得到饱和度颜色。
先创建一个对比度为0的颜色(各个分量都是0.5),再使用_Contrast将其与上面饱和度调整后的值lerp。得到最终的结果。 - 关闭该shader的fallback: Fallback Off
边缘检测
原理:使用一些边缘检测算子对图像进行卷积操作。
1. 在v2f结构中定义了一个维数为9的纹理数组,对应了使用Sobel算子采样是需要的9个邻域纹理坐标。通过把计算采样纹理坐标的代码从片元着色器移动到顶点着色器会减少运算,提高性能。由于从顶点着色器到片元着色器的插值是线性的,因此不会影响结果。
2. 片元着色器,首先调用Sobel函数计算当前像素的梯度值edge。首先定义水平和竖直方向的卷积核Gx和Gy。然后依次对9个像素进行采样,计算它们的亮度值。将亮度值与卷积核对应的权重相乘后,叠加到各自的梯度值上。最后,从1中减去水平方向和竖直方向的梯度值的绝对值,得到edge。edge越小越可能是边缘点。
3. 利用edge混合边缘色和原图得到颜色,再利用edge混合边缘色和背景色得到颜色,再利用_EdgeOnly在两者间插值得到最终的像素值。
高斯模糊
高斯滤波
利用高斯核,同样是卷积运算。距离越大,影响越大——高斯方程。
N大小变大,N*N变得更大,采样次数会变得非常巨大。幸运的是,我们能把这个二维的高斯函数拆分为两个一维函数。
实现
调用两个pass,一个pass将使用竖直方向的一维高斯核对图像进行滤波,一个pass用水平方向的。还将用图像缩放(降采样)减少运算数量,提高性能;调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)。
1. blurSpread和downSample都是出于性能的考虑,高斯核数不变的情况下,blurSpread越大,模糊程度越高,但采样数却不会受影响。但过大的blurSpread会造成虚影。downSample越大,需要处理的像素值越少,同时也进一步提高模糊程度,但过大的downSample可能会使图像像素化。
2. 由于高斯模糊需要调用两个pass,需要使用一块中间缓存来存储第一个pass执行完毕后的模糊结果,所以用RenderTexture.GetTemporary函数分配一块与屏幕图像大小相同的缓冲区。blit完毕后,需要调用RenderTexture.ReleaseTemporary来释放之前分配的缓存。
3. 我们为两个pass定义公用的片元着色器,我们只记录3个高斯权重,声明各个领域像素对应的权重,再将邻域像素值和权重相乘后的结果叠加到sum中,返回sum.
4. 我们为两个pass使用了NAME语义定义他们的名字,名字都用大写字母。这样,可以在其他shader中引用该pass。
bloom
原理:根据一个阈值提取出图像中较亮的区域,把他们存储到一张渲染纹理中,用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其与原图像进行混合,达到最终的效果。
1. 降采样,buffer0更小;
2. 使用shader中第1个pass提取图像中较亮区域,存到buffer0;
3. 迭代进行高斯模糊:每个迭代包括水平模糊和竖直方向模糊;
4. 第4个pass将模糊后的较亮区域与源图像混合。
运动模糊
运动模糊的实现有多种方法:1. 累计缓存:利用一块累计缓存来混合多张连续的图像。当物体快速产生多张图像后,取他们之间的平均值作为最终的结果。这种方法对性能的消耗很大,因为想要获得多张帧图像往往要在同一帧中渲染多次场景。2.速度缓存,这个缓存中存储了各个像素的当前速度,利用该值决定模糊的方向和大小。
我们这里采用类似第一种方法,不需要在一帧中将场景渲染几次,但需要保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。这种方法比累计缓存的方法性能好,但效果可能会略有影响。
1. 创建继承基类的脚本;
2. 定义运动模糊在混合图像时使用的模糊参数;该值越大,运动拖尾的效果就越明显。
3. 定义一个RenderTexture类型的变量,保存之前图像叠加的结果;
4. 在该脚本不运行时,即调用OnDisable时,立即销毁RT,因为我们希望下一次开始应用运动模糊时重新叠加图像;
5. OnRenderImage里,判断是否存在RT,若不存在要创建;若存在,判断是否是屏幕大小,若不是,需要销毁后重建,RenderTexture的hideFlags参数是HideAndDontSave,这表示不会显示在hierarchy,也不会保存到场景。创建完后,将当前的帧图像初始化accumulationTexture,用blit;
6. 得到该accumulationTexture后,调用其MaskRestoreExpect函数来表明我们需要进行一个RT的恢复操作。恢复操作发生在渲染到该纹理而该纹理又没有被提前清空或销毁的情况下。在本例中,每次都用OnRenderImage需要把当前的帧图像和accumulationTexture中的图像混合,accumulationTexture不需要清空,因为它保存了我们之前的混合结果。
7. 我们调用Graphics.blit把当前的屏幕图像叠加到accumulationTexture上,最后用blit把结果显示到屏幕上。