NGUI通用边框流光

UI层的矩形按钮、矩形头像,很多时候都需要有个高亮的边框来点缀。最近本人写了一个扩展版的UI边框,跟使用UITexture一样方便,得到了特效师的高度评价,便拿出来献丑分享。效果如下:

NGUI通用边框流光_第1张图片

小新的头像是一个UITexture,外面的边框是一个叫做UIHollowedTexture的UI组件(拿UITexture魔改而来),可以设置边框贴图,可以UV滚动。本文接下来会介绍UIHollowedTexture的做法。

对于正常的UITexture来说,其位置和尺寸决定了它的4个边角顶点的位置,然后根据这4个顶点绘制2个三角形,合成一个矩形图案;而对于UIHollowedTexture来说,得到4个边角点后,再根据我们需要的边框宽度,外拓得到4个外顶点,然后把每一侧的2个内点和2个外点绘制成一个四边形,如下图所示:

NGUI通用边框流光_第2张图片

绘制顶点的问题解决了,然后是决定UV的问题,如下图所示,8个顶点分别对应整张贴图的指定位置,UV依据该图分配即可,注意我把图横向拉长了。

NGUI通用边框流光_第3张图片

如果就这么做的话,显示上会有一个小问题,大家观察一下2367组成的四边形,它是个梯形,而我们贴在梯形上的贴图是个矩形。结果会导致贴图变形,原因如下图

NGUI通用边框流光_第4张图片

假设有一张方形贴图,它有一条左上角到右下角的斜线,贴到矩形面片里是正常的(图1),而如果贴到梯形里(为了简化问题我用直角梯形说明),我最初以为会是图2那样,结果却会变成图3。因为一个四边形是2个三角形组成的,如果我们把一个矩形的右下角顶点朝右边拉一段距离,结果是左上角的三角形没有任何改变,而右下角的三角形发生了变形,导致斜线的斜率发生了变化,结果就是一条直线变成了折线。(为了能浅显解释这个问题我想了好久)。

我为这个显示问题请教了美术同学,他们表示可以通过把大梯形切割成多个小梯形来优化效果

NGUI通用边框流光_第5张图片

如图所示,把梯形拆成上下堆叠的小梯形,这条斜线是不是平滑了一些?如果切的够细,就可以把它变成一条平滑的弧线,这样看起来就不突兀了。


上面把一些原理性的东西讲完了,接下来是具体代码:

脚本 UIHollowedTexture.cs

//-------------------------------------------------
//            NGUI: Next-Gen UI kit
// Copyright © 2011-2018 Tasharen Entertainment Inc
// Create By LZ
//-------------------------------------------------

using UnityEngine;
using System.Collections.Generic;

/// 
/// If you don't have or don't wish to create an atlas, you can simply use this script to draw a texture.
/// Keep in mind though that this will create an extra draw call with each UITexture present, so it's
/// best to use it only for backgrounds or temporary visible widgets.
/// 

[ExecuteInEditMode]
[AddComponentMenu("NGUI/UI/NGUI Hollowed Texture")]
public class UIHollowedTexture : UIBasicSprite
{
	[HideInInspector][SerializeField] Rect mRect = new Rect(0f, 0f, 1f, 1f);
	[HideInInspector][SerializeField] Texture mTexture;
	[HideInInspector][SerializeField] Shader mShader;
	[HideInInspector][SerializeField] Vector4 mBorder = Vector4.zero;
	[HideInInspector][SerializeField] bool mFixedAspect = false;

	[HideInInspector] [SerializeField] int mBorderWidth = 10; // 边框宽度(像素)
	[HideInInspector] [SerializeField] int mBorderPliesNum = 1; // 边框层数(由几圈边框组成)
	[HideInInspector] [SerializeField] float mUvAnimSpeedH = 0; // UV动画速度 水平
	[HideInInspector] [SerializeField] float mUvAnimSpeedV = 0; // UV动画速度 竖直
	[HideInInspector] [SerializeField] bool mUvAnimIgnoreTimeScale = true; // UV动画是否忽略时间缩放

	[System.NonSerialized] int mPMA = -1;

	/// 
	/// Texture used by the UITexture. You can set it directly, without the need to specify a material.
	/// 

