Unity中使用后处理技术实现模型描边或自发光


前言

在3D游戏中描边或者说边缘发光、边缘是非常常见的技术,通常这种技术用来提醒玩家当前选中的目标、角色、建筑等
在unity中选中会高亮
在unity中选中模型会有边缘发光

基本原理

在Unity的组件脚本中,给我们提供了OnRenderImage回调函数(该函数只能在有摄影机组件的对象上使用),这个回调函数给了我们开发者实现各种全屏幕效果的可能。同时呢还提供了CommandBuffer类用于自定义渲染模型。结合这两种功能我们通过CommandBuffer使用纯色渲染出想要描边的物体到一个RenderTexture,通过自定义的Shader将其边缘模糊,然后将模糊的部分再次赋予纯色从而扩展出边缘,将边缘再次模糊平滑后与原来的纯色RenderTexture做插值抠出边缘部分,再将这部分与OnRenderImage()提供的原图像混合从实现描边效果

实现过程

编写纯色Shader

一个简单的将模型渲染成纯色的Shader

Shader "Outline/SingleColor"
{
    Properties
    {
        _outLineColor("OutLineColor",Color)= (0,0,0,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog
            #include "UnityCG.cginc"
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };
            float4 _MainTex_ST;
            float4 _outLineColor;
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _outLineColor;
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

编写实现各种图像处理效果的Shader

整个Shader分为5Pass,分别对应横向和纵向的模糊、图像插值、图像混合与边缘实化

横向纵向模糊Pass

//pass 0 横向模糊
		pass {
			CGPROGRAM
			#include"UnityCG.cginc"
			#pragma vertex vert_heng
			#pragma fragment frag
			//横向扩展
			v2f vert_heng(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				float2 uv = v.uv;
				o.uv[0] = uv;
				o.uv[1] = uv + float2(1, 0) * _MainTex_TexelSize.xy;
				o.uv[2] = uv + float2(-1, 0) * _MainTex_TexelSize.xy;
				o.uv[3] = uv + float2(2, 0) * _MainTex_TexelSize.xy;
				o.uv[4] = uv + float2(-2, 0) * _MainTex_TexelSize.xy;
				return o;
			}
			ENDCG
		}
		//pass 1 竖直模糊
		pass {
			CGPROGRAM
			#include"UnityCG.cginc"
			#pragma vertex vert_shu
			#pragma fragment frag
			v2f vert_shu(a2v v) {//竖直扩展
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				float2 uv = v.uv;
				o.uv[0] = uv;
				o.uv[1] = uv + float2(0, 1) * _MainTex_TexelSize.xy;
				o.uv[2] = uv + float2(0, -1) * _MainTex_TexelSize.xy;
				o.uv[3] = uv + float2(0, 2) * _MainTex_TexelSize.xy;
				o.uv[4] = uv + float2(0, -2) * _MainTex_TexelSize.xy;
				return o;
			}
			ENDCG
		}

公用的片源着色器与结构体

	CGINCLUDE
	sampler2D _MainTex;
	float4 _MainTex_TexelSize;

	struct a2v {
		float4 vertex:POSITION;
		float2 uv:TEXCOORD0;
	};
	struct v2f {
		float4 pos:SV_POSITION;
		float2 uv[5]:TEXCOORD0;
	};

	fixed4 frag(v2f i) :SV_TARGET{
		float3 col = tex2D(_MainTex,i.uv[0]).xyz * 0.4026;
		float3 col1 = tex2D(_MainTex,i.uv[1]).xyz * 0.2442;
		float3 col2 = tex2D(_MainTex,i.uv[2]).xyz * 0.2442;
		float3 col3 = tex2D(_MainTex,i.uv[3]).xyz * 0.0545;
		float3 col4 = tex2D(_MainTex,i.uv[4]).xyz * 0.0545;
		float3 finalCol = col + col1 + col2 + col3 + col4;
		return fixed4(finalCol,1.0);
	}
	ENDCG

图像差值Pass

该Pass将没有模糊的图像和模糊后的图像相减从而获得物体的轮廓图像

	//pass 2 将没有模糊的图像和模糊后的图像相减获得轮廓图像
		Pass{
			CGPROGRAM
			#include "UnityCG.cginc"
			#pragma vertex vert
			#pragma fragment frag
			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};
			struct v2f1
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};
			v2f1 vert(appdata v)
			{
				v2f1 o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = v.uv;
				return o;
			}
			sampler2D _RenderTex;
			fixed4 frag(v2f1 i) : SV_Target
			{
				float3 col = tex2D(_MainTex,i.uv).xyz;
				float3 commandCol = tex2D(_RenderTex,i.uv).xyz;
				float3 finalCol = col - commandCol;
				return fixed4(finalCol,1.0);
			}
			ENDCG
		}

