【Unity3D】選中物體描邊特效

1 前言

描邊的難點在于如何檢測和識別邊緣,當前實現(xiàn)描邊特效的方法主要有以下幾種:

1)基于頂點膨脹的描邊方法

在 SubShader 中開 2 個 Pass 渲染通道,第一個 Pass 通道渲染膨脹的頂點,即將頂點坐標沿著法線方向向外擴展,并使用純色給擴展后的頂點著色,第二個 Pass 通道渲染原頂點,并覆蓋第一個 Pass 通道渲染的內(nèi)部。

該方案實現(xiàn)簡單,算法效率高,但是對于拐角較大的兩個面交界處(法線突變較大處),會出現(xiàn)描邊斷裂,并且描邊的寬度會受到透視投影影響。

基于模板測試和頂點膨脹的描邊方法 解決了描邊斷裂和描邊寬度受透視影響問題。

2)基于法線和視線的描邊方法

對于物體的任意一頂點,判斷視線向量(該點指向相機的向量)與該點法線向量的夾角(記為 θ)是否近似 90 度,如果近似 90 度,就將該點識別為邊緣,并進行邊緣著色。實際應用中,通常采用模糊描邊著色法,而不是閾值法,即將 sin(θ) 作為該點渲染描邊色的強度。

該方案屬于內(nèi)描邊,實現(xiàn)簡單,算法效率高,但是物體中間 θ 值近似 90 度的地方(凹陷處)也會被描邊。

3)基于屏幕紋理的描邊方法

對渲染后的屏幕紋理進行二次渲染,根據(jù)像素點周圍顏色的差異判斷是否是物體邊緣像素,如果是邊緣,需要重新進行邊緣著色。判斷邊緣的具體做法是:對該像素點周圍的像素點的亮度進行卷積運算,得到該點的梯度(反映該點附近亮度突變的強度),根據(jù)梯度閾值判斷該點是否是邊緣。

該方案屬于內(nèi)描邊,實現(xiàn)有一定難度,算法效率一般,算法依賴梯度閾值,并且受顏色、光照、陰影等影響較大如:地面的影子可能會被描邊。

案例見→邊緣檢測特效

4)基于深度和法線紋理的描邊方法

對渲染后的屏幕紋理進行二次渲染,根據(jù)像素點周圍深度和法線的差異判斷是否是物體邊緣像素,如果是邊緣,需要重新進行邊緣著色。判斷邊緣的具體做法是:對該像素點周圍的像素點的深度和法線進行卷積運算,得到該點的梯度(反映該點附近深度和法線突變的強度),根據(jù)梯度閾值判斷該點是否是邊緣。

該方案屬于內(nèi)描邊,效果較好,實現(xiàn)較難,算法依賴梯度閾值。

案例見→基于深度和法線紋理的邊緣檢測方法。

5)基于模板紋理模糊膨脹的描邊方法

首先使用純色對選中的物體進行渲染,得到模板紋理,接著對模板紋理進行模糊處理,使模板顏色往外擴,得到模糊紋理,再根據(jù)模板紋理和模糊紋理對所有物體重新渲染,渲染規(guī)則:如果該像素點在模板紋理內(nèi)部,就渲染原色,如果在模板紋理外部,就根據(jù)模糊紋理的透明度判斷渲染原色還是模糊紋理色。

該方案屬于外描邊,效果較好,實現(xiàn)較難,但算法不依賴閾值。

本文代碼資源見→Unity3D選中物體描邊特效。

2 基本原理

本文采用基于模板紋理模糊膨脹的描邊方法,本節(jié)將通過圖文詳細介紹該算法的原理。

1)原圖

2)模板紋理

說明:清屏顏色為 (0, 0, 0, 0),后面會用到 。通過 Graphics.ExecuteCommandBuffer(commandBuffer) 對選中的物體進行渲染,得到模板紋理。

3)模糊紋理

說明:通過對模板紋理進行模糊處理, 使模板顏色向外擴展,得到模糊紋理,外擴的部分就是需要描邊的部分。

4)合成紋理

說明:根據(jù)模板紋理和模糊紋理對所有物體重新渲染,渲染規(guī)則:如果該像素點在模板紋理內(nèi)部,就渲染原色,如果在模板紋理外部,就根據(jù)模糊紋理的透明度判斷渲染原色還是模糊紋理色,如下:

// 由于模糊紋理的外部清屏顏色是(0, 0, 0, 0), blur.a=0, 因此模糊紋理的外部也會被渲染為原色
color.rgb = lerp(source.rgb, blur.rgb, blur.a); // lerp(a,b,x)=(1-x)*a+x*b
color.a = source.a;

