UI层的矩形按钮、矩形头像,很多时候都需要有个高亮的边框来点缀。最近本人写了一个扩展版的UI边框,跟使用UITexture一样方便,得到了特效师的高度评价,便拿出来献丑分享。效果如下:
小新的头像是一个UITexture,外面的边框是一个叫做UIHollowedTexture的UI组件(拿UITexture魔改而来),可以设置边框贴图,可以UV滚动。本文接下来会介绍UIHollowedTexture的做法。
对于正常的UITexture来说,其位置和尺寸决定了它的4个边角顶点的位置,然后根据这4个顶点绘制2个三角形,合成一个矩形图案;而对于UIHollowedTexture来说,得到4个边角点后,再根据我们需要的边框宽度,外拓得到4个外顶点,然后把每一侧的2个内点和2个外点绘制成一个四边形,如下图所示:
绘制顶点的问题解决了,然后是决定UV的问题,如下图所示,8个顶点分别对应整张贴图的指定位置,UV依据该图分配即可,注意我把图横向拉长了。
如果就这么做的话,显示上会有一个小问题,大家观察一下2367组成的四边形,它是个梯形,而我们贴在梯形上的贴图是个矩形。结果会导致贴图变形,原因如下图
假设有一张方形贴图,它有一条左上角到右下角的斜线,贴到矩形面片里是正常的(图1),而如果贴到梯形里(为了简化问题我用直角梯形说明),我最初以为会是图2那样,结果却会变成图3。因为一个四边形是2个三角形组成的,如果我们把一个矩形的右下角顶点朝右边拉一段距离,结果是左上角的三角形没有任何改变,而右下角的三角形发生了变形,导致斜线的斜率发生了变化,结果就是一条直线变成了折线。(为了能浅显解释这个问题我想了好久)。
我为这个显示问题请教了美术同学,他们表示可以通过把大梯形切割成多个小梯形来优化效果
如图所示,把梯形拆成上下堆叠的小梯形,这条斜线是不是平滑了一些?如果切的够细,就可以把它变成一条平滑的弧线,这样看起来就不突兀了。
上面把一些原理性的东西讲完了,接下来是具体代码:
脚本 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参数没用了
代码的干货都在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