当前位置:   article > 正文

Unity中使用后处理技术实现模型描边或自发光_unity 边缘自定角度高光,描边

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
        }
    }
}

  • 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

编写实现各种图像处理效果的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
		}
  • 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

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

	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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

图像差值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
		}
  • 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

图像混合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
		}
  • 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

边缘实化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
		}
  • 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

脚本实现

主要的绘制相关的工作都是在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;
        }    
    }
}
  • 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
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155

总结

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

项目代码

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/110137
推荐阅读
相关标签
  

闽ICP备14008679号