5)描邊顏色和寬度漸變

描邊顏色由模板顏色決定,通過設置模板顏色隨時間變化,實現(xiàn)描邊顏色漸變,通過設置模板透明度隨時間變化,實現(xiàn)描邊在出現(xiàn)和消失,視覺上感覺描邊在擴大和縮小。

fixed4 frag(v2f i) : SV_Target // 片段著色器
{
    float t1 = sin(_Time.z); // _Time = float4(t/20, t, t*2, t*3)
    float t2 = cos(_Time.z);
    // 描邊顏色隨時間變化, 描邊透明度隨時間變化, 視覺上感覺描邊在膨脹和收縮
    return float4(t1 + 1, t2 + 1, 1 - t1, 1 - t2);
}

漸變模板圖如下:

漸變描邊效果如下:

6)缺陷

如果描邊的物體存在重疊,由于所有物體共一個模板紋理,將存在描邊消融現(xiàn)象。

模板紋理如下:

描邊消融如下:

可以看到,正方體、球體、膠囊體、圓柱體下方及人體都沒有描邊特效,因為它們在模板紋理的內(nèi)部,被消融掉了。

3 代碼實現(xiàn)

OutlineEffect.cs

using System;
using UnityEngine;
using UnityEngine.Rendering;

public class OutlineEffect : MonoBehaviour {
    public static Action<CommandBuffer> renderEvent; // 渲染事件
    public float offsetScale = 2; // 模糊處理像素偏移
    public int iterate = 3; // 模糊處理迭代次數(shù)
    public float outlineStrength = 3; // 描邊強度

    private Material blurMaterial; // 模糊材質(zhì)
    private Material compositeMaterial; // 合成材質(zhì)
    private CommandBuffer commandBuffer; // 用于渲染模板紋理
    private RenderTexture stencilTex; // 模板紋理
    private RenderTexture blurTex; // 模糊紋理

    private void Awake() {
        blurMaterial = new Material(Shader.Find("Custom/Outline/Blur"));
        compositeMaterial = new Material(Shader.Find("Custom/Outline/Composite"));
        commandBuffer = new CommandBuffer();
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination) {
        if (renderEvent != null) {
            RenderStencil(); // 渲染模板紋理
            RenderBlur(source.width, source.height); // 渲染模糊紋理
            RenderComposite(source, destination); // 渲染合成紋理
        } else {
            Graphics.Blit(source, destination); // 保持原圖
        }
    }

    private void RenderStencil() { // 渲染模板紋理
        stencilTex = RenderTexture.GetTemporary(Screen.width, Screen.height, 0);
        commandBuffer.SetRenderTarget(stencilTex);
        commandBuffer.ClearRenderTarget(true, true, Color.clear); // 設置模板清屏顏色為(0,0,0,0)
        renderEvent.Invoke(commandBuffer);
        Graphics.ExecuteCommandBuffer(commandBuffer);
    }

    private void RenderBlur(int width, int height) { // 對模板紋理進行模糊化
        blurTex = RenderTexture.GetTemporary(width, height, 0);
        RenderTexture temp = RenderTexture.GetTemporary(width, height, 0);
        blurMaterial.SetFloat("_OffsetScale", offsetScale);
        Graphics.Blit(stencilTex, blurTex, blurMaterial);
        for (int i = 0; i < iterate; i ++) {
            Graphics.Blit(blurTex, temp, blurMaterial);
            Graphics.Blit(temp, blurTex, blurMaterial);
        }
        RenderTexture.ReleaseTemporary(temp);
    }

    private void RenderComposite(RenderTexture source, RenderTexture destination) { // 渲染合成紋理
        compositeMaterial.SetTexture("_MainTex", source);
        compositeMaterial.SetTexture("_StencilTex", stencilTex);
        compositeMaterial.SetTexture("_BlurTex", blurTex);
        compositeMaterial.SetFloat("_OutlineStrength", outlineStrength);
        Graphics.Blit(source, destination, compositeMaterial);
        RenderTexture.ReleaseTemporary(stencilTex);
        RenderTexture.ReleaseTemporary(blurTex);
        stencilTex = null;
        blurTex = null;
    }
}