	public override Texture mainTexture
	{
		get
		{
			if (mTexture != null) return mTexture;
			if (mMat != null) return mMat.mainTexture;
			return null;
		}
		set
		{
			if (mTexture != value)
			{
				if (drawCall != null && drawCall.widgetCount == 1 && mMat == null)
				{
					mTexture = value;
					drawCall.mainTexture = value;
				}
				else
				{
					RemoveFromPanel();
					mTexture = value;
					mPMA = -1;
					MarkAsChanged();
				}
			}
		}
	}

	/// 
	/// Material used by the widget.
	/// 

	public override Material material
	{
		get
		{
			return mMat;
		}
		set
		{
			if (mMat != value)
			{
				RemoveFromPanel();
				mShader = null;
				mMat = value;
				mPMA = -1;
				MarkAsChanged();
			}
		}
	}

	/// 
	/// Shader used by the texture when creating a dynamic material (when the texture was specified, but the material was not).
	/// 

	public override Shader shader
	{
		get
		{
			if (mMat != null) return mMat.shader;
			if (mShader == null) mShader = Shader.Find("Unlit/Transparent Colored");
			return mShader;
		}
		set
		{
			if (mShader != value)
			{
				if (drawCall != null && drawCall.widgetCount == 1 && mMat == null)
				{
					mShader = value;
					drawCall.shader = value;
				}
				else
				{
					RemoveFromPanel();
					mShader = value;
					mPMA = -1;
					mMat = null;
					MarkAsChanged();
				}
			}
		}
	}

	/// 
	/// Whether the texture is using a premultiplied alpha material.
	/// 

	public override bool premultipliedAlpha
	{
		get
		{
			if (mPMA == -1)
			{
				Material mat = material;
				mPMA = (mat != null && mat.shader != null && mat.shader.name.Contains("Premultiplied")) ? 1 : 0;
			}
			return (mPMA == 1);
		}
	}


	/// 
	/// Sprite's border. X = left, Y = bottom, Z = right, W = top.
	/// 

	public override Vector4 border
	{
		get
		{
			return mBorder;
		}
		set
		{
			if (mBorder != value)
			{
				mBorder = value;
				MarkAsChanged();
			}
		}
	}

	/// 
	/// UV rectangle used by the texture.
	/// 

	public Rect uvRect
	{
		get
		{
			return mRect;
		}
		set
		{
			if (mRect != value)
			{
				mRect = value;
				MarkAsChanged();
			}
		}
	}

	/// 
	/// Widget's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top.
	/// This function automatically adds 1 pixel on the edge if the texture's dimensions are not even.
	/// It's used to achieve pixel-perfect sprites even when an odd dimension widget happens to be centered.
	/// 

	public override Vector4 drawingDimensions
	{
		get
		{
			Vector2 offset = pivotOffset;

			float x0 = -offset.x * mWidth;
			float y0 = -offset.y * mHeight;
			float x1 = x0 + mWidth;
			float y1 = y0 + mHeight;

			if (mTexture != null && mType != UISprite.Type.Tiled)
			{
				int w = mTexture.width;
				int h = mTexture.height;
				int padRight = 0;
				int padTop = 0;

				float px = 1f;
				float py = 1f;

				if (w > 0 && h > 0 && (mType == UISprite.Type.Simple || mType == UISprite.Type.Filled))
				{
					if ((w & 1) != 0) ++padRight;
					if ((h & 1) != 0) ++padTop;

					px = (1f / w) * mWidth;
					py = (1f / h) * mHeight;
				}

				if (mFlip == UISprite.Flip.Horizontally || mFlip == UISprite.Flip.Both)
				{
					x0 += padRight * px;
				}
				else x1 -= padRight * px;

				if (mFlip == UISprite.Flip.Vertically || mFlip == UISprite.Flip.Both)
				{
					y0 += padTop * py;
				}
				else y1 -= padTop * py;
			}

			float fw, fh;

			if (mFixedAspect)
			{
				fw = 0f;
				fh = 0f;
			}
			else
			{
				Vector4 br = border;
				fw = br.x + br.z;
				fh = br.y + br.w;
			}

			float vx = Mathf.Lerp(x0, x1 - fw, mDrawRegion.x);
			float vy = Mathf.Lerp(y0, y1 - fh, mDrawRegion.y);
			float vz = Mathf.Lerp(x0 + fw, x1, mDrawRegion.z);
			float vw = Mathf.Lerp(y0 + fh, y1, mDrawRegion.w);

			return new Vector4(vx, vy, vz, vw);
		}
	}

