• 【Unity3D】选中物体描边特效


    1 前言

            描边的难点在于如何检测和识别边缘,当前实现描边特效的方法主要有以下几种:

            1)基于顶点膨胀的描边方法

            在 SubShader 中开 2 个 Pass 渲染通道,第一个 Pass 通道渲染膨胀的顶点,即将顶点坐标沿着法线方向向外扩展,并使用纯色给扩展后的顶点着色,第二个 Pass 通道渲染原顶点,并覆盖第一个 Pass 通道渲染的内部。

            该方案实现简单,算法效率高,但是对于拐角较大的两个面交界处(法线突变较大处),会出现描边断裂,并且描边的宽度会受到透视投影影响。

            基于模板测试和顶点膨胀的描边方法 解决了描边断裂和描边宽度受透视影响问题。

            2)基于法线和视线的描边方法

            对于物体的任意一顶点,判断视线向量(该点指向相机的向量)与该点法线向量的夹角(记为 θ)是否近似 90 度,如果近似 90 度,就将该点识别为边缘,并进行边缘着色。实际应用中,通常采用模糊描边着色法,而不是阈值法,即将 sin(θ) 作为该点渲染描边色的强度。

            该方案属于内描边,实现简单,算法效率高,但是物体中间 θ 值近似 90 度的地方(凹陷处)也会被描边。

            3)基于屏幕纹理的描边方法

            对渲染后的屏幕纹理进行二次渲染,根据像素点周围颜色的差异判断是否是物体边缘像素,如果是边缘,需要重新进行边缘着色。判断边缘的具体做法是:对该像素点周围的像素点的亮度进行卷积运算,得到该点的梯度(反映该点附近亮度突变的强度),根据梯度阈值判断该点是否是边缘。

            该方案属于内描边,实现有一定难度,算法效率一般,算法依赖梯度阈值,并且受颜色、光照、阴影等影响较大,如:地面的影子可能会被描边。

            案例见→边缘检测特效

            4)基于深度和法线纹理的描边方法

             对渲染后的屏幕纹理进行二次渲染,根据像素点周围深度和法线的差异判断是否是物体边缘像素,如果是边缘,需要重新进行边缘着色。判断边缘的具体做法是:对该像素点周围的像素点的深度和法线进行卷积运算,得到该点的梯度(反映该点附近深度和法线突变的强度),根据梯度阈值判断该点是否是边缘。

            该方案属于内描边,效果较好,实现较难,算法依赖梯度阈值。

            案例见→基于深度和法线纹理的边缘检测方法

            5)基于模板纹理模糊膨胀的描边方法

            首先使用纯色对选中的物体进行渲染,得到模板纹理,接着对模板纹理进行模糊处理,使模板颜色往外扩,得到模糊纹理,再根据模板纹理和模糊纹理对所有物体重新渲染,渲染规则:如果该像素点在模板纹理内部,就渲染原色,如果在模板纹理外部,就根据模糊纹理的透明度判断渲染原色还是模糊纹理色。

            该方案属于外描边,效果较好,实现较难,但算法不依赖阈值。

            本文代码资源见→Unity3D选中物体描边特效

    2 基本原理

            本文采用基于模板纹理模糊膨胀的描边方法,本节将通过图文详细介绍该算法的原理。

            1)原图

            2)模板纹理

            说明:清屏颜色为 (0, 0, 0, 0),后面会用到 。通过 Graphics.ExecuteCommandBuffer(commandBuffer) 对选中的物体进行渲染,得到模板纹理。

            3)模糊纹理

            说明:通过对模板纹理进行模糊处理, 使模板颜色向外扩展,得到模糊纹理,外扩的部分就是需要描边的部分

            4)合成纹理

             说明:根据模板纹理和模糊纹理对所有物体重新渲染,渲染规则:如果该像素点在模板纹理内部,就渲染原色,如果在模板纹理外部,就根据模糊纹理的透明度判断渲染原色还是模糊纹理色,如下:

    1. // 由于模糊纹理的外部清屏颜色是(0, 0, 0, 0), blur.a=0, 因此模糊纹理的外部也会被渲染为原色
    2. color.rgb = lerp(source.rgb, blur.rgb, blur.a); // lerp(a,b,x)=(1-x)*a+x*b
    3. color.a = source.a;

            5)描边颜色和宽度渐变

             描边颜色由模板颜色决定,通过设置模板颜色随时间变化,实现描边颜色渐变,通过设置模板透明度随时间变化,实现描边在出现和消失,视觉上感觉描边在扩大和缩小。

    1. fixed4 frag(v2f i) : SV_Target // 片段着色器
    2. {
    3. float t1 = sin(_Time.z); // _Time = float4(t/20, t, t*2, t*3)
    4. float t2 = cos(_Time.z);
    5. // 描边颜色随时间变化, 描边透明度随时间变化, 视觉上感觉描边在膨胀和收缩
    6. return float4(t1 + 1, t2 + 1, 1 - t1, 1 - t2);
    7. }

            渐变模板图如下:

             渐变描边效果如下:

           

            6)缺陷

             如果描边的物体存在重叠,由于所有物体共一个模板纹理,将存在描边消融现象。

            模板纹理如下:

             描边消融如下:

             可以看到,正方体、球体、胶囊体、圆柱体下方及人体都没有描边特效,因为它们在模板纹理的内部,被消融掉了。

    3 代码实现

            OutlineEffect.cs

    1. using System;
    2. using UnityEngine;
    3. using UnityEngine.Rendering;
    4. public class OutlineEffect : MonoBehaviour {
    5. public static Action renderEvent; // 渲染事件
    6. public float offsetScale = 2; // 模糊处理像素偏移
    7. public int iterate = 3; // 模糊处理迭代次数
    8. public float outlineStrength = 3; // 描边强度
    9. private Material blurMaterial; // 模糊材质
    10. private Material compositeMaterial; // 合成材质
    11. private CommandBuffer commandBuffer; // 用于渲染模板纹理
    12. private RenderTexture stencilTex; // 模板纹理
    13. private RenderTexture blurTex; // 模糊纹理
    14. private void Awake() {
    15. blurMaterial = new Material(Shader.Find("Custom/Outline/Blur"));
    16. compositeMaterial = new Material(Shader.Find("Custom/Outline/Composite"));
    17. commandBuffer = new CommandBuffer();
    18. }
    19. private void OnRenderImage(RenderTexture source, RenderTexture destination) {
    20. if (renderEvent != null) {
    21. RenderStencil(); // 渲染模板纹理
    22. RenderBlur(source.width, source.height); // 渲染模糊纹理
    23. RenderComposite(source, destination); // 渲染合成纹理
    24. } else {
    25. Graphics.Blit(source, destination); // 保持原图
    26. }
    27. }
    28. private void RenderStencil() { // 渲染模板纹理
    29. stencilTex = RenderTexture.GetTemporary(Screen.width, Screen.height, 0);
    30. commandBuffer.SetRenderTarget(stencilTex);
    31. commandBuffer.ClearRenderTarget(true, true, Color.clear); // 设置模板清屏颜色为(0,0,0,0)
    32. renderEvent.Invoke(commandBuffer);
    33. Graphics.ExecuteCommandBuffer(commandBuffer);
    34. }
    35. private void RenderBlur(int width, int height) { // 对模板纹理进行模糊化
    36. blurTex = RenderTexture.GetTemporary(width, height, 0);
    37. RenderTexture temp = RenderTexture.GetTemporary(width, height, 0);
    38. blurMaterial.SetFloat("_OffsetScale", offsetScale);
    39. Graphics.Blit(stencilTex, blurTex, blurMaterial);
    40. for (int i = 0; i < iterate; i ++) {
    41. Graphics.Blit(blurTex, temp, blurMaterial);
    42. Graphics.Blit(temp, blurTex, blurMaterial);
    43. }
    44. RenderTexture.ReleaseTemporary(temp);
    45. }
    46. private void RenderComposite(RenderTexture source, RenderTexture destination) { // 渲染合成纹理
    47. compositeMaterial.SetTexture("_MainTex", source);
    48. compositeMaterial.SetTexture("_StencilTex", stencilTex);
    49. compositeMaterial.SetTexture("_BlurTex", blurTex);
    50. compositeMaterial.SetFloat("_OutlineStrength", outlineStrength);
    51. Graphics.Blit(source, destination, compositeMaterial);
    52. RenderTexture.ReleaseTemporary(stencilTex);
    53. RenderTexture.ReleaseTemporary(blurTex);
    54. stencilTex = null;
    55. blurTex = null;
    56. }
    57. }

            说明: OnRenderImage 方法是MonoBehaviour的生命周期方法,在所有的渲染完成后由 MonoBehavior 自动调用,该方法依赖相机组件,由于 OnRenderImage 在渲染后调用,因此被称为后处理操作,它是 Unity3D 特效的重要理论分支;Graphics.Blit(source, dest, material) 用于将 source 纹理按照 material 材质重新渲染到 dest;CommandBuffer 携带一系列的渲染命令,依赖相机,用来拓展渲染管线的渲染效果;OutlineEffect 脚本组件必须挂在相机上

            OutlineObject.cs

    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3. public class OutlineObject : MonoBehaviour {
    4. private Material stencilMaterial; // 模板材质
    5. private void Awake() {
    6. stencilMaterial = new Material(Shader.Find("Custom/Outline/Stencil"));
    7. }
    8. private void OnEnable() {
    9. OutlineEffect.renderEvent += OnRenderEvent;
    10. // _StartTime用于控制每个选中的对象颜色渐变不同步
    11. stencilMaterial.SetFloat("_StartTime", Time.timeSinceLevelLoad * 2);
    12. }
    13. private void OnDisable() {
    14. OutlineEffect.renderEvent -= OnRenderEvent;
    15. }
    16. private void OnRenderEvent(CommandBuffer commandBuffer) {
    17. Renderer[] renderers = gameObject.GetComponentsInChildren();
    18. foreach (Renderer r in renderers) {
    19. commandBuffer.DrawRenderer(r, stencilMaterial); // 将renderer和material提交到主camera的commandbuffer列表进行渲染
    20. }
    21. }
    22. }

            说明:被选中的物体将会添加 OutlineObject 脚本组件,用于渲染选中对象的模板纹理,每个选中对象独立持有 stencilMaterial,互不干扰,描边的渐变相位(由_StartTime控制)可以由选中对象独立控制,这样每个模板的颜色就可以独立控制,从而实现每个选中对象描边各异的效果。

            SelectController.cs

    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. public class SelectController : MonoBehaviour {
    4. private List<GameObject> targets; // 选中的游戏对象
    5. private List<GameObject> loseFocus; // 失焦的游戏对象
    6. private RaycastHit hit; // 碰撞信息
    7. private void Start() {
    8. Camera.main.gameObject.AddComponent();
    9. targets = new List();
    10. loseFocus = new List();
    11. hit = new RaycastHit();
    12. }
    13. private void Update() {
    14. if (Input.GetMouseButtonUp(0)) {
    15. GameObject hitObj = GetHitObj();
    16. if (hitObj == null) { // 未选中任何物体, 已描边的全部取消描边
    17. targets.ForEach(obj => loseFocus.Add(obj));
    18. targets.Clear();
    19. }
    20. else if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) {
    21. if (targets.Contains(hitObj)) { // Ctrl重复选中, 取消描边
    22. loseFocus.Add(hitObj);
    23. targets.Remove(hitObj);
    24. } else { // Ctrl追加描边
    25. targets.Add(hitObj);
    26. }
    27. } else { // 单选描边
    28. targets.ForEach(obj => loseFocus.Add(obj));
    29. targets.Clear();
    30. targets.Add(hitObj);
    31. loseFocus.Remove(hitObj);
    32. }
    33. DrawOutline();
    34. }
    35. }
    36. private void DrawOutline() { // 描边
    37. targets.ForEach(obj => {
    38. if (obj.GetComponent() == null) {
    39. obj.AddComponent();
    40. } else {
    41. obj.GetComponent().enabled = true;
    42. }
    43. });
    44. loseFocus.ForEach(obj => {
    45. if (obj.GetComponent() != null) {
    46. obj.GetComponent().enabled = false;
    47. }
    48. });
    49. loseFocus.Clear();
    50. }
    51. private GameObject GetHitObj() { // 获取屏幕射线碰撞的物体
    52. Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    53. if (Physics.Raycast(ray, out hit)) {
    54. return hit.collider.gameObject;
    55. }
    56. return null;
    57. }
    58. }

            说明:通过单击物体,给选中的物体添加 OutlineObject 脚本组件,再由 OutlineEffect 控制描边。按住 Ctrl 键再单击物体会追加选中,如果重复选中,会取消描边。

            StencilShader.shader

    1. Shader "Custom/Outline/Stencil"
    2. {
    3. Properties
    4. {
    5. _StartTime ("startTime", Float) = 0 // _StartTime用于控制每个选中的对象颜色渐变不同步
    6. }
    7. SubShader
    8. {
    9. Pass
    10. {
    11. CGPROGRAM // CG语言的开始
    12. // 编译指令 着色器名称 函数名称
    13. #pragma vertex vert // 顶点着色器, 每个顶点执行一次
    14. #pragma fragment frag // 片段着色器, 每个像素执行一次
    15. #pragma fragmentoption ARB_precision_hint_fastest // fragment使用最低精度, fp16, 提高性能和速度
    16. // 导入头文件
    17. #include "UnityCG.cginc"
    18. float _StartTime;
    19. struct a2v // 顶点函数输入结构体
    20. {
    21. float4 vertex: POSITION; // 顶点坐标
    22. };
    23. struct v2f // 顶点函数输出结构体
    24. {
    25. float4 pos : SV_POSITION;
    26. };
    27. v2f vert(a2v v) // 顶点着色器
    28. {
    29. v2f o;
    30. o.pos = UnityObjectToClipPos(v.vertex);
    31. return o;
    32. }
    33. fixed4 frag(v2f i) : SV_Target // 片段着色器
    34. {
    35. float t1 = sin(_Time.z - _StartTime); // _Time = float4(t/20, t, t*2, t*3)
    36. float t2 = cos(_Time.z - _StartTime);
    37. // 描边颜色随时间变化, 描边透明度随时间变化, 视觉上感觉描边在膨胀和收缩
    38. return float4(t1 + 1, t2 + 1, 1 - t1, 1 - t2);
    39. }
    40. ENDCG // CG语言的结束
    41. }
    42. }
    43. FallBack off
    44. }

            说明: StencilShader 用于渲染模板纹理,并且其颜色和透明度随时间变化,实现描边颜色渐变、宽度在膨胀和收缩效果。_StartTime 用于控制时间偏移,由物体被选中的时间决定,每个物体被选中的时间不一样,因此选中物体模板颜色各异,描边颜色也各异。

            BlurShader.shader

    1. Shader "Custom/Outline/Blur"
    2. {
    3. Properties
    4. {
    5. _MainTex ("stencil", 2D) = "" {}
    6. _OffsetScale ("offsetScale", Range (0.1, 3)) = 2 // 模糊采样偏移
    7. }
    8. SubShader
    9. {
    10. Pass
    11. {
    12. ZTest Always
    13. Cull Off
    14. ZWrite Off
    15. Lighting Off
    16. Fog { Mode Off }
    17. CGPROGRAM // CG语言的开始
    18. #pragma vertex vert // 顶点着色器, 每个顶点执行一次
    19. #pragma fragment frag // 片段着色器, 每个像素执行一次
    20. #pragma fragmentoption ARB_precision_hint_fastest // fragment使用最低精度, fp16, 提高性能和速度
    21. #include "UnityCG.cginc"
    22. sampler2D _MainTex;
    23. half _OffsetScale;
    24. half4 _MainTex_TexelSize; //_MainTex的像素尺寸大小, float4(1/width, 1/height, width, height)
    25. struct a2v // 顶点函数输入结构体
    26. {
    27. float4 vertex: POSITION;
    28. half2 texcoord: TEXCOORD0;
    29. };
    30. struct v2f // 顶点函数输出结构体
    31. {
    32. float4 pos : POSITION;
    33. half2 uv[4] : TEXCOORD0;
    34. };
    35. v2f vert(a2v v) // 顶点着色器
    36. {
    37. v2f o;
    38. o.pos = UnityObjectToClipPos(v.vertex);
    39. half2 offs = _MainTex_TexelSize.xy * _OffsetScale;
    40. // uv坐标向四周扩散
    41. o.uv[0].x = v.texcoord.x - offs.x;
    42. o.uv[0].y = v.texcoord.y - offs.y;
    43. o.uv[1].x = v.texcoord.x + offs.x;
    44. o.uv[1].y = v.texcoord.y - offs.y;
    45. o.uv[2].x = v.texcoord.x + offs.x;
    46. o.uv[2].y = v.texcoord.y + offs.y;
    47. o.uv[3].x = v.texcoord.x - offs.x;
    48. o.uv[3].y = v.texcoord.y + offs.y;
    49. return o;
    50. }
    51. fixed4 frag(v2f i) : COLOR // 片段着色器
    52. {
    53. fixed4 color1 = tex2D(_MainTex, i.uv[0]);
    54. fixed4 color2 = tex2D(_MainTex, i.uv[1]);
    55. fixed4 color3 = tex2D(_MainTex, i.uv[2]);
    56. fixed4 color4 = tex2D(_MainTex, i.uv[3]);
    57. fixed4 color;
    58. // max: 2个向量中每个分量都取较大者, 这里通过max函数将模板的边缘向外扩, rgb=stencil.rgb
    59. color.rgb = max(color1.rgb, color2.rgb);
    60. color.rgb = max(color.rgb, color3.rgb);
    61. color.rgb = max(color.rgb, color4.rgb);
    62. color.a = (color1.a + color2.a + color3.a + color4.a) / 4; // 透明度向外逐渐减小
    63. return color;
    64. }
    65. ENDCG // CG语言的结束
    66. }
    67. }
    68. Fallback off
    69. }

            说明: BlurShader 用于渲染模糊纹理,通过对模板纹理模糊化处理,实现模板颜色外扩,外扩的部分就是需要描边的部分。 

            CompositeShader.shader

    1. Shader "Custom/Outline/Composite"
    2. {
    3. Properties
    4. {
    5. _MainTex ("source", 2D) = "" {}
    6. _StencilTex ("stencil", 2D) = "" {}
    7. _BlurTex ("blur", 2D) = "" {}
    8. _OutlineStrength ("OutlineStrength", Range(1, 5)) = 3
    9. }
    10. SubShader
    11. {
    12. Pass
    13. {
    14. ZTest Always
    15. Cull Off
    16. ZWrite Off
    17. Lighting Off
    18. Fog { Mode off }
    19. CGPROGRAM // CG语言的开始
    20. #pragma vertex vert // 顶点着色器, 每个顶点执行一次
    21. #pragma fragment frag // 片段着色器, 每个像素执行一次
    22. #pragma fragmentoption ARB_precision_hint_fastest // fragment使用最低精度, fp16, 提高性能和速度
    23. #include "UnityCG.cginc"
    24. sampler2D _MainTex;
    25. sampler2D _StencilTex;
    26. sampler2D _BlurTex;
    27. float _OutlineStrength;
    28. float4 _MainTex_TexelSize; //_MainTex的像素尺寸大小, float4(1/width, 1/height, width, height)
    29. struct a2v // 顶点函数输入结构体
    30. {
    31. float4 vertex: POSITION;
    32. half2 texcoord: TEXCOORD0;
    33. };
    34. struct v2f // 顶点函数输出结构体
    35. {
    36. float4 pos : POSITION;
    37. half2 uv : TEXCOORD0;
    38. };
    39. v2f vert(a2v v) // 顶点着色器
    40. {
    41. v2f o;
    42. o.pos = UnityObjectToClipPos(v.vertex);
    43. o.uv = v.texcoord;
    44. if (_MainTex_TexelSize.y < 0)
    45. o.uv.y = 1 - o.uv.y; // 在Direct3D平台下, 如果我们开启了抗锯齿, 则_MainTex_TexelSize.y 会变成负值
    46. return o;
    47. }
    48. fixed4 frag(v2f i) : COLOR // 片段着色器
    49. {
    50. fixed4 source = tex2D(_MainTex, i.uv);
    51. fixed4 stencil = tex2D(_StencilTex, i.uv);
    52. if (any(stencil.rgb))
    53. { // 绘制选中物体
    54. return source;
    55. }
    56. else
    57. { // 绘制选中物体以外的图像
    58. fixed4 blur = tex2D(_BlurTex, i.uv);
    59. fixed4 color;
    60. color.rgb = lerp(source.rgb, blur.rgb * _OutlineStrength, saturate(blur.a - stencil.a));
    61. color.a = source.a;
    62. return color;
    63. }
    64. }
    65. ENDCG // CG语言的结束
    66. }
    67. }
    68. Fallback Off
    69. }

            说明: CompositeShader 用于渲染合成纹理,根据模板纹理和模糊纹理对所有物体重新渲染,渲染规则:如果该像素点在模板纹理内部,就渲染原色,如果在模板纹理外部,就根据模糊纹理的透明度判断渲染原色还是模糊纹理色。

    4 运行效果

            单击物体选中描边,按住 Ctrl 键单击物体追加选中描边,单击地面或空白地方所有已描边物体取消描边(删除了地面的碰撞体组件)。

    5 拓展

            HighlightingSystem 插件也实现了基于模板纹理模糊膨胀的描边方法,插件资源在Unity3D选中物体描边特效的【Assets\Plugins\HighlightingSystem】目录下。

            该插件与本文的区别在于模板纹理的渲染方式不同,本文通过 CommandBuffer 渲染模板纹理,HighlightingSystem 通过第二个相机渲染模板纹理(camera.Render() 方法),具体操作如下:在 HighlightingEffect 脚本组件的 OnPreRender 方法中,通过 Copy 主相机新生成一个相机,并将其 cullingMask 设置为 highlightingLayer(值为7),用于渲染图层为 7  的物体的模板纹理,被添加 HighlightableObject 脚本组件的物体在渲染模板纹理时其图层将会被临时更改为 7,渲染结束后又恢复原图层。

            该插件存在一个缺陷,7 号图层需要预留出来,否则 7 号图层的物体周围将存在一个模糊的灰色边缘。

            HighlightingSystem 插件的使用方法如下:将 HighlightingEffect 脚本组件挂在相机下,给需要描边的物体添加 SpectrumController 脚本组件。运行时,已添加 SpectrumController 脚本组件的物体将会被自动添加 HighlightableObject 脚本组件。

    6 推荐阅读

  • 相关阅读:
    基础复习——图形定制——图形Drawable——形状图形——九宫格图片——状态列表图形...
    如何用Java+SpringBoot+Vue构建一个靓车汽车销售网站?
    MySQL InnoDB 表不存在问题修复
    【模板】2-SAT
    体验极速——在旭日X3派上使用双频1300M USB无线网卡
    现代化个人博客系统 ModStartBlog v5.6.0 备案信息完善,功能组件优化
    Vue前端开发:事件传参
    NOIP 装箱问题
    go sync.Map包装过的对象nil值的判断
    Centos7 查看磁盘i/o, 定位占用i/o读写高的进程
  • 原文地址:https://blog.csdn.net/m0_37602827/article/details/127937019