Unity3d游戏角色描边
游戏里经常需要在角色上做描边,这里总结一下平时几种常见的描边做法。
一,两批次法:
优点是简单,效果直接,性价比高。
1. 定点对着法线方向外移,缺点是可以看出顶点之间有断裂
- Shader "ly/Outline_2Pass_1"
- {
- Properties
- {
- _MainTex("Texture", 2D) = "white"{}
- _Outline("Outline", range(0, 1)) = 0.02
- _OutlineColor("Outline Color", Color) = (1,1,1,1)
- }
- 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(0, 0, 0, 1);
- 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("Outline", range(0, 1)) = 0.02
- _OutlineColor("Outline Color", Color) = (1,1,1,1)
- }
- 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));
- float dotProduct = 1 - dot(normalize(v.normal), viewDir);
- fixed3 rimCol = smoothstep(1 - _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;
- public Color outlineColor = new Color(0, 1f, 0, 1f);// Color.green;
- [Range(0, 10)]
- public int outlineWidth = 1;
- [Range(1, 9)]
- public int iterations = 1;
- 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 = 1 << 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("Outline Color", Color) = (0,1,0,0.8) //描边颜色
- _Width("Outline Width", int) = 1 //描边宽度
- _Iterations("Iterations", int) = 1 //描边迭代次数(越多越平滑,消耗越高)
- }
- 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
- {
- //迭代次数为奇数,保证对称
- int iterations = _Iterations * 2 + 1;
- float ColorIntensityInRadius;
- float Tx_x = _MainTex_TexelSize.x * _Width;
- float Tx_y = _MainTex_TexelSize.y * _Width;
- //计算是否大于0,则此像素属于外边的范围内
- for (int k=0; k<iterations; k+=1)
- {
- for (int j=0; j<iterations; j+=1)
- {
- ColorIntensityInRadius += tex2D(_MainTex, i.uv.xy + float2((k - iterations / 2) * Tx_x, (j - iterations / 2) * Tx_y));
- }
- }
- //如果有颜色,或者不在外边的范围内,则渲染原场景。否则,在外边内,渲染描边。
- if (tex2D(_MainTex, i.uv.xy).r > 0 || ColorIntensityInRadius == 0)
- return tex2D(_SceneTex, i.uv);
- else
- return _Color.a * _Color + (1 - _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("Outline Color", Color) = (0,1,0,1) //描边颜色
- _Width("Outline Width", float) = 1 //描边宽度
- }
- 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[2] : TEXCOORD0;
- half2 uv2[4] : TEXCOORD2;
- };
- v2f vert(appdata_base v)
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);
- o.uv[0] = v.texcoord.xy;
- o.uv[1] = v.texcoord.xy;
- half2 offs = _SceneTex_TexelSize.xy * _Width;
- o.uv2[0].x = v.texcoord.x - offs.x;
- o.uv2[0].y = v.texcoord.y - offs.y;
- o.uv2[1].x = v.texcoord.x + offs.x;
- o.uv2[1].y = v.texcoord.y - offs.y;
- o.uv2[2].x = v.texcoord.x + offs.x;
- o.uv2[2].y = v.texcoord.y + offs.y;
- o.uv2[3].x = v.texcoord.x - offs.x;
- o.uv2[3].y = v.texcoord.y + offs.y;
- if (_SceneTex_TexelSize.y < 0)
- {
- o.uv[1].y = 1 - o.uv[1].y;
- o.uv2[0].y = 1 - o.uv2[0].y;
- o.uv2[1].y = 1 - o.uv2[1].y;
- o.uv2[2].y = 1 - o.uv2[2].y;
- o.uv2[3].y = 1 - o.uv2[3].y;
- }
- return o;
- }
- half4 frag(v2f i) : COLOR
- {
- fixed4 stencil = tex2D(_MainTex, i.uv[1]);
- // 有剪影的部分,显示原图
- if (any(stencil.rgb))
- {
- fixed4 framebuffer = tex2D(_SceneTex, i.uv[0]);
- return framebuffer;
- }
- // 没有剪影的部分,先把剪影扩张,扩张出颜色的部分用剪影,没有颜色的用原图
- else
- {
- fixed4 color1 = tex2D(_MainTex, i.uv2[0]);
- fixed4 color2 = tex2D(_MainTex, i.uv2[1]);
- fixed4 color3 = tex2D(_MainTex, i.uv2[2]);
- fixed4 color4 = tex2D(_MainTex, i.uv2[3]);
- 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[0]);
- return framebuffer;
- }
- }
- }
- ENDCG
- }
- }
- SubShader
- {
- Pass
- {
- SetTexture[_MainTex]{}
- }
- }
- Fallback Off
- }