1.command buffer具有很高的灵活性,它的作用是预定义一些渲染指令,然后在我们想要执行的时候去执行这些指令(见图1),绿点表示可以在“Forward Rendering Path”和“Deferred Path”中执行这些命令的阶段。
查看Unity文档,我们有以下事件可以在其中添加CommandBuffer的指令。
https://docs.unity3d.com/ScriptReference/Rendering.CameraEvent.html

图1.1 渲染管线
通常,在实现某些屏幕后处理时(比如Bloom效果、屏幕灰化、物体的外轮廓等),场景的所有物体都会受到处理,用command buffer的话,就能选择性地处理物体的效果。command buffer还有一个重要的功能,就是它能代替GrabPass{},去截取在它身后的图像,然后去实现水面的扰动、火焰的热空气扰动、玻璃的伪折射等的效果,在移动端,它所消耗的性能比GrabPass{}的低很多。
2. 用command buffer实现物体发光轮廓
为了减少代码的重复书写,首先我们将创建一个Common.cginc文件,以便各个shader都能调用。
我们还需要一些GaussianBlur以达到我们要处理的效果。关于高斯模糊可以看这一点。 Gaussian Kernel Calculator.
现在,给出原图的图片。
图2.1
在初始状态下,我们想要中间的正方体是一个被选中的状态,粗略分解一下该操作步骤
1) 我们使用CommandBuffer中的DrawRenderer方法,将要实现效果的物体绘制出来,该(见图2.2)。
图2.2
2) 对于先前获得的纹理,我们将使用高斯模糊对其进行处理。 (见图2.3)。
图2.3
3) 用模糊过的纹理减去原始的纹理,乘以颜色和亮度,就可以得到发光外轮廓。(见图2.4)
原始图像的正方体是一个白色物体,颜色值为1,而模糊过的图像的颜色值是小于1的,模糊图像减去原始图片的话是<0的,所以中间部分显示的是黑色。因为高斯模糊是一个加权平均操作,每个像素的颜色值都是由其本身和相邻像素的颜色值进行加权平均得到,越靠近像素本身,权值越高,越偏离像素,权值越低。所以,正方体周围的像素也进行加权平均操作,得到淡一点的颜色。
图2.4
为此,我们将首先创建负责处理所需效果GlowOutlineEffect.cs的类(此脚本附加到主相机),还包括要渲染的每个纹理所必需的着色器。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; namespace FI { public class GlowOutlineEffect : MonoBehaviour { #region ENUMS private enum Pass { PrePass, BlurredPass, FinalPass, Result } [SerializeField] private Pass passToShow; #endregion #region PRIVATE_VARS [SerializeField, Range(0, 3)] private int iterations; [SerializeField, Range(0, 10)] private float intensity; [SerializeField] private Color glowColor; [SerializeField] private Renderer[] toRender; private Material unlitMat; private Material gaussianBlurMat; private Material finalPassMat; private Material glowOutlineMat; private Material showPrePassMat; private Material showBlurredMat; private Material showFinalPassMat; private CommandBuffer buffer; private int prePassTexID; private int blurredTexID; private int finalTexID; private int tempText1ID; #endregion #region UNITY_API void Start() { //初始化 InitializeBuffer(); InitializeMaterials(); InitializeProperties(); } // Update is called once per frame void Update() { UpdateBuffer(); UpdateGlowProperties(); MouseButtonInput(); } #endregion #region PRIVATE_METHODS private void MouseButtonInput() { if(Input.GetMouseButtonDown(0)) { RaycastHit hit; var ray = Camera.main.ScreenPointToRay(Input.mousePosition); if(Physics.Raycast(ray,out hit)) { if(hit.transform) { toRender[0] = hit.transform.GetComponentInChildren<Renderer>(); } } } } private void InitializeBuffer() { buffer = new CommandBuffer(); Camera.main.AddCommandBuffer(CameraEvent.BeforeImageEffects, buffer); } private void InitializeMaterials() { unlitMat = new Material(Shader.Find("Unlit/Color")); gaussianBlurMat = new Material(Shader.Find("FI/GaussianBlur")); finalPassMat = new Material(Shader.Find("FI/FinalPass")); glowOutlineMat = new Material(Shader.Find("FI/GlowOutline")); showPrePassMat = new Material(Shader.Find("FI/ShowPrePass")); showBlurredMat = new Material(Shader.Find("FI/ShowBlurred")); showFinalPassMat = new Material(Shader.Find("FI/ShowFinalPass")); } private void InitializeProperties() { prePassTexID = Shader.PropertyToID("_PrePassTex"); blurredTexID = Shader.PropertyToID("_BlurredTex"); tempText1ID = Shader.PropertyToID("_TempTex1"); finalTexID = Shader.PropertyToID("_FinalTex"); } private void UpdateGlowProperties() { Shader.SetGlobalFloat("_Intensity", intensity); Shader.SetGlobalColor("_GlowColor", glowColor); } private void UpdateBuffer() { buffer.Clear(); UpdatePrePassTexture(); UpdateBlurredTexture(); UpdateFinalTexture(); } private void UpdatePrePassTexture() { //创建一个临时纹理,prePassTexID是纹理标识, //第二和第三个参数为-1,表示该纹理的大小跟屏幕大小一样大。 //添加双线性过滤和抗锯齿的,以确保基本纹理尽可能清晰 buffer.GetTemporaryRT(prePassTexID, -1, -1, 0, FilterMode.Bilinear, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default, QualitySettings.antiAliasing); buffer.SetRenderTarget(prePassTexID); buffer.ClearRenderTarget(true, true, Color.clear); for (int i = 0; i < toRender.Length; i++) { if (toRender[i].gameObject.activeSelf) { buffer.DrawRenderer(toRender[i], unlitMat); } } } private void UpdateBlurredTexture() { /*创建临时纹理,第二和第三个参数为-2,代表该纹理的大小为屏幕大小的1/2 * 这样可以降低纹理的分辨率以获得更模糊的结果,并降低成本 */ buffer.GetTemporaryRT(blurredTexID, -2, -2, 0, FilterMode.Bilinear); buffer.GetTemporaryRT(tempText1ID, -2, -2, 0, FilterMode.Bilinear); //prePassTexID的纹理经过gaussianBlurMat材质的处理后,复制给blurredTexID buffer.Blit(prePassTexID, blurredTexID, gaussianBlurMat); //循环迭代,迭代次数越多越模糊 for (int i = 0; i < iterations; i++) { //最后一个参数代表执行材质Shader里的哪一个Pass //0代表执行该Shader的第一个Pass,即在水平方向进行模糊 //1代表执行该Shader的第二个Pass,即在垂直方向上进行模糊 buffer.Blit(blurredTexID, tempText1ID, gaussianBlurMat,0); buffer.Blit(tempText1ID, blurredTexID, gaussianBlurMat,1); } } private void UpdateFinalTexture() { buffer.GetTemporaryRT(finalTexID, -1, -1, 0, FilterMode.Bilinear); buffer.Blit(null, finalTexID, finalPassMat); } /// <summary> /// 在所有的对象都渲染完后执行的方法,source是全屏幕的初始图像, /// Blit()函数把source传给材质处理,在材质的Shader中用_MainTex /// 去获取source的图像。 /// </summary> /// <param name="source"></param> /// <param name="destination"></param> private void OnRenderImage(RenderTexture source, RenderTexture destination) { switch (passToShow) { case Pass.PrePass: Graphics.Blit(null, destination, showPrePassMat); break; case Pass.BlurredPass: Graphics.Blit(null, destination, showBlurredMat); break; case Pass.FinalPass: Graphics.Blit(null, destination, showFinalPassMat); break; default: Graphics.Blit(source, destination, glowOutlineMat); break; } } #endregion } }
有了外发光轮廓的纹理后,剩下的就是将其添加到屏幕上渲染的纹理。
Shader "FI/GlowOutline" { Properties { _MainTex("Texture", 2D) = "white" {} } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Common.cginc" sampler2D _FinalTex; fixed4 frag(v2f i) : SV_Target { return tex2D(_MainTex, i.uv) + tex2D(_FinalTex, i.uv); } ENDCG } } }
浙公网安备 33010602011771号