本文发布于游戏程序员刘宇的个人博客,欢迎转载,请注明来源https://www.cnblogs.com/xiaohutu/p/10834491.html

游戏里经常需要在角色上做描边,这里总结一下平时几种常见的描边做法。

一,两批次法:

优点是简单,效果直接,性价比高。

1. 定点对着法线方向外移,缺点是可以看出顶点之间有断裂

Shader "ly/Outline_2Pass_1"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white"{}
        _Outline(, )) = 0.02
        _OutlineColor(,,,)
    }

    SubShader
    {
        //第一个批次,画描边
        Pass
        {
            //Cull掉前面的一半,只让描边显示在后面
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            fixed _Outline;
            fixed4 _OutlineColor;

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 color : COLOR;
            };

            v2f vert (appdata_full v)
            {
                v2f o;

                //源顶点位置添加法线方向乘以参数的偏移量
                v.vertex.xyz += v.normal * _Outline;

                //位置从自身坐标系转换到投影空间
                //旧版本o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
                o.pos = UnityObjectToClipPos(v.vertex);

                //描边颜色
                o.color = _OutlineColor;
                return o;
            }

            float4 frag (v2f i) : COLOR
            {
                return i.color; //描边
            }
            ENDCG
        }

        //第二个批次
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            sampler2D _MainTex;
            half4 _MainTex_ST;

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                fixed4 color : COLOR;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                o.color = fixed4(, , , );
                return o;
            }

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

2. 得到法线在投影空间上的xy轴,作为偏移方向将顶点外移,得到的结果类似1,也有断裂

3. 顶点的位置作为方向矢量,则不会因为方向差距较大而断裂

Shader "ly/Outline_2Pass_2"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white"{}
        _Outline(, )) = 0.02
        _OutlineColor(,,,)
    }

    SubShader
    {
        //第一个批次,画描边
        Pass
        {
            //Cull掉前面的一半,只让描边显示在后面
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            fixed _Outline;
            fixed4 _OutlineColor;

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
            };

            v2f vert (appdata_full v)
            {
                v2f o;

                //位置从自身坐标系转换到投影空间
                //旧版本o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
                o.pos = UnityObjectToClipPos(v.vertex);

                //方式二,扩张顶点位置
                //法线变换到投影空间
                //float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
                //得到投影空间的偏移
                //float2 offset = TransformViewToProjection(normal.xy);

                ////方式三,把顶点当做方向矢量,在方向矢量的方向偏移
                float3 dir = normalize(v.vertex.xyz);
                dir = mul((float3x3)UNITY_MATRIX_IT_MV, dir);
                float2 offset = TransformViewToProjection(dir.xy);

                //有一些情况下,侧边看不到,所以把方式一和二的算法相结合
                //float3 dir = normalize(v.vertex.xyz);
                //float3 dir2 = v.normal;
                //float D = dot(dir, dir2);
                //D = (1 + D / _Outline) / (1 + 1 / _Outline);
                //dir = lerp(dir2, dir, D);
                //dir = mul((float3x3)UNITY_MATRIX_IT_MV, dir);
                //float2 offset = TransformViewToProjection(dir.xy);
                //offset = normalize(offset);

                //在xy两个方向上偏移顶点的位置
                o.pos.xy += offset * o.pos.z * _Outline;

                return o;
            }

            float4 frag (v2f i) : COLOR
            {
                return _OutlineColor; //描边
            }
            ENDCG
        }
        //第二个批次,略
}

二,边缘光

顶点的视角dir和法线dir点乘,得出偏离度,越靠近边缘,颜色的强度越高。

优点是节约批次。

v2f vert (appdata_full v)
{
  v2f o;

  //略  //_RimColor边缘光颜色    //_RimPower边缘光强度
  float3 viewDir = normalize(ObjSpaceViewDir(v.vertex));
   - dot(normalize(v.normal), viewDir);
  fixed3 rimCol = smoothstep( - _RimPower, 1.0, dotProduct) * _RimColor;
  o.color = rimCol;

  //略
   return o;
}

  

三,后处理方式来画描边

优点是效果完美,缺点是消耗性能。

摄像机上挂一个脚本,处理后处理的步骤,outlineCamera 为临时摄像机,参数与主摄像机相同,看着同样的Unit层。

临时摄像机渲染到RT上,先画剪影,然后用自定义的描边shader画上去。

using UnityEngine;
using UnitySampleAssets.ImageEffects;