	/// 
	/// Whether the drawn texture will always maintain a fixed aspect ratio.
	/// This setting is not compatible with drawRegion adjustments (sliders, progress bars, etc).
	/// 

	public bool fixedAspect
	{
		get
		{
			return mFixedAspect;
		}
		set
		{
			if (mFixedAspect != value)
			{
				mFixedAspect = value;
				mDrawRegion = new Vector4(0f, 0f, 1f, 1f);
				MarkAsChanged();
			}
		}
	}

	/// 
	/// Adjust the scale of the widget to make it pixel-perfect.
	/// 

	public override void MakePixelPerfect ()
	{
		base.MakePixelPerfect();
		if (mType == Type.Tiled) return;

		Texture tex = mainTexture;
		if (tex == null) return;

		if (mType == Type.Simple || mType == Type.Filled || !hasBorder)
		{
			if (tex != null)
			{
				int w = tex.width;
				int h = tex.height;

				if ((w & 1) == 1) ++w;
				if ((h & 1) == 1) ++h;

				width = w;
				height = h;
			}
		}
	}

	/// 
	/// Adjust the draw region if the texture is using a fixed aspect ratio.
	/// 

	protected override void OnUpdate ()
	{
		base.OnUpdate();
		
		if (mFixedAspect)
		{
			Texture tex = mainTexture;

			if (tex != null)
			{
				int w = tex.width;
				int h = tex.height;
				if ((w & 1) == 1) ++w;
				if ((h & 1) == 1) ++h;
				float widgetWidth = mWidth;
				float widgetHeight = mHeight;
				float widgetAspect = widgetWidth / widgetHeight;
				float textureAspect = (float)w / h;

				if (textureAspect < widgetAspect)
				{
					float x = (widgetWidth - widgetHeight * textureAspect) / widgetWidth * 0.5f;
					drawRegion = new Vector4(x, 0f, 1f - x, 1f);
				}
				else
				{
					float y = (widgetHeight - widgetWidth / textureAspect) / widgetHeight * 0.5f;
					drawRegion = new Vector4(0f, y, 1f, 1f - y);
				}
			}
		}

		// UV动画
		if (Application.isPlaying && (mUvAnimSpeedH != 0 || mUvAnimSpeedV != 0))
		{
			float delta = mUvAnimIgnoreTimeScale ? RealTime.deltaTime : Time.deltaTime;
			Rect uvr = uvRect;
			uvr.x += mUvAnimSpeedH * delta * uvr.width;
			uvr.y += mUvAnimSpeedV * delta * uvr.height;
			uvr.x -= Mathf.Floor(uvRect.x);
			uvr.y -= Mathf.Floor(uvRect.y);
			uvRect = uvr;
		}
	}

	/// 
	/// Virtual function called by the UIPanel that fills the buffers.
	/// 

	public override void OnFill (List verts, List uvs, List cols)
	{
		Texture tex = mainTexture;
		if (tex == null) return;

		Rect outer = new Rect(mRect.x * tex.width, mRect.y * tex.height, tex.width * mRect.width, tex.height * mRect.height);
		Rect inner = outer;
		Vector4 br = border;
		inner.xMin += br.x;
		inner.yMin += br.y;
		inner.xMax -= br.z;
		inner.yMax -= br.w;

		float w = 1f / tex.width;
		float h = 1f / tex.height;

		outer.xMin *= w;
		outer.xMax *= w;
		outer.yMin *= h;
		outer.yMax *= h;

		inner.xMin *= w;
		inner.xMax *= w;
		inner.yMin *= h;
		inner.yMax *= h;

		int offset = verts.Count;
		Fill(verts, uvs, cols, outer, inner);

		if (onPostFill != null)
			onPostFill(this, offset, verts, uvs, cols);
	}
	