图像混合Pss

这个Pass将物体的轮廓图像与场景图像混合获得最终图像

		//pass 3 将正常图像和轮廓图像混合到一起
		Pass{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

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

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

			v2f2 vert(appdata v){
				v2f2 o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = v.uv;
				return o;
			}
			sampler2D _OutLineTex;
			fixed4 _OutlineColor;				
			fixed4 frag(v2f2 i) : SV_Target{
				fixed4 col = tex2D(_MainTex, i.uv);
				fixed4 lineCol = tex2D(_OutLineTex,i.uv);
				float a=(lineCol.r+lineCol.g+lineCol.b)/3;
				col.rgb = col.rgb*(1-a)+_OutlineColor*a;
				return col;
			}
			ENDCG
		}

边缘实化Pass

这个Pass将第一阶段的模糊的边缘变成纯色从而扩展出边缘

//pass 4 边缘实体化
		Pass{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

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

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

			v2f4 vert(appdata v){
				v2f4 o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = v.uv;
				return o;
			}	
						
			fixed4 frag(v2f4 i) : SV_Target{
				fixed4 whiteCol=(1,1,1,1);
				fixed4 col = tex2D(_MainTex, i.uv);
				if(col.r>0 ||col.g>0 || col.b>0){
					col.rgb = whiteCol;
				}
				return col;
			}
			ENDCG
		}

脚本实现

主要的绘制相关的工作都是在OnRenderImage函数中实现的
首先我们用纯色Shader绘制出需要描边物体
绘制出纯色物体
然后将其边缘模糊
整体模糊
再将边缘部分实体化
边缘部分实体化