[RequireComponent(typeof(Camera))]
[AddComponentMenu("Image Effects/Other/Post Effect Outline")]

class PostEffectOutline : PostEffectsBase
{
    public enum OutLineMethod
    {
        eIteration,
        eScale,
    }

    private Camera attachCamera;
    private Camera outlineCamera;
    private Shader simpleShader;
    private Shader postOutlineShader;
    private Material postOutlineMat;
    private RenderTexture mTempRT;

    , 1f, , 1f);// Color.green;

    [Range(, )]
    ;

    [Range(, )]
    ;

    public OutLineMethod outlineMethod = OutLineMethod.eIteration;

    void Awake()
    {
        FindShaders();
    }

    void FindShaders()
    {
        if (!simpleShader)
            simpleShader = Shader.Find("ly/DrawSimple");

        if (outlineMethod == OutLineMethod.eIteration)
        {
            if (!postOutlineShader)
                postOutlineShader = Shader.Find("ly/PostOutlineIteration");
        }
        else
        {
            if (!postOutlineShader)
                postOutlineShader = Shader.Find("ly/PostOutlineScale");
        }
    }

    protected override void Start()
    {
        base.Start();
        attachCamera = GetComponent<Camera>();
        if (outlineCamera == null)
        {
            outlineCamera = new GameObject().AddComponent<Camera>();
            outlineCamera.enabled = false;
            outlineCamera.transform.parent = attachCamera.transform;
            outlineCamera.name = "outlineCam";
        }
        postOutlineMat = new Material(postOutlineShader);
    }

    public override bool CheckResources()
    {
        CheckSupport(false);

        if (!isSupported)
            ReportAutoDisable();
        return isSupported;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (CheckResources() == false)
        {
            Graphics.Blit(source, destination);
            return;
        }

        outlineCamera.CopyFrom(attachCamera);
        outlineCamera.clearFlags = CameraClearFlags.Color;
        outlineCamera.backgroundColor = Color.black;
        outlineCamera.cullingMask =  << LayerMask.NameToLayer("Unit");

        if (mTempRT == null)
            mTempRT = RenderTexture.GetTemporary(source.width, source.height, source.depth);

        mTempRT.Create();

        outlineCamera.targetTexture = mTempRT;
        outlineCamera.RenderWithShader(simpleShader, "");

        postOutlineMat.SetTexture("_SceneTex", source);
        postOutlineMat.SetColor("_Color", outlineColor);
        postOutlineMat.SetInt("_Width", outlineWidth);
        postOutlineMat.SetInt("_Iterations", iterations);

        //画描边混合材质
        Graphics.Blit(mTempRT, destination, postOutlineMat);

        mTempRT.Release();
    }
}

先用简单的shader画出剪影

Shader "ly/DrawSimple"
{
    FallBack OFF
}

然后就是这个自定义的描边shader画的过程。

第一种是类似高斯模糊的方式来迭代,迭代次数越多则越细腻。

// ly 类似高斯模糊方式迭代循环处理描边

Shader "ly/PostOutlineIteration"
{
    Properties
    {
        _MainTex("Main Texture", 2D) = "black"{}        //画完物体面积后的纹理
        _SceneTex("Scene Texture", 2D) = "black"{}        //原场景纹理
        _Color(,,,0.8)        //描边颜色
        _Width(                //描边宽度
        _Iterations(                //描边迭代次数(越多越平滑,消耗越高)
    }

    SubShader
    {
        Pass
        {
            CGPROGRAM
            sampler2D _MainTex;
            float2 _MainTex_TexelSize;
            sampler2D _SceneTex;
            fixed4 _Color;
            float _Width;
            int _Iterations;

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

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

            v2f vert(appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord.xy;
                return o;
            }

            half4 frag(v2f i) : COLOR
            {
                //迭代次数为奇数,保证对称
                 + ;
                float ColorIntensityInRadius;
                float Tx_x = _MainTex_TexelSize.x * _Width;
                float Tx_y = _MainTex_TexelSize.y * _Width;

                //计算是否大于0,则此像素属于外边的范围内
                ; k<iterations; k+=)
                {
                    ; j<iterations; j+=)
                    {
                        ColorIntensityInRadius += tex2D(_MainTex, i.uv.xy + float2((k - iterations / ) * Tx_x, (j - iterations / ) * Tx_y));
                    }
                }

                //如果有颜色,或者不在外边的范围内,则渲染原场景。否则,在外边内,渲染描边。
                 || ColorIntensityInRadius == )
                    return tex2D(_SceneTex, i.uv);
                else
                     - _Color.a)*tex2D(_SceneTex, i.uv);
            }

            ENDCG
        }
    }
}