	protected new void Fill(List verts, List uvs, List cols, Rect outer, Rect inner)
	{
		Rect mOuterUV = outer;
		Rect mInnerUV = inner;
		
		Vector4 v = drawingDimensions;

		Vector4 u;
		if (mFlip == Flip.Horizontally) u = new Vector4(mOuterUV.xMax, mOuterUV.yMin, mOuterUV.xMin, mOuterUV.yMax);
		else if (mFlip == Flip.Vertically) u = new Vector4(mOuterUV.xMin, mOuterUV.yMax, mOuterUV.xMax, mOuterUV.yMin);
		else if (mFlip == Flip.Both) u = new Vector4(mOuterUV.xMax, mOuterUV.yMax, mOuterUV.xMin, mOuterUV.yMin);
		else u = new Vector4(mOuterUV.xMin, mOuterUV.yMin, mOuterUV.xMax, mOuterUV.yMax);
		
		Color gc = color;
		gc.a = finalAlpha;
		if (premultipliedAlpha) gc = NGUITools.ApplyPMA(gc);
		
		mBorderPliesNum = Mathf.Clamp(mBorderPliesNum, 1, 99);
		float borderPlieWidth = (float)mBorderWidth / mBorderPliesNum;
		float uvPlieHeight = (u.w - u.y) / mBorderPliesNum;

		for (int i = 0; i < mBorderPliesNum; i++)
		{
			//顶点
			{
				float borderOffset = borderPlieWidth * i;
				Vector4 vi = new Vector4(v.x - borderOffset, v.y - borderOffset, v.z + borderOffset, v.w + borderOffset);
				Vector4 vo = new Vector4(vi.x - borderPlieWidth, vi.y - borderPlieWidth, vi.z + borderPlieWidth, vi.w + borderPlieWidth);
				//左
				verts.Add(new Vector3(vi.x, vi.y));
				verts.Add(new Vector3(vo.x, vo.y));
				verts.Add(new Vector3(vo.x, vo.w));
				verts.Add(new Vector3(vi.x, vi.w));
				//上
				verts.Add(new Vector3(vi.x, vi.w));
				verts.Add(new Vector3(vo.x, vo.w));
				verts.Add(new Vector3(vo.z, vo.w));
				verts.Add(new Vector3(vi.z, vi.w));
				//右
				verts.Add(new Vector3(vi.z, vi.w));
				verts.Add(new Vector3(vo.z, vo.w));
				verts.Add(new Vector3(vo.z, vo.y));
				verts.Add(new Vector3(vi.z, vi.y));
				//下
				verts.Add(new Vector3(vi.z, vi.y));
				verts.Add(new Vector3(vo.z, vo.y));
				verts.Add(new Vector3(vo.x, vo.y));
				verts.Add(new Vector3(vi.x, vi.y));
			}
			//UV
			{
				float deltaUX = (u.z - u.x) * 0.5f;
				float deltaUX1 = deltaUX * height / (width + height);
				float deltaUX2 = deltaUX - deltaUX1;
				float uyOffset = uvPlieHeight * i;
				float uy1 = u.y + uyOffset;
				float uy2 = uy1 + uvPlieHeight;
				//左
				float ux1 = u.x;
				float ux2 = ux1 + deltaUX1;
				uvs.Add(new Vector2(ux1, uy1));
				uvs.Add(new Vector2(ux1, uy2));
				uvs.Add(new Vector2(ux2, uy2));
				uvs.Add(new Vector2(ux2, uy1));
				//上
				ux1 = ux2;
				ux2 += deltaUX2;
				uvs.Add(new Vector2(ux1, uy1));
				uvs.Add(new Vector2(ux1, uy2));
				uvs.Add(new Vector2(ux2, uy2));
				uvs.Add(new Vector2(ux2, uy1));
				//右
				ux1 = ux2;
				ux2 += deltaUX1;
				uvs.Add(new Vector2(ux1, uy1));
				uvs.Add(new Vector2(ux1, uy2));
				uvs.Add(new Vector2(ux2, uy2));
				uvs.Add(new Vector2(ux2, uy1));
				//下
				ux1 = ux2;
				ux2 += deltaUX2;
				uvs.Add(new Vector2(ux1, uy1));
				uvs.Add(new Vector2(ux1, uy2));
				uvs.Add(new Vector2(ux2, uy2));
				uvs.Add(new Vector2(ux2, uy1));
			}
		}
		//填充颜色
		int count = 48 * mBorderPliesNum;
		for (int i = 0; i < count; i++)
		{
			cols.Add(gc);
		}
	}
}

然后是 UIHollowedTextureInspector.cs,注意要放到Editor文件夹下

//-------------------------------------------------
//            NGUI: Next-Gen UI kit
// Copyright © 2011-2018 Tasharen Entertainment Inc
// Create By LZ
//-------------------------------------------------

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;

/// 
/// Inspector class used to edit UIHollowedTexture.
/// 

[CanEditMultipleObjects]
[CustomEditor(typeof(UIHollowedTexture), true)]
public class UIHollowedTextureInspector : UIBasicSpriteEditor
{
	UIHollowedTexture mTex;

