C for Graphic:CommandBuffer外描边

      这一篇聊一聊最近一个功能需要使用CommandBuffer,还是工作上碰到的问题,原始需求很简单:就是用户操作人物角色在场景中四处寻找物体,在靠近需要拾取的物体,则给物体一个外发光描边,或者用户不知道该怎么做了,给予当前流程的拾取物体一个外发光描边的提示效果。

      这一类功能其实很常用,而且也不难,我们以前也学过,可以返回去看。无非就是给物体一个基于顶点法向量dot视口向量的阈值做边缘光,或者基于法向量膨胀模型做前向裁剪渲染,渲染出描边效果。

     当然我们还有一种方法就是深度偏移,如下:

Shader "CmdBuffer/CmdDepthShader"
{
    Properties
    {
        _MainColor("Main Color",Color) = (1,1,1,1)
        _Factor("Offset Factor",Range(-50,0)) = -5
        _Unit("Offset Unit",Range(-20,20)) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Offset [_Factor],[_Unit]
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            float4 _MainColor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _MainColor;
                return col;
            }
            ENDCG
        }
    }
}

      c#代码就是注册commandbuffer渲染队列:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CbufferObject : MonoBehaviour
{
    public Material material;

    private MeshRenderer render;
    private CommandBuffer cmdBuffer;

    private void Awake()
    {
        render = GetComponent<MeshRenderer>();
    }

    void Start()
    {
        
    }

    private void OnEnable()
    {
        cmdBuffer = new CommandBuffer();
        cmdBuffer.DrawRenderer(render, material);
        Camera.main.AddCommandBuffer(CameraEvent.AfterEverything, cmdBuffer);
    }

    private void OnDisable()
    {
        if (Camera.main != null)
        {
            Camera.main.RemoveCommandBuffer(CameraEvent.AfterEverything, cmdBuffer);
        }
    }
}

       效果如下:

 

      这里解释一下Offset命令的意义:unity cull&depth

Offset

Offset Factor, Units

Allows you specify a depth offset with two parameters. factor and unitsFactor scales the maximum Z slope, with respect to X or Y of the polygon, and units scale the minimum resolvable depth buffer value. This allows you to force one polygon to be drawn on top of another although they are actually in the same position. For example Offset 0, -1 pulls the polygon closer to the camera
 ignoring the polygon’s slope, whereas Offset -1, -1 will pull the polygon even closer when looking at a grazing angle.

       我能找到资料就是:

       Offset = K*Factor+U*Unit;

       其中K是顶点深度的斜率,这个斜率的计算与视锥体近裁剪面相关,越与近裁剪面平行,斜率趋近0。