說明: OnRenderImage 方法是MonoBehaviour的生命周期方法,在所有的渲染完成后由 MonoBehavior 自動調(diào)用,該方法依賴相機組件,由于 OnRenderImage 在渲染后調(diào)用,因此被稱為后處理操作,它是 Unity3D 特效的重要理論分支;Graphics.Blit(source, dest, material) 用于將 source 紋理按照 material 材質(zhì)重新渲染到 dest;CommandBuffer 攜帶一系列的渲染命令,依賴相機,用來拓展渲染管線的渲染效果;OutlineEffect 腳本組件必須掛在相機上。

OutlineObject.cs

using UnityEngine;
using UnityEngine.Rendering;

public class OutlineObject : MonoBehaviour {
    private Material stencilMaterial; // 模板材質(zhì)

    private void Awake() {
        stencilMaterial = new Material(Shader.Find("Custom/Outline/Stencil"));
    }

    private void OnEnable() {
        OutlineEffect.renderEvent += OnRenderEvent;
        // _StartTime用于控制每個選中的對象顏色漸變不同步
        stencilMaterial.SetFloat("_StartTime", Time.timeSinceLevelLoad * 2);
    }

    private void OnDisable() {
        OutlineEffect.renderEvent -= OnRenderEvent;
    }

    private void OnRenderEvent(CommandBuffer commandBuffer) {
        Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
        foreach (Renderer r in renderers) {
            commandBuffer.DrawRenderer(r, stencilMaterial); // 將renderer和material提交到主camera的commandbuffer列表進行渲染
        }
    }
}

說明:被選中的物體將會添加 OutlineObject 腳本組件,用于渲染選中對象的模板紋理,每個選中對象獨立持有 stencilMaterial,互不干擾,描邊的漸變相位(由_StartTime控制)可以由選中對象獨立控制,這樣每個模板的顏色就可以獨立控制,從而實現(xiàn)每個選中對象描邊各異的效果。

SelectController.cs

using System.Collections.Generic;
using UnityEngine;

public class SelectController : MonoBehaviour {
    private List<GameObject> targets; // 選中的游戲?qū)ο?    private List<GameObject> loseFocus; // 失焦的游戲?qū)ο?    private RaycastHit hit; // 碰撞信息

    private void Start() {
        Camera.main.gameObject.AddComponent<OutlineEffect>();
        targets = new List<GameObject>();
        loseFocus = new List<GameObject>();
        hit = new RaycastHit();
    }

    private void Update() {
        if (Input.GetMouseButtonUp(0)) {
            GameObject hitObj = GetHitObj();
            if (hitObj == null) { // 未選中任何物體, 已描邊的全部取消描邊
                targets.ForEach(obj => loseFocus.Add(obj));
                targets.Clear();
            }
            else if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) {
                if (targets.Contains(hitObj)) { // Ctrl重復選中, 取消描邊
                    loseFocus.Add(hitObj);
                    targets.Remove(hitObj);
                } else { // Ctrl追加描邊
                    targets.Add(hitObj);
                }
            } else { // 單選描邊
                targets.ForEach(obj => loseFocus.Add(obj));
                targets.Clear();
                targets.Add(hitObj);
                loseFocus.Remove(hitObj);
            }
            DrawOutline();
        }
    }

    private void DrawOutline() { // 描邊
        targets.ForEach(obj => {
            if (obj.GetComponent<OutlineObject>() == null) {
                obj.AddComponent<OutlineObject>();
            } else {
                obj.GetComponent<OutlineObject>().enabled = true;
            }
        });
        loseFocus.ForEach(obj => {
            if (obj.GetComponent<OutlineObject>() != null) {
                obj.GetComponent<OutlineObject>().enabled = false;
            }
        });
        loseFocus.Clear();
    }

    private GameObject GetHitObj() { // 獲取屏幕射線碰撞的物體
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out hit)) {
            return hit.collider.gameObject;
        }
        return null;
    }
}

說明:通過單擊物體,給選中的物體添加 OutlineObject 腳本組件,再由 OutlineEffect 控制描邊。按住 Ctrl 鍵再單擊物體會追加選中,如果重復選中,會取消描邊。

StencilShader.shader