第二种方法简单些,直接把剪影的部分uv扩大,再把原图叠上去。

// ly 扩张剪影uv来填充描边

Shader "ly/PostOutlineScale"
{
    Properties
    {
        _MainTex("Main Texture", 2D) = "black"{}        //画完物体面积后的纹理
        _SceneTex("Scene Texture", 2D) = "black"{}        //原场景纹理
        _Color(,,,)        //描边颜色
        _Width(                //描边宽度
    }

    SubShader
    {
        Pass
        {
            CGPROGRAM
            sampler2D _MainTex;
            sampler2D _SceneTex;
            float2 _SceneTex_TexelSize;

            fixed4 _Color;
            float _Width;

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                half2 uv[] : TEXCOORD0;
                half2 uv2[] : TEXCOORD2;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.uv[] = v.texcoord.xy;
                o.uv[] = v.texcoord.xy;

                half2 offs = _SceneTex_TexelSize.xy * _Width;

                o.uv2[].x = v.texcoord.x - offs.x;
                o.uv2[].y = v.texcoord.y - offs.y;

                o.uv2[].x = v.texcoord.x + offs.x;
                o.uv2[].y = v.texcoord.y - offs.y;

                o.uv2[].x = v.texcoord.x + offs.x;
                o.uv2[].y = v.texcoord.y + offs.y;

                o.uv2[].x = v.texcoord.x - offs.x;
                o.uv2[].y = v.texcoord.y + offs.y;

                )
                {
                    o.uv[].y =  - o.uv[].y;
                    o.uv2[].y =  - o.uv2[].y;
                    o.uv2[].y =  - o.uv2[].y;
                    o.uv2[].y =  - o.uv2[].y;
                    o.uv2[].y =  - o.uv2[].y;
                }
                return o;
            }

            half4 frag(v2f i) : COLOR
            {
                fixed4 stencil = tex2D(_MainTex, i.uv[]);

                // 有剪影的部分,显示原图
                if (any(stencil.rgb))
                {
                    fixed4 framebuffer = tex2D(_SceneTex, i.uv[]);
                    return framebuffer;
                }
                // 没有剪影的部分,先把剪影扩张,扩张出颜色的部分用剪影,没有颜色的用原图
                else
                {
                    fixed4 color1 = tex2D(_MainTex, i.uv2[]);
                    fixed4 color2 = tex2D(_MainTex, i.uv2[]);
                    fixed4 color3 = tex2D(_MainTex, i.uv2[]);
                    fixed4 color4 = tex2D(_MainTex, i.uv2[]);
                    fixed4 color;
                    color.rgb = max(color1.rgb, color2.rgb);
                    color.rgb = max(color.rgb, color3.rgb);
                    color.rgb = max(color.rgb, color4.rgb);
                    if (any(color.rgb))
                    {
                        return _Color;
                        //color.a = (color1.a + color2.a + color3.a + color4.a) * 0.25;
                        //return color;
                    }
                    else
                    {
                        fixed4 framebuffer = tex2D(_SceneTex, i.uv[]);
                        return framebuffer;
                    }
                }
            }

            ENDCG
        }
    }

    SubShader
    {
        Pass
        {
            SetTexture[_MainTex]{}
        }
    }

    Fallback Off
}