一次这样的操作看不什么效果,我们多迭代几次,就变成了下面的效果。这是迭代了5次的效果,可以明显的看到比原物体大了一圈
迭代5次次效果
然后再对边缘进行模糊使其变得更加柔和,下面是模糊迭代5次的效果
模糊后图篇
将原图与边缘扩展好的图做差值计算就可以得到轮廓图了
边缘轮廓图
将轮廓图的RGB值当作alpha值,用自定义的颜色填充全图。再将这个图片与场景图片做混合就能的到最终的描边效果了
最终效果
Unity脚本OutlineSystem.cs(该脚本必须挂在到Camera下)代码如下

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteInEditMode]
public class OutlineSystem : MonoBehaviour
{
    private List<GameObject> gameObjects = new List<GameObject>();
    private Material effectMat = null;
    private Material renderMat = null;
    private CommandBuffer commandBuffer = null;
    private RenderTexture renderTex = null;
    private Renderer targetEBO;
    private int meshcount = 0;
    public Color outLineColor = Color.green;      //renderMat
    [Range(0, 10)]
    public int outLineSize = 1;
    [Range(0, 50)]
    public int BlurSize = 5;
    void Start()
    {
        commandBuffer = new CommandBuffer();
        renderTex = RenderTexture.GetTemporary(Screen.width, Screen.height, 0);
        commandBuffer.SetRenderTarget(renderTex);
        commandBuffer.ClearRenderTarget(true, true, Color.black);
        //创建材质
        effectMat = new Material(Shader.Find("Outline/OutlintEffect"));
        renderMat= new Material(Shader.Find("Outline/SingleColor"));
    }
    public void AddOutline(GameObject gameObject)
    {
        if (gameObjects.IndexOf(gameObject) != -1)
            return;
        gameObjects.Add(gameObject);
        targetEBO = gameObject.GetComponent<Renderer>();
        if (targetEBO is MeshRenderer)
        {
            var f = targetEBO.GetComponent<MeshFilter>();
            if (f != null && f.sharedMesh != null)
                meshcount = f.sharedMesh.subMeshCount;
        }
        for (int i = 0; i < meshcount; i++)
        {
            commandBuffer.DrawRenderer(targetEBO, renderMat, i, 0);
        }
    }
    public bool IsOutline(GameObject gameObject)
    {
        if (gameObjects.IndexOf(gameObject) != -1)
            return true;
        else
            return false;
    }
    public void CancelOutline(GameObject gameObject) {
        this.gameObjects.Remove(gameObject);
        Outline();
    }
    /// <summary>
    /// 取消所有的描边效果
    /// </summary>
    public void CancelOutlineAll() {
        commandBuffer.Clear();
        commandBuffer.SetRenderTarget(renderTex);
        commandBuffer.ClearRenderTarget(true, true, Color.black);
        this.gameObjects.Clear();
    }
    /// <summary>
    /// 重新计算描边命令缓冲区
    /// </summary>
    private void Outline()
    {
        commandBuffer.Clear();
        commandBuffer.SetRenderTarget(renderTex);
        commandBuffer.ClearRenderTarget(true, true, Color.black);
        foreach (GameObject gobj in gameObjects)
        {
            targetEBO = gobj.GetComponent<Renderer>();
            if (targetEBO is MeshRenderer)
            {
                var f = targetEBO.GetComponent<MeshFilter>();
                if (f != null && f.sharedMesh != null)
                    meshcount = f.sharedMesh.subMeshCount;
            }
            for (int i = 0; i < meshcount; i++)
            {
                commandBuffer.DrawRenderer(targetEBO, renderMat, i, 0);
            }
        }
    }
    private void OnEnable()
    {
        if (renderTex)
        {
            RenderTexture.ReleaseTemporary(renderTex);
            renderTex = null;
        }
        if (commandBuffer != null)
        {
            commandBuffer.Release();
            commandBuffer = null;
        }
    }
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (targetEBO == null) {
            Graphics.Blit(src, dest);
            return;
        }
        if (!targetEBO.gameObject.activeSelf) {
            Graphics.Blit(src, dest);
            return;
        }
        if (renderMat && renderTex && commandBuffer != null )
        {
            //用白色绘制出图像
            renderMat.SetColor("_outLineColor", Color.white);
            Graphics.ExecuteCommandBuffer(commandBuffer);
            //声明用来模糊的RT
            RenderTexture temp1 = RenderTexture.GetTemporary(src.width, src.width, 0);
            RenderTexture temp2 = RenderTexture.GetTemporary(src.width, src.width, 0);
            //先进行一次模糊,因为无法直接用循环叠加commandBuffer
            Graphics.Blit(renderTex, temp1, effectMat, 0);
            Graphics.Blit(temp1, temp2, effectMat, 1);
            //边缘实体强化 
            Graphics.Blit(temp2, temp1, effectMat, 4);
            Graphics.Blit(temp1, temp2, effectMat, 4);
            //执行多次边缘扩展
            for (int i = 0; i < outLineSize; i++)
            {
                Graphics.Blit(temp2, temp1, effectMat, 0);
                Graphics.Blit(temp1, temp2, effectMat, 1);

                Graphics.Blit(temp2, temp1, effectMat, 4);
                Graphics.Blit(temp1, temp2, effectMat, 4);
            }
            //执行多次模糊
            for (int i = 0; i < BlurSize; i++)
            {
                Graphics.Blit(temp2, temp1, effectMat, 0);
                Graphics.Blit(temp1, temp2, effectMat, 1);
            }
            //将模糊后的图片减去commandBuffer中的实心剪影
            effectMat.SetTexture("_RenderTex", renderTex);
            Graphics.Blit(temp2, temp1, effectMat, 2);
            //后期处理,叠入渲染成果
            effectMat.SetTexture("_OutLineTex", temp1);
            effectMat.SetColor("_OutlineColor", outLineColor);
            Graphics.Blit(src, dest, effectMat, 3);
            //释放RT
            RenderTexture.ReleaseTemporary(temp1);
            RenderTexture.ReleaseTemporary(temp2);
            return;
        }    
    }
}

总结

本文简单的介绍一种基于后处理技术的模型描边方法,该方法的优点在于可以对任何大小、形状的模型进行比较完美的描边。
相比于法向量扩展的描边方法,该方法不会出现边缘断裂的情况更加的美观。
当然,目前还是有些缺点,在要求实现多物体不同颜色描边时候需要用到多个CommandBuffer和多重的纹理叠加,性能会有显著的消耗。

项目代码

码云链接:https://gitee.com/mo_ni/outline-demo

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值