Shader "Custom/Outline/Stencil"
{
    Properties
    {
        _StartTime ("startTime", Float) = 0 // _StartTime用于控制每個選中的對象顏色漸變不同步
    }

    SubShader
    {
        Pass
        {   
            CGPROGRAM // CG語言的開始
            // 編譯指令 著色器名稱 函數(shù)名稱
            #pragma vertex vert // 頂點著色器, 每個頂點執(zhí)行一次
            #pragma fragment frag // 片段著色器, 每個像素執(zhí)行一次
            #pragma fragmentoption ARB_precision_hint_fastest // fragment使用最低精度, fp16, 提高性能和速度

            // 導入頭文件
            #include "UnityCG.cginc"

            float _StartTime;

            struct a2v // 頂點函數(shù)輸入結(jié)構(gòu)體
            {
                float4 vertex: POSITION; // 頂點坐標
            };

            struct v2f // 頂點函數(shù)輸出結(jié)構(gòu)體
            {
                float4 pos : SV_POSITION;
            };
            
            v2f vert(a2v v) // 頂點著色器
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target // 片段著色器
            {
                float t1 = sin(_Time.z - _StartTime); // _Time = float4(t/20, t, t*2, t*3)
                float t2 = cos(_Time.z - _StartTime);
                // 描邊顏色隨時間變化, 描邊透明度隨時間變化, 視覺上感覺描邊在膨脹和收縮
                return float4(t1 + 1, t2 + 1, 1 - t1, 1 - t2);
            }

            ENDCG // CG語言的結(jié)束
        }
    }

    FallBack off
}

說明: StencilShader 用于渲染模板紋理,并且其顏色和透明度隨時間變化,實現(xiàn)描邊顏色漸變、寬度在膨脹和收縮效果。_StartTime 用于控制時間偏移,由物體被選中的時間決定,每個物體被選中的時間不一樣,因此選中物體模板顏色各異,描邊顏色也各異。

BlurShader.shader

Shader "Custom/Outline/Blur"
{
    Properties
    {
        _MainTex ("stencil", 2D) = "" {}
        _OffsetScale ("offsetScale", Range (0.1, 3)) = 2 // 模糊采樣偏移
    }
    
    SubShader
    {
        Pass
        {
            ZTest Always
            Cull Off
            ZWrite Off
            Lighting Off
            Fog { Mode Off }
            
            CGPROGRAM // CG語言的開始
            #pragma vertex vert // 頂點著色器, 每個頂點執(zhí)行一次
            #pragma fragment frag // 片段著色器, 每個像素執(zhí)行一次
            #pragma fragmentoption ARB_precision_hint_fastest // fragment使用最低精度, fp16, 提高性能和速度
            
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            half _OffsetScale;
            half4 _MainTex_TexelSize; //_MainTex的像素尺寸大小, float4(1/width, 1/height, width, height)

            struct a2v // 頂點函數(shù)輸入結(jié)構(gòu)體
            {
                float4 vertex: POSITION;
                half2 texcoord: TEXCOORD0;
            };

            struct v2f // 頂點函數(shù)輸出結(jié)構(gòu)體
            {
                float4 pos : POSITION;
                half2 uv[4] : TEXCOORD0;
            };
            
            v2f vert(a2v v) // 頂點著色器
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                half2 offs = _MainTex_TexelSize.xy * _OffsetScale;
                // uv坐標向四周擴散
                o.uv[0].x = v.texcoord.x - offs.x;
                o.uv[0].y = v.texcoord.y - offs.y;
                o.uv[1].x = v.texcoord.x + offs.x;
                o.uv[1].y = v.texcoord.y - offs.y;
                o.uv[2].x = v.texcoord.x + offs.x;
                o.uv[2].y = v.texcoord.y + offs.y;
                o.uv[3].x = v.texcoord.x - offs.x;
                o.uv[3].y = v.texcoord.y + offs.y;
                return o;
            }

            fixed4 frag(v2f i) : COLOR // 片段著色器
            {
                fixed4 color1 = tex2D(_MainTex, i.uv[0]);
                fixed4 color2 = tex2D(_MainTex, i.uv[1]);
                fixed4 color3 = tex2D(_MainTex, i.uv[2]);
                fixed4 color4 = tex2D(_MainTex, i.uv[3]);
                fixed4 color;
                // max: 2個向量中每個分量都取較大者, 這里通過max函數(shù)將模板的邊緣向外擴, rgb=stencil.rgb
                color.rgb = max(color1.rgb, color2.rgb);
                color.rgb = max(color.rgb, color3.rgb);
                color.rgb = max(color.rgb, color4.rgb);
                color.a = (color1.a + color2.a + color3.a + color4.a) / 4; // 透明度向外逐漸減小
                return color;
            }
            
            ENDCG // CG語言的結(jié)束
        }
    }
    
    Fallback off
}

說明: BlurShader 用于渲染模糊紋理,通過對模板紋理模糊化處理,實現(xiàn)模板顏色外擴,外擴的部分就是需要描邊的部分。