扫描二维码关注公众号,回复: 13213008 查看本文章

       U代表渲染管线能区分深度差异的最小值。

       而视口near面到far面取值范围时[-1,1]

       那么Factor为负数越大时,Offset越靠近near面

       所以表现形式为:一个sphereA派生出一个更靠近near面的CullFront的sphereB,则B遮住A的头部Cull掉,留下一圈绿色的描边,同时并不扩大屏幕上sphere的体积。

       回到正题,我们场景追求运行效率,用了简模型、烘培、剔除等常用操作,特别是美术模型的简化和制作规范问题,我这边的问题如下:

       1.模型顶点,比如一张纸片就是一个quad,四个顶点,而且法向量朝上,所以扩展法向量描边无效

      2.模型中心点不在质心(几何中心),那么缩放模型描边无效,缩放有问题

      3.模型不是闭合多边形,那么深度偏移无效,因为会三角面会重合穿插

      这时候我们只能考虑使用rendertexture做描边效果了,我们既然在forwardbase下无法统一描边的问题,那么我们可以考虑“提取”gameobject的渲染targettexture,然后在二维图片的基础上做外描边。

      我们把步骤分解一下:

      1.首先要使用commandbuffer“提取”gameobject渲染的rendertexture(obejctRT),同时commandbuffer绑定渲染材质为纯色(描边色)输出。

      2.对objectRT进行高斯模糊,因为我们想要在二维图片上描边,得通过高斯模糊把gameobject的“原始大小”扩展一下。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CmdBufferOutlineCamera : MonoBehaviour
{
    public GameObject cmdObject;
    public Material objectMat;              //纯绿色的着色
    public Material gaussMat;               //高斯模糊
    private MeshRenderer render;
    private RenderTexture objectRT = null;   //object渲染出的rt
    private CommandBuffer cmdBuffer = null;

    [Range(1, 20)]
    public int downSample = 1;

    private int wid;
    private int hei;

    private void Awake()
    {
        wid = Screen.width / downSample;
        hei = Screen.height / downSample;
        render = cmdObject.GetComponent<MeshRenderer>();
    }

    private void Start()
    {

    }

    private void OnEnable()
    {
        //渲染出一个纯绿色的objectRT纹理
        objectRT = RenderTexture.GetTemporary(wid, hei, 0, RenderTextureFormat.ARGB32);
        cmdBuffer = new CommandBuffer();
        cmdBuffer.SetRenderTarget(objectRT);
        cmdBuffer.ClearRenderTarget(true, true, Color.black);
        cmdBuffer.DrawRenderer(render, objectMat);
        Camera.main.AddCommandBuffer(CameraEvent.AfterImageEffects, cmdBuffer);
    }

    private void OnDisable()
    {
        RenderTexture.ReleaseTemporary(objectRT);
        objectRT = null;

        Camera.main.RemoveCommandBuffer(CameraEvent.AfterImageEffects, cmdBuffer);
        cmdBuffer.Clear();
        cmdBuffer = null;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (cmdBuffer != null)
        {
            Graphics.ExecuteCommandBuffer(cmdBuffer);
        }
        //将objectRT绑定高斯渲染输出到dest查看
        Graphics.Blit(objectRT, destination, gaussMat);
    }
}

       纯绿色渲染shader:

Shader "CmdBuffer/CmdObjectOutlineShader"
{
    Properties
    {
        _MainColor("Main Color",Color) = (0,1,0,1)
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 100

        Pass
        { 
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            float4 _MainColor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _MainColor;
                return col;
            }
            ENDCG
        }
    }
}

       高斯模糊shader,以前也讲过,网上也很多,不明白可以返回以前查看:

Shader "CmdBuffer/CmdScreenOutlineShader"
{
    Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_GaussSpread("Gauss Spread",Range(0,20)) = 1
        _GaussLumin("Gauss Luminus",Range(0,2)) = 1
	}
		SubShader
	{
		Tags { "RenderType" = "Opaque" }
		LOD 100
 
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
 
			#include "UnityCG.cginc"
 
			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};
 
			struct v2f
			{
				float2 uv[9] : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};
 
			sampler2D _MainTex;
			float4 _MainTex_ST;
			float4 _MainTex_TexelSize;
			int _GaussSpread;	
            float _GaussLumin;					
 
			static float _GaussMatrix[9] = {
				0.05854983,
				0.09653235,
				0.05854983,
				0.09653235,
				0.1591549,
				0.09653235,
				0.05854983,
				0.09653235,
				0.05854983
			};
 
			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				float2 uv = TRANSFORM_TEX(v.uv, _MainTex);
 
				int c = 1;
				for (int x = 0; x < 3; x++)
				{
					for (int y = 0; y < 3; y++)
					{
						o.uv[x * 3 + y] = uv + _MainTex_TexelSize.xy*float2((y - c)*_GaussSpread, (c - x)*_GaussSpread); 
					}
				}
				return o;
			}
 
			fixed4 frag(v2f i) : SV_Target
			{
				fixed4 col = fixed4(0,0,0,0);
				for (int k = 0; k < 9; k++)
				{
					col += tex2D(_MainTex, i.uv[k])*_GaussMatrix[k]*_GaussLumin;
				}
				return col;
			}
			ENDCG
		}
	}
}

       解释一下代码,cmdBuffer通过objectRT把sphere渲染到二维,然后通过OnRenderImage将Gauss Shader处理后的objectRT绑定到destination上,运行效果如下:

 

       以上得到一个纯绿色带高斯像素扩展的的objectRT纹理。

       接下来要处理的就是“叠加”问题,就是怎么把除了“描边”以外的像素剔除掉,如下:

 

       只保留红圈内部部分,其实也很简单啦,我们再搞一张没有经过高斯模糊的objectRT(B),用已经高斯模糊过的objectRT(A)去做像素RGBA减法就可以啦,如下:

       首先写一个纹理像素减法的shader:

Shader "CmdBuffer/CmdColorMinusShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _GaussTex("Gauss Tex",2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            sampler2D _GaussTex;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 minusColor(fixed4 fcol,fixed4 tcol)
            {
                return fcol - tcol;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 mcol = tex2D(_MainTex, i.uv);
                fixed4 gcol = tex2D(_GaussTex,i.uv);
                fixed4 col = minusColor(gcol,mcol);
                return col;
            }
            ENDCG
        }
    }
}

        然后在OnImageRender中“提取”纹理做减法渲染:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CmdBufferOutlineCamera : MonoBehaviour
{
    public GameObject cmdObject;
    public Material objectMat;              //纯绿色的着色
    public Material gaussMat;               //高斯模糊
    public Material minusMat;               //减法shader
    private MeshRenderer render;
    private RenderTexture objectRT = null;   //object渲染出的rt
    private CommandBuffer cmdBuffer = null;

    [Range(1, 20)]
    public int downSample = 1;

    private int wid;
    private int hei;

    private void Awake()
    {
        wid = Screen.width / downSample;
        hei = Screen.height / downSample;
        render = cmdObject.GetComponent<MeshRenderer>();
    }

    private void Start()
    {

    }

    private void OnEnable()
    {
        //渲染出一个纯绿色的objectRT纹理
        objectRT = RenderTexture.GetTemporary(wid, hei, 0, RenderTextureFormat.ARGB32);
        cmdBuffer = new CommandBuffer();
        cmdBuffer.SetRenderTarget(objectRT);
        cmdBuffer.ClearRenderTarget(true, true, Color.black);
        cmdBuffer.DrawRenderer(render, objectMat);
        Camera.main.AddCommandBuffer(CameraEvent.AfterImageEffects, cmdBuffer);
    }

    private void OnDisable()
    {
        RenderTexture.ReleaseTemporary(objectRT);
        objectRT = null;

        Camera.main.RemoveCommandBuffer(CameraEvent.AfterImageEffects, cmdBuffer);
        cmdBuffer.Clear();
        cmdBuffer = null;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (cmdBuffer != null)
        {
            Graphics.ExecuteCommandBuffer(cmdBuffer);
        }
        //首先“提取”高斯模糊纹理
        RenderTexture gRT = RenderTexture.GetTemporary(wid, hei, 0, RenderTextureFormat.ARGB32);
        Graphics.Blit(objectRT, gRT, gaussMat);

        //再“提取”减法纹理
        minusMat.SetTexture("_MainTex", objectRT);
        minusMat.SetTexture("_GaussTex", gRT);

        RenderTexture mRT = RenderTexture.GetTemporary(wid, hei, 0, RenderTextureFormat.ARGB32);
        Graphics.Blit(null, mRT, minusMat);

        //绑定dest查看
        Graphics.Blit(mRT, destination);

        RenderTexture.ReleaseTemporary(gRT);
        RenderTexture.ReleaseTemporary(mRT);
    }
}

        效果如下:

 

 

        用这两个纹理做减法(gaussRT-objectRT)就得到描边了,因为我为了性能做了降采样,所以边缘毛糙。

        最后,我们得到描边后,再“叠加”原本的source主纹理就好了,我们改造一下shader:

Shader "CmdBuffer/CmdColorMinusShader"
{
    Properties
    {
        _SrcTex("Source Tex",2D) = "white" {}
        _ObjTex ("Object Tex", 2D) = "white" {}
        _GaussTex("Gauss Tex",2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _SrcTex;
            float4 _SrcTex_ST;

            sampler2D _ObjTex;
            sampler2D _GaussTex;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _SrcTex);
                return o;
            }

            fixed4 overlayColor(fixed4 scol,fixed4 ocol,fixed4 gcol)
            {
                return scol + saturate(gcol-ocol);
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 scol = tex2D(_SrcTex, i.uv);
                fixed4 ocol = tex2D(_ObjTex,i.uv);
                fixed4 gcol = tex2D(_GaussTex,i.uv);
                fixed4 col = overlayColor(scol,ocol,gcol);
                return col;
            }
            ENDCG
        }
    }
}

       OnImageRender也改造一下:

private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (cmdBuffer != null)
        {
            Graphics.ExecuteCommandBuffer(cmdBuffer);
        }
        //首先“提取”高斯模糊纹理
        RenderTexture gRT = RenderTexture.GetTemporary(wid, hei, 0, RenderTextureFormat.ARGB32);
        Graphics.Blit(objectRT, gRT, gaussMat);

        //再“提取”混合纹理
        minusMat.SetTexture("_SrcTex", source);
        minusMat.SetTexture("_ObjTex", objectRT);
        minusMat.SetTexture("_GaussTex", gRT);

        Graphics.Blit(null, destination, minusMat);

        RenderTexture.ReleaseTemporary(gRT);
    }

        效果如下:

 

       可以看得出来因为降采样导致毛糙,我们再改一改就行了:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CmdBufferOutlineCamera : MonoBehaviour
{
    public GameObject cmdObject;
    public Material objectMat;              //纯绿色的着色
    public Material gaussMat;               //高斯模糊
    public Material minusMat;               //减法shader
    private MeshRenderer render;
    private RenderTexture objectRT = null;   //object渲染出的rt
    private CommandBuffer cmdBuffer = null;

    [Range(1, 20)]
    public int downSample = 1;

    private int wid;
    private int hei;

    private void Awake()
    {
        wid = Screen.width / downSample;
        hei = Screen.height / downSample;
        render = cmdObject.GetComponent<MeshRenderer>();
    }

    private void Start()
    {

    }

    private void OnEnable()
    {
        //渲染出一个纯绿色的objectRT纹理
        objectRT = RenderTexture.GetTemporary(Screen.width, Screen.height, 0, RenderTextureFormat.ARGB32);
        cmdBuffer = new CommandBuffer();
        cmdBuffer.SetRenderTarget(objectRT);
        cmdBuffer.ClearRenderTarget(true, true, Color.black);
        cmdBuffer.DrawRenderer(render, objectMat);
        Camera.main.AddCommandBuffer(CameraEvent.AfterImageEffects, cmdBuffer);
    }

    private void OnDisable()
    {
        RenderTexture.ReleaseTemporary(objectRT);
        objectRT = null;

        Camera.main.RemoveCommandBuffer(CameraEvent.AfterImageEffects, cmdBuffer);
        cmdBuffer.Clear();
        cmdBuffer = null;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (cmdBuffer != null)
        {
            Graphics.ExecuteCommandBuffer(cmdBuffer);
        }

        //降采样objectRT
        RenderTexture dRT = RenderTexture.GetTemporary(wid, hei, 0, RenderTextureFormat.ARGB32);
        Graphics.Blit(objectRT, dRT);

        //首先“提取”高斯模糊纹理
        RenderTexture gRT = RenderTexture.GetTemporary(wid, hei, 0, RenderTextureFormat.ARGB32);
        Graphics.Blit(dRT, gRT, gaussMat);

        //再“提取”混合纹理
        minusMat.SetTexture("_SrcTex", source);
        minusMat.SetTexture("_ObjTex", objectRT);
        minusMat.SetTexture("_GaussTex", gRT);

        Graphics.Blit(null, destination, minusMat);

        RenderTexture.ReleaseTemporary(gRT);
        RenderTexture.ReleaseTemporary(dRT);
    }
}

       代码也简单,就是不降低objectRT的采样,只降低gaussRT的采样就行了,效果就正常了:

     

      so,因为我只是写博客讲解原理,童鞋们正式整合到工程里需要改成插件形式,我就不发我写好的源码了,毕竟公司项目。还有就是gauss采样可以多重采样(虽然增加性能消耗),可以提高描边的效果。

    

猜你喜欢

转载自blog.csdn.net/yinhun2012/article/details/106630219