Unity3d游戏角色描边的更多相关文章

  1. Unity3D 游戏开发构架篇 ——角色类的设计与持久化

    在游戏开发中,游戏角色占了很大的篇幅,可以说游戏中所有的内容都是由主角所带动.这里就介绍一下角色类的设计和持久化. 一.角色类应用场景和设计思想 游戏中的角色类型不一而足,有不同的技能,有不同的属性等 ...

  2. Unity3D游戏开发从零单排(四) - 制作一个iOS游戏

    提要 此篇是一个国外教程的翻译,尽管有点老,可是适合新手入门. 自己去写代码.debug,布置场景,能够收获到非常多.游戏邦上已经有前面两部分的译文,这里翻译的是游戏的最后一个部分. 欢迎回来 在第一 ...

  3. [Unity3D]Unity3D游戏开发3D选择场景中的对象,并显示轮廓效果强化版

    大家好,我是秦培,欢迎关注我的博客,我的博客地址blog.csdn.net/qinyuanpei. 在上一篇文章中,我们通过自己定义着色器实现了一个简单的在3D游戏中选取.显示物体轮廓的实例. 在文章 ...

  4. [Unity3D]Unity3D游戏开发之跑酷游戏项目解说

    大家好,我是秦元培.我參加了CSDN2014博客之星的评选,欢迎大家为我投票,同一时候希望在新的一年里大家能继续支持我的博客. 大家晚上好.我是秦元培,欢迎大家关注我的博客,我的博客地址是blog.c ...

  5. [Unity3D]Unity3D游戏开发之伤害数值显示

    大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是blog.csdn.net/qinyuanpei.众所周知,在RPG游戏策划中最为重要的一个环节是数值策划.数值策划是一个关于游戏平衡方面的概念 ...

  6. [Unity3D]Unity3D游戏开发之怪物AI

    大家好.欢迎大家关注由我为大家带来的Unity3D游戏开发系列文章,我的博客地址为:http://blog.csdn.net/qinyuanpei.        在上一篇文章中,我们基本上实现了一个 ...

  7. Unity3D游戏开发之C#编程中常见数据结构的比较

    一.前言 Unity3D是如今最火爆的游戏开发引擎,它可以让我们能轻松创建诸如三维视频游戏.建筑可视化.实时三维动画等类型的互动内容.它支持2D/3D游戏开发,据不完全统计,目前国内80%的手机游戏都 ...

  8. Unity3D游戏开发初探—2.初步了解3D模型基础

    一.什么是3D模型? 1.1 3D模型概述 简而言之,3D模型就是三维的.立体的模型,D是英文Dimensions的缩写. 3D模型也可以说是用3Ds MAX建造的立体模型,包括各种建筑.人物.植被. ...

  9. Unity3D游戏在iOS上因为trampolines闪退的原因与解决办法

    http://7dot9.com/?p=444 http://whydoidoit.com/2012/08/20/unity-serializer-mono-and-trampolines/ 确定具体 ...

随机推荐

  1. HTML是什么?

    HTML,即超文本标记语言,它不是一种编程语言,而是一种标记语言,是网页制作所必备的.“超文本”就是指页面内可以包含图片.链接,甚至音乐.程序等非文字元素.超文本标记语言的结构包括“头”部分.和“主体 ...

  2. js简单实现promise

    function myPromise(fn){ let status='pending',successCallback=[],failedCallback=[],data=null,reason=n ...

  3. ZR7.26

    7.26 A 并查集维护,时间复杂度我写的貌似不大对,先鸽一鸽 B 敦爷:\(w\)是这个区间的最大值当且仅当他是这个区间内最大的 我们发现结合昨天课件内的并查集 发现我们每次不断合并的本质是把所有\ ...

  4. 2018-11-13-WPF-禁用实时触摸

    title author date CreateTime categories WPF 禁用实时触摸 lindexi 2018-11-13 10:45:37 +0800 2018-5-4 21:0:3 ...

  5. JS逻辑运算操作

    非! 如果一个操作数是一个对象,返回false; 如果一个操作数是一个空字符串,返回true; 如果一个操作数是一个非空字符串,返回false; 如果一个操作数是一个数值0,返回true; 如果一个操 ...

  6. CP防火墙排错装逼三件套

    1.tcpdump 通常用来抓包处理经过网卡的交互包 [Expert@BJ-OFFICE-GW:0]# tcpdump -nni any host 10.158.1.100 -w /var/log/t ...

  7. 一点资讯 视频抓取 phantomjs

    # _*_ coding: utf- _*_ """ 思路: .列表页使用phantomjs模拟点击.每个链接只抓取第一页9-10条内容,按照标题去重 .布置定时任务,每 ...

  8. C# ref参数

    ref关键字用于将方法内的变量改变后带出方法外.具体我们通过例子来说明: static void Main(string[] args) { int c = 0; Add(1, 2,ref c); C ...

  9. 浪潮服务器装linux系统无法识别硬盘

    先说无法识别硬盘的原因是没有安装和系统相对应的raid驱动. 公司需要两台Linux的物理服务器来部署业务系统. 客户给了两台浪潮的服务器. 1.先把linux系统刻录到u盘 2要先看一下raid卡的 ...

  10. 解决模糊查询问题 element UI 从服务器搜索数据,输入关键字进行查找

    做项目是遇见下拉框的形式,后台返回来3万多条,用element UI中的select选择器中的搜索还是会造成页面卡顿和系统崩溃,因此用了它的远程搜索功能,发现还不错,解决了这个问题. 代码1 < ...