求知若饥,虚心若愚
屏幕后处理效果(screen post-processing effects)是游戏中实现屏幕特效的常见方法。屏幕后处理通常指的是在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特性。例如景深、运动模糊等
建立一个基本的屏幕后处理脚本系统
屏幕后处理的基础在于得到渲染后的屏幕图像,即抓取屏幕,在unity中可以使用接口OnRenderImage函数:
1 void MonoBehaviour.OnRenderImage(RenderTexture src, RenderTexture dest)
在脚本声明此函数后,Unity会把当前渲染得到的图像存储在第一个参数对应的源渲染纹理中,通过函数内自定义的一系列操作后,再把目标渲染纹理,即第二个参数对应的渲染纹理显示到屏幕上。所以我们需要把结果输出给第二个参数,通常是使用Graphics.Blit 函数来完成对渲染纹理的处理。
1 2 3 4 // pass值为-1代表依次调用shader内的所有pass public static void Blit(Texture src, RenderTexture dest); public static void Blit(Texture src, RenderTexture dest, Materail mat, int pass = -1); public static void Blit(Texture src, RenderTexture dest, int pass = -1);
通常情况下,OnRenderImage函数会在所有的不透明和透明的Pass执行完毕后被调用,以便对场景中所有游戏对象都产生影响。但可以在OnRenderImage函数前面添加ImageEffectOpaque属性来改变渲染的顺序。
要在Unity种实现屏幕后处理效果,过程通常如下:
先在相机中添加用于后处理的脚本,实现OnRenderImage获取当前屏幕的渲染纹理
再调用Graphics.Blit使用特定的UnityShader来对当前图像进行处理,再把返回的渲染纹理显示再屏幕上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 // 用于屏幕后处理效果的一个基类,增加了检查当前平台是否支持功能,以及创建用于处理渲染纹理的材质 using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Unity_Shaders_Learn { [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class PostEffectsBase : MonoBehaviour { protected void Start() { CheckResource(); } protected void CheckResource() { bool isSupported = CheckSupport(); if (isSupported == false) NotSupported(); } protected void NotSupported() { enabled = false; } 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 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) { hideFlags = HideFlags.DontSave }; return material ? material : null; } } } }
调整屏幕的亮度、饱和度和对比度
挂载相机部分的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 using System; using UnityEngine; namespace Unity_Shaders_Learn { public class BrightnessSaturationAndContrast : PostEffectsBase { public Shader briSatConShader; private Material _briSatConMaterial; public Material BriSatConMaterial { get { _briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, _briSatConMaterial); return _briSatConMaterial; } } [Header("亮度"), Range(0.0f, 3.0f)] public float brightness = 1.0f; [Header("饱和度"), Range(0.0f, 3.0f)] public float saturation = 1.0f; [Header("对比度"), Range(0.0f, 3.0f)] public float contrast = 1.0f; private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (BriSatConMaterial != null) { BriSatConMaterial.SetFloat("_Brightness", brightness); BriSatConMaterial.SetFloat("_Saturation", saturation); BriSatConMaterial.SetFloat("_Contrast", contrast); Graphics.Blit(src, dest, BriSatConMaterial); } else { Graphics.Blit(src, dest); } } } }
编辑器警告:在PostEffectsBase.cs中,SystemInfo.supportsImageEffects和SystemInfo.supportRenderTexture是obsolete(过时的),这两个永远返回true,不要使用它。猜测现代显卡可能都支持了?
下面是着色器部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 Shader "Unity Shaders Learn/Chapter12/BrightnessSaturationAndContrast" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} // 可以不暴露出去,属性由脚本传进来 _Brightness ("Brightness", Float) = 1 _Saturation ("Saturation", Float) = 1 _Contrast ("Contrast", Float) = 1 } SubShader { Pass { // 防止影响后续物体的渲染 Cull Off ZWrite Off ZTest Always CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; half _Brightness; half _Saturation; half _Contrast; struct a2v { float4 vertex : POSITION; half2 texcoord : TEXCOORD0; }; struct v2f { half2 uv : TEXCOORD0; float4 pos : SV_POSITION; }; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 renderTex = tex2D(_MainTex, i.uv); // 亮度 fixed3 finalColor = renderTex.rgb * _Brightness; // 饱和度,先计算亮度值,根据这个亮度值得出饱和度为0的颜色值,在使用饱和度为0的颜色值进行插值 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); // 对比度,使用对比度为0的颜色值进行插值 fixed3 avgColor = fixed3(0.5, 0.5, 0.5); finalColor = lerp(avgColor, finalColor, _Contrast); return fixed4(finalColor, renderTex.a); } ENDCG } } Fallback Off }
亮度:通过每个颜色分量乘以一个特定的系数
饱和度:计算当前亮度的亮度值,使用该亮度值创建一个饱和度为0的颜色值(一般是灰色),再根据饱和度值与其插值
对比度:创建对比度为0的颜色值(各分量为0.5),再根据对比度值与其插值
使用了内置结构体appdata_img,可看UnityCG.cginc中的定义,其实就是常规的v2f,但texcoord使用了half2节省空间。
1 2 3 4 5 6 struct appdata_img { float4 vertex : POSITION; half2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID };
右图:调整了亮度(值为1.2)、饱和度(值为1.6)和对比度(值为1.2)后的效果
边缘检测
边缘检测是描边效果的一种实现方法,原理是利用一些边缘检测算子对图像进行 卷积(convolution) 操作。
什么是卷积
在图像处理中,卷积操作指的是使用一个 卷积核(kernel) 对一张图像中的每个像素进行一系列操作。卷积核通常是一个四方形网格结构(例如2x2、3x3的方形区域),该区域内的每一个方格都有一个权重值。
下面使用一个3×3大小的卷积核对一张5×5大小的图像进行卷积操作,当计算图中红色方块对应的像素的卷积结果时,我们首先把卷积核的中心放置在该像素位置,翻转核之后再依次计算核中每个元素和其覆盖的图像像素值的乘积并求和,得到新的像素值
常见的边缘检测算子
边是如何形成的: 如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,我们就会认为它们之间应该有一条边界。这种相邻像素之间的差值可以用 梯度(gradient) 来表示。
三种常见的边缘检测算子
它们都包含了两个方向的卷积核,分别用于检测水平方向和竖直方向上的边缘信息。在进行边缘检测时,对每个像素分别进行一次卷积计算,得到两个方向上的梯度值 和 ,整体的梯度值为 ,处于性能考虑有时会使用绝对值操作代替开根号 。
实现
使用Sobel算子进行边缘检测,实现描边效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 using System; using UnityEngine; namespace Unity_Shaders_Learn { public class EdgeDetection : PostEffectsBase { public Shader edgeDetectShader; private Material _edgeDetectMaterial; public Material EdgeDetectMaterial { get { _edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, _edgeDetectMaterial); return _edgeDetectMaterial; } } [Header("是否仅渲染边缘"), Range(0.0f, 1.0f)] public float edgesOnly = 0.0f; [Header("边缘颜色")] public Color edgeColor = Color.black; [Header("背景颜色")] public Color backgroundColor = Color.white; private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (EdgeDetectMaterial != null) { EdgeDetectMaterial.SetFloat("_EdgeOnly", edgesOnly); EdgeDetectMaterial.SetColor("_EdgeColor", edgeColor); EdgeDetectMaterial.SetColor("_BackgroundColor", backgroundColor); Graphics.Blit(src, dest, EdgeDetectMaterial); } else { Graphics.Blit(src, dest); } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 Shader "Unity Shaders Learn/Chapter12/EdgeDetection" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _EdgeOnly ("Edge Only", Float) = 1.0 _EdgeColor ("Edge Color", Color) = (0,0,0,1) _BackgroundColor ("Background Color", Color) = (1,1,1,1) } SubShader { Pass { // 防止影响后续物体的渲染 Cull Off ZWrite Off ZTest Always CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_TexelSize; //_MainTex对应每个纹素的大小(如512x512就是1/512) fixed _EdgeOnly; fixed4 _EdgeColor; fixed4 _BackgroundColor; struct v2f { float4 pos : SV_POSITION; half2 uv[9] : TEXCOORD0; }; // 计算亮度值 fixed luminance(fixed4 color) { return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; } // 使用Sobel算子进行卷积 half Sobel(v2f i) { // 水平方向卷积核 const half Gx[9] = {-1, -2, -1, 0, 0, 0, 1, 2, 1}; // 竖直方向卷积核 const half Gy[9] = {-1, 0, 1, -2, 0, 2, -1, 0, 1}; half texColor; half edgeX = 0; // 梯度值X half edgeY = 0; // 梯度值Y for (int it = 0; it < 9; it++) { texColor = luminance(tex2D(_MainTex, i.uv[it])); edgeX += texColor * Gx[it]; edgeY += texColor * Gy[it]; } // 值越小越可能是边缘像素,|edgeX| + |edgeY|越大说明和边缘像素差距比较大 half edge = 1 - abs(edgeX) - abs(edgeY); return edge; } v2f vert (appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; // 取Sobel算子所需的周围纹理的坐标 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 frag (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); } ENDCG } } Fallback Off }
新变量_MainTex_TexelSize,来获取每个纹素的大小。在顶点着色器中,计算了边缘检测时需要的纹理坐标。片元着色器利用Sobel函数,计算出edge值,根据edge值对最终颜色进行插值。edge值越小越边缘,越使用边缘颜色。其中Sobel函数,使用了Sobel算子,将当前像素周围的像素点进行计算梯度,根据梯度得到edge值。
注意本节实现的边缘检测仅仅利用了屏幕颜色信息,在实际应用中,物体的纹理、阴影等会影响检测。为了得到更准确的边缘信息,往往会在屏幕的深度纹理和法线纹理上进行边缘检测。
高斯模糊
卷积的另一个常见应用:高斯模糊。
实现模糊的一些方法:
均值模糊:卷积,卷积核中各元素相等且相加等于1,即取邻域内各像素的平均值
中值模糊:选择邻域内所有像素排序后的中值替换掉原颜色
高斯模糊:更高级,用高斯核进行卷积
高斯滤波
高斯模糊同样使用了卷积计算,使用的卷积核名为高斯核。高斯核是一个正方形大小的滤波核,其中每个元素的计算都是基于下面的高斯方程:
其中σ是标准方差(一般取值为1),x和y分别对应了当前位置到卷积核中心的整数距离。为了保证滤波后的图像不会变暗,对高斯核中的权重进行归一化和为1,所以e前面的系数实际不会对结果有任何影响。
高斯方程很好地模拟了邻域每个像素对当前处理像素的影响程度–距离越近,影响越大。高斯核维数越高,模糊程度越大。
使用一个NxN的高斯核对图像进行卷积滤波,就需要NxNxWxH次纹理采样(消耗十分巨大)。但可以把这个二维高斯函数拆分成两个一维函数。即使用两个一维的高斯核先后对图像进行滤波,得到的结果和直接使用二维高斯核是一样的。但采样次数只需要2xNxWxH。
一个5×5大小的高斯核。左图显示了标准方差为1的高斯核的权重分布。我们可以把这个二维高斯核拆分成两个一维的高斯核(右图)
下面会使用上述5x5的高斯核对原图像进行高斯模糊,先后调用两个Pass。第一个使用竖直方向的一维高斯核对图像进行滤波,第二个使用水平方向的一维高斯核对图像进行滤波。在实现中,还会利用图像缩放来进一步提高性能,并通过调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 using System; using UnityEngine; namespace Unity_Shaders_Learn { public class GaussianBlur : PostEffectsBase { public Shader gaussianBlurShader; private Material _gaussianBlurMaterial; public Material GaussianBlurMaterial { get { _gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, _gaussianBlurMaterial); return _gaussianBlurMaterial; } } [Header("高斯模糊迭代次数"), Range(0, 4)] public int iterations = 3; [Header("模糊范围"), Range(0.2f, 3.0f)] public float blurSpread = 0.6f; [Header("缩放系数"), Range(1, 8)] public int downSample = 2; private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (GaussianBlurMaterial != null) { #region 基础版本 // int rtW = src.width; // int rtH = src.height; // RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0); // // // 竖直方向的高斯核pass // Graphics.Blit(src, buffer, GaussianBlurMaterial, 0); // // 水平方向的高斯核pass // Graphics.Blit(buffer, dest, GaussianBlurMaterial, 1); // // // 释放缓存 // RenderTexture.ReleaseTemporary(buffer); #endregion #region 在基础版本加入缩放图像来降采样 // int rtW = src.width / downSample; // int rtH = src.height / downSample; // RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0); // buffer.filterMode = FilterMode.Bilinear; //双线性滤波 // // // 竖直方向的高斯核pass // Graphics.Blit(src, buffer, GaussianBlurMaterial, 0); // // 水平方向的高斯核pass // Graphics.Blit(buffer, dest, GaussianBlurMaterial, 1); // // // 释放缓存 // RenderTexture.ReleaseTemporary(buffer); #endregion #region 在基础版本加入缩放图像来降采样以及多次迭代高斯模糊功能 int rtW = src.width / downSample; int rtH = src.height / downSample; RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0); buffer0.filterMode = FilterMode.Bilinear; //双线性滤波 Graphics.Blit(src, buffer0); for (int i = 0; i < iterations; i++) { GaussianBlurMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread); RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); // 竖直方向的高斯核pass Graphics.Blit(buffer0, buffer1, GaussianBlurMaterial, 0); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); // 水平方向的高斯核pass Graphics.Blit(buffer0, buffer1, GaussianBlurMaterial, 1); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; // 指向同一个引用 } Graphics.Blit(buffer0, dest); // 释放缓存 RenderTexture.ReleaseTemporary(buffer0); #endregion } else { Graphics.Blit(src, dest); } } } }
在SubShader中新使用了CGINCLUDE/ENDCG来包住一些变量和方法,相当于C++的头文件,可以在Pass中共用,适合Pass中调用了相同的方法时使用。同时为Pass定义名字,可以在其他Shader中直接通过它们的名字来使用该Pass,而不需要重复编写代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 Shader "Unity Shaders Learn/Chapter12/GaussianBlur" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _BlurSize ("Blur Size", Float) = 1.0 } SubShader { // 防止影响后续物体的渲染 Cull Off ZWrite Off ZTest Always CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_TexelSize; float _BlurSize; struct v2f { float4 pos : POSITION; half2 uv[5] : TEXCOORD0; }; v2f vertBlurVertical(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0] = uv; o.uv[1] = uv + float2(0.0f, _MainTex_TexelSize.y * 1.0f) * _BlurSize; o.uv[2] = uv - float2(0.0f, _MainTex_TexelSize.y * 1.0f) * _BlurSize; o.uv[3] = uv + float2(0.0f, _MainTex_TexelSize.y * 2.0f) * _BlurSize; o.uv[4] = uv - float2(0.0f, _MainTex_TexelSize.y * 2.0f) * _BlurSize; return o; } v2f vertBlurHorizontal(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0] = uv; o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0f, 0.0f) * _BlurSize; o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0f, 0.0f) * _BlurSize; o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0f, 0.0f) * _BlurSize; o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0f, 0.0f) * _BlurSize; return o; } fixed4 fragBlur(v2f i) : SV_Target { float weight[3] = {0.4026, 0.2442, 0.0545}; fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0]; for (int it = 1; it < 3; it ++) { sum += tex2D(_MainTex, i.uv[it * 2 - 1]).rgb * weight[it]; sum += tex2D(_MainTex, i.uv[it * 2]).rgb * weight[it]; } return fixed4(sum, 1.0); } ENDCG Pass { NAME "GAUSSIAN_BLUR_VERTICAL" CGPROGRAM #pragma vertex vertBlurVertical #pragma fragment fragBlur ENDCG } Pass { NAME "GAUSSIAN_BLUR_HORIZONTAL" CGPROGRAM #pragma vertex vertBlurHorizontal #pragma fragment fragBlur ENDCG } } }
Bloom效果
Bloom可以模拟真实相机的一种图像效果,让画面中较亮的区域“扩散”到周围的区域中,造成一种朦胧的效果。
Bloom的实现原理:首先根据一个阈值提取出图像中的较亮区域,把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合,得到最终的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 using System; using UnityEngine; namespace Unity_Shaders_Learn { public class Bloom : PostEffectsBase { public Shader bloomShader; private Material _bloomMaterial = null; public Material BloomMaterial { get { _bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, _bloomMaterial); return _bloomMaterial; } } [Header("高斯模糊迭代次数"), Range(0, 4)] public int iterations = 3; [Header("模糊范围"), Range(0.2f, 3.0f)] public float blurSpread = 0.6f; [Header("缩放系数"), Range(1, 8)] public int downSample = 2; [Header("亮度阈值"), Range(0.0f, 4.0f)] public float luminanceThreshold = 0.6f; private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (BloomMaterial != null) { BloomMaterial.SetFloat("_LuminanceThreshold", luminanceThreshold); int rtW = src.width / downSample; int rtH = src.height / downSample; RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0); buffer0.filterMode = FilterMode.Bilinear; // part1:先提取亮度值高于阈值的部分到buffer0 Graphics.Blit(src, buffer0, BloomMaterial, 0); // part2:对buffer0进行高斯模糊 for (int i = 0; i < iterations; i++) { BloomMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread); RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); Graphics.Blit(buffer0, buffer1, BloomMaterial, 1); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); Graphics.Blit(buffer0, buffer1, BloomMaterial, 2); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; } // part3:混合原图像和高亮高斯模糊后的图像 BloomMaterial.SetTexture("_Bloom", buffer0); Graphics.Blit(src, dest, BloomMaterial, 3); RenderTexture.ReleaseTemporary(buffer0); } else { Graphics.Blit(src, dest); } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 Shader "Unity Shaders Learn/Chapter12/Bloom" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _Bloom ("Bloom (RGB)", 2D) = "white" {} _LuminanceThreshold ("Luminance Threshold", Float) = 0.5 _BlurSize ("Blur Size", Float) = 1.0 } SubShader { // No culling or depth Cull Off ZWrite Off ZTest Always CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_TexelSize; sampler2D _Bloom; float _LuminanceThreshold; float _BlurSize; struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; }; v2f vertExtractBright(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; return o; } fixed luminance(fixed4 color) { return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; } fixed4 fragExtractBright(v2f i) : SV_Target { fixed4 c = tex2D(_MainTex, i.uv); // 暗部为0,所以暗部部分颜色被舍弃了 fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0); return c * val; } struct v2fBloom { float4 pos : SV_POSITION; half4 uv : TEXCOORD0; }; v2fBloom vertBloom(appdata_img v) { v2fBloom o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord; o.uv.zw = v.texcoord; // 处理平台差异 #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0.0) o.uv.w = 1.0 - o.uv.w; #endif return o; } fixed4 fragBloom(v2fBloom i) : SV_Target { return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw); } ENDCG Pass { CGPROGRAM #pragma vertex vertExtractBright #pragma fragment fragExtractBright ENDCG } UsePass "Unity Shaders Learn/Chapter12/GaussianBlur/GAUSSIAN_BLUR_VERTICAL" UsePass "Unity Shaders Learn/Chapter12/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL" Pass { CGPROGRAM #pragma vertex vertBloom #pragma fragment fragBloom ENDCG } } Fallback Off }
运动模糊
运动模糊效果可以让物体运动看起来更加平滑。实现有多种方法:
累积缓存(accumulation buffer) 混合多张连续的图像。当物体快速移动产生多张图像后,我们取它们之间的平均值作为最后的运动模糊图像。这种暴力的方法对性能的消耗很大
速度缓存(velocity buffer) 存储各个像素当前的运动速度,然后利用该值来决定模糊的方向和大小
下面使用类似于累积缓存的方法实现,不同的是不需要在一帧中把场景渲染多次,但需要保存之前的渲染结果,不断地把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。这种方法性能比积累缓存好,但是效果较差。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 using System; using UnityEngine; namespace Unity_Shaders_Learn { public class MotionBlur : PostEffectsBase { public Shader motionBlurShader; private Material _motionBlurMaterial = null; public Material MotionBlurMaterial { get { _motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, _motionBlurMaterial); return _motionBlurMaterial; } } [Header("模糊系数"), Range(0.0f, 0.9f)] public float blurAmount = 0.5f; private RenderTexture _accumulationTexture; private void OnDisable() { DestroyImmediate(_accumulationTexture); } private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (MotionBlurMaterial != null) { // 检测RT是否有效 if (_accumulationTexture == null || _accumulationTexture.width != src.width || _accumulationTexture.height != src.height) { DestroyImmediate(_accumulationTexture); _accumulationTexture = new RenderTexture(src.width, src.height, 0); _accumulationTexture.hideFlags = HideFlags.HideAndDontSave; Graphics.Blit(src, _accumulationTexture); } // 表明需要进行一个渲染纹理的 恢复操作(发生在渲染到纹理而该纹理有没有被提前清空或销毁的情况下) // 是告诉Unity我们要恢复了,让Unity不要发出警告,但该方法已经过时了 _accumulationTexture.MarkRestoreExpected(); MotionBlurMaterial.SetFloat("_BlurAmount", 1.0f - blurAmount); Graphics.Blit(src, _accumulationTexture, MotionBlurMaterial); Graphics.Blit(_accumulationTexture, dest); } else { Graphics.Blit(src, dest); } } } }
MarkRestoreExpected: 表示预期将进行 RenderTexture 恢复操作。在移动图形仿真模式下,当执行 RenderTexture“恢复”操作时,Unity 会发出警告。如果在不先进行清除或丢弃 (DiscardContents) 的情况下渲染到纹理,就会执行恢复操作。对于许多移动 GPU 和多 GPU 系统来说,这是一项代价高昂的操作,应该予以避免。但是,如果渲染效果要求必须进行 RenderTexture 恢复,则您可以调用该函数来指示 Unity 恢复操作是预期行为,不要发出警告。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 Shader "Unity Shaders Learn/Chapter12/MotionBlur" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _BlurAmount ("Blur Amount", Float) = 1.0 } SubShader { // No culling or depth Cull Off ZWrite Off ZTest Always CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; fixed _BlurAmount; struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; }; v2f vert(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; return o; } // RGB通道片元着色器 // 对当前图像采样,将A通道设为_BlurAmount // 以便在后面混合时可以使用A通道 fixed4 fragRGB(v2f i) : SV_Target { return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount); } // A通道片元着色器 // 直接返回采样结果 // 为了维护A通道的值,不受混合使用的A通道值影响 half4 fragA(v2f i) : SV_Target { return tex2D(_MainTex, i.uv); } ENDCG Pass { Blend SrcAlpha OneMinusSrcAlpha ColorMask RGB CGPROGRAM #pragma vertex vert #pragma fragment fragRGB ENDCG } Pass { Blend One Zero ColorMask A CGPROGRAM #pragma vertex vert #pragma fragment fragA ENDCG } } Fallback Off }
扩展阅读
更多特效的实现:https://docs.unity.cn/550/Documentation/Manual/comp-ImageEffects.html
GPU Gems:https://developer.nvidia.com/gpugems/gpugems/part-iv-image-processing/chapter-27-framework-image-processing
卷积:https://www.bilibili.com/video/BV1VV411478E