	protected override void OnEnable ()
	{
		base.OnEnable();
		mTex = target as UIHollowedTexture;
	}

	protected override bool ShouldDrawProperties ()
	{
		if (target == null) return false;
		SerializedProperty sp = NGUIEditorTools.DrawProperty("Texture", serializedObject, "mTexture");
		NGUIEditorTools.DrawProperty("Material", serializedObject, "mMat");

		if (sp != null) NGUISettings.texture = sp.objectReferenceValue as Texture;

		if (mTex != null && (mTex.material == null || serializedObject.isEditingMultipleObjects))
		{
			NGUIEditorTools.DrawProperty("Shader", serializedObject, "mShader");
		}

		EditorGUI.BeginDisabledGroup(mTex == null || mTex.mainTexture == null || serializedObject.isEditingMultipleObjects);

		NGUIEditorTools.DrawRectProperty("UV Rect", serializedObject, "mRect");

		sp = serializedObject.FindProperty("mFixedAspect");
		bool before = sp.boolValue;
		NGUIEditorTools.DrawProperty("Fixed Aspect", sp);
		if (sp.boolValue != before) (target as UIWidget).drawRegion = new Vector4(0f, 0f, 1f, 1f);

		if (sp.boolValue)
		{
			EditorGUILayout.HelpBox("Note that Fixed Aspect mode is not compatible with Draw Region modifications done by sliders and progress bars.", MessageType.Info);
		}

		EditorGUI.EndDisabledGroup();

		//额外属性
		GUILayout.Space(10);
		NGUIEditorTools.DrawProperty("边框宽度", serializedObject, "mBorderWidth");
		NGUIEditorTools.DrawProperty("边框圈数", serializedObject, "mBorderPliesNum");
		EditorGUILayout.BeginHorizontal();
		NGUIEditorTools.DrawProperty("UV横滚动速度", serializedObject, "mUvAnimSpeedH");
		NGUIEditorTools.DrawProperty("UV竖滚动速度", serializedObject, "mUvAnimSpeedV");
		EditorGUILayout.EndHorizontal();
		NGUIEditorTools.DrawProperty("忽略时间缩放", serializedObject, "mUvAnimIgnoreTimeScale");
		GUILayout.Space(4);

		return true;
	}

	/// 
	/// Allow the texture to be previewed.
	/// 

	public override bool HasPreviewGUI ()
	{
		return (Selection.activeGameObject == null || Selection.gameObjects.Length == 1) &&
			(mTex != null) && (mTex.mainTexture as Texture2D != null);
	}

	/// 
	/// Draw the sprite preview.
	/// 

	public override void OnPreviewGUI (Rect rect, GUIStyle background)
	{
		Texture2D tex = mTex.mainTexture as Texture2D;

		if (tex != null)
		{
			Rect tc = mTex.uvRect;
			tc.xMin *= tex.width;
			tc.xMax *= tex.width;
			tc.yMin *= tex.height;
			tc.yMax *= tex.height;
			NGUIEditorTools.DrawSprite(tex, rect, mTex.color, tc, mTex.border);
		}
	}
}

把上面两个代码放到项目里,就可以添加 “UIHollowedTexture” 这个组件了,用法与UITexture相同,多了几个参数,并且Type参数没用了

NGUI通用边框流光_第6张图片

代码的干货都在Fill方法里,可以对照着基类的Fill方法比较异同。

最后再说一下shader行,如果希望这个边框图像以颜色叠加(特效)的方式显示,那么需要替换shader,最简单的办法是复制NGUI自带的 “Unlit - Transparent Colored”、“Unlit - Transparent Colored 1”、“Unlit - Transparent Colored 2”、“Unlit - Transparent Colored3” 这四个Shader,然后改名为 “Unlit - Transparent Colored Additive” 等,再依次打开内容,第一行先改名,然后找到 "Blend SrcAlpha OneMinusSrcAlpha" 全部替换为 "Blend SrcAlpha One",以后把 “Unlit - Transparent Colored Additive” 拖到 Shader行替换默认的 “Unlit - Transparent Colored”。这样改过的的shader支持NGUI的panel裁切,用在ScrollView里不会出错。

写到这里就算全部写完了,如果觉得有用就给个赞再走吧。

注:本文所用的NGUI版本是3.12.1

你可能感兴趣的:(Unity)