CompositeShader.shader

Shader "Custom/Outline/Composite"
{
    Properties
    {
        _MainTex ("source", 2D) = "" {}
        _StencilTex ("stencil", 2D) = "" {}
        _BlurTex ("blur", 2D) = "" {}
        _OutlineStrength ("OutlineStrength", Range(1, 5)) = 3
    }
    
    SubShader
    {
        Pass
        {
            ZTest Always
            Cull Off
            ZWrite Off
            Lighting Off
            Fog { Mode off }
            
            CGPROGRAM // CG語言的開始
            #pragma vertex vert // 頂點著色器, 每個頂點執(zhí)行一次
            #pragma fragment frag // 片段著色器, 每個像素執(zhí)行一次
            #pragma fragmentoption ARB_precision_hint_fastest // fragment使用最低精度, fp16, 提高性能和速度
            
            #include "UnityCG.cginc"
        
            sampler2D _MainTex;
            sampler2D _StencilTex;
            sampler2D _BlurTex;
            float _OutlineStrength;
            float4 _MainTex_TexelSize; //_MainTex的像素尺寸大小, float4(1/width, 1/height, width, height)

            struct a2v // 頂點函數(shù)輸入結(jié)構(gòu)體
            {
                float4 vertex: POSITION;
                half2 texcoord: TEXCOORD0;
            };

            struct v2f // 頂點函數(shù)輸出結(jié)構(gòu)體
            {
                float4 pos : POSITION;
                half2 uv : TEXCOORD0;
            };
            
            v2f vert(a2v v) // 頂點著色器
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                if (_MainTex_TexelSize.y < 0)
                    o.uv.y = 1 - o.uv.y; // 在Direct3D平臺下, 如果我們開啟了抗鋸齒, 則_MainTex_TexelSize.y 會變成負值
                return o;
            }
            
            fixed4 frag(v2f i) : COLOR // 片段著色器
            {
                fixed4 source = tex2D(_MainTex, i.uv);
                fixed4 stencil = tex2D(_StencilTex, i.uv);
                if (any(stencil.rgb))
                { // 繪制選中物體
                    return source;
                }
                else
                { // 繪制選中物體以外的圖像
                    fixed4 blur = tex2D(_BlurTex, i.uv);
                    fixed4 color;
                    color.rgb = lerp(source.rgb, blur.rgb * _OutlineStrength, saturate(blur.a - stencil.a));
                    color.a = source.a;
                    return color;
                }
            }

            ENDCG // CG語言的結(jié)束
        }
    }
    
    Fallback Off
}

說明: CompositeShader 用于渲染合成紋理,根據(jù)模板紋理和模糊紋理對所有物體重新渲染,渲染規(guī)則:如果該像素點在模板紋理內(nèi)部,就渲染原色,如果在模板紋理外部,就根據(jù)模糊紋理的透明度判斷渲染原色還是模糊紋理色。

4 運行效果

單擊物體選中描邊,按住 Ctrl 鍵單擊物體追加選中描邊,單擊地面或空白地方所有已描邊物體取消描邊(刪除了地面的碰撞體組件)。

5 拓展

HighlightingSystem 插件也實現(xiàn)了基于模板紋理模糊膨脹的描邊方法,插件資源在Unity3D選中物體描邊特效的【Assets\Plugins\HighlightingSystem】目錄下。

該插件與本文的區(qū)別在于模板紋理的渲染方式不同,本文通過 CommandBuffer 渲染模板紋理,HighlightingSystem 通過第二個相機渲染模板紋理(camera.Render() 方法),具體操作如下:在 HighlightingEffect 腳本組件的 OnPreRender 方法中,通過 Copy 主相機新生成一個相機,并將其 cullingMask 設置為 highlightingLayer(值為7),用于渲染圖層為 7 的物體的模板紋理,被添加 HighlightableObject 腳本組件的物體在渲染模板紋理時其圖層將會被臨時更改為 7,渲染結(jié)束后又恢復原圖層。

該插件存在一個缺陷,7 號圖層需要預留出來,否則 7 號圖層的物體周圍將存在一個模糊的灰色邊緣。

HighlightingSystem 插件的使用方法如下:將 HighlightingEffect 腳本組件掛在相機下,給需要描邊的物體添加 SpectrumController 腳本組件。運行時,已添加 SpectrumController 腳本組件的物體將會被自動添加 HighlightableObject 腳本組件。

6 推薦閱讀

聲明:本文轉(zhuǎn)自[【Unity3D】選中物體描邊特效

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容