曲面UI_切角边框流光效果_案例分享
相关续篇
这篇【Unity的HDRP渲染管线下实现好用的GUI模糊和外描边流光效果_Blur_OutLine_案例分享(内附源码)】的外描边流光效果
- 效果如图:
效果图如下:
- 切角曲面边框流光效果
- 如下图:
Canvas 节点上的CurvedUISettings组件:
- 如图,在
Canvas
画布节点上添加CurvedUISettings组件;
是在
Canvas
节点上
CurvedUISettings组件
CurvedUISettings代码如下:
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections.Generic;
#if CURVEDUI_TMP
using TMPro;
#endif
/// <summary>
/// This class stores settings for the entire canvas. It also stores useful methods for converting cooridinates to and from 2d canvas to curved canvas, or world space.
/// CurvedUIVertexEffect components (added to every canvas gameobject)ask this class for per-canvas settings when applying their curve effect.
/// </summary>
namespace CurvedUI
{
[AddComponentMenu("CurvedUI/CurvedUISettings")]
[RequireComponent(typeof(Canvas))]
public class CurvedUISettings : MonoBehaviour
{
#region SETTINGS
[SerializeField]
float quality = 1f;
//Cyllinder settings
[SerializeField]
int angle = 90;
[SerializeField]
bool preserveAspect = true;
//internal system settings
int baseCircleSegments = 24;
//support variables
Vector2 savedRectSize;
float savedRadius;
Canvas myCanvas;
#endregion
#region LIFECYCLE
void Start()
{
//find needed references
if (myCanvas == null)
myCanvas = GetComponent<Canvas>();
savedRadius = GetCyllinderRadiusInCanvasSpace();
}
void OnEnable()
{
//Redraw canvas object on enable.
foreach (UnityEngine.UI.Graphic graph in (this).GetComponentsInChildren<UnityEngine.UI.Graphic>())
{
graph.SetAllDirty();
}
}
void OnDisable()
{
foreach (UnityEngine.UI.Graphic graph in (this).GetComponentsInChildren<UnityEngine.UI.Graphic>())
{
graph.SetAllDirty();
}
}
void Update()
{
//recreate the geometry if entire canvas has been resized
if ((transform as RectTransform).rect.size != savedRectSize)
{
savedRectSize = (transform as RectTransform).rect.size;
SetUIAngle(angle);
}
//check for improper canvas size
if (savedRectSize.x == 0 || savedRectSize.y == 0)
Debug.LogError("CurvedUI: Your Canvas size must be bigger than 0!");
}
#endregion
#region PRIVATE
/// <summary>
/// Changes the horizontal angle of the canvas.
/// </summary>
/// <param name="newAngle"></param>
void SetUIAngle(int newAngle)
{
if (myCanvas == null)
myCanvas = GetComponent<Canvas>();
//temp fix to make interactions with angle 0 possible
if (newAngle == 0) newAngle = 1;
angle = newAngle;
savedRadius = GetCyllinderRadiusInCanvasSpace();
foreach (CurvedUIVertexEffect ve in GetComponentsInChildren<CurvedUIVertexEffect>())
ve.TesselationRequired = true;
foreach (Graphic graph in GetComponentsInChildren<Graphic>())
graph.SetVerticesDirty();
}
Vector3 CanvasToCyllinder(Vector3 pos)
{
float theta = (pos.x / savedRectSize.x) * Angle * Mathf.Deg2Rad;
pos.x = Mathf.Sin(theta) * (SavedRadius + pos.z);
pos.z += Mathf.Cos(theta) * (SavedRadius + pos.z) - (SavedRadius + pos.z);
return pos;
}
Vector3 CanvasToCyllinderVertical(Vector3 pos)
{
float theta = (pos.y / savedRectSize.y) * Angle * Mathf.Deg2Rad;
pos.y = Mathf.Sin(theta) * (SavedRadius + pos.z);
pos.z += Mathf.Cos(theta) * (SavedRadius + pos.z) - (SavedRadius + pos.z);
return pos;
}
#endregion
#region PUBLIC
/// <summary>
/// Adds the CurvedUIVertexEffect component to every child gameobject that requires it.
/// CurvedUIVertexEffect creates the curving effect.
/// </summary>
public void AddEffectToChildren()
{
foreach (UnityEngine.UI.Graphic graph in GetComponentsInChildren<UnityEngine.UI.Graphic>(true))
{
if (graph.GetComponent<CurvedUIVertexEffect>() == null)
{
graph.gameObject.AddComponent<CurvedUIVertexEffect>();
graph.SetAllDirty();
}
}
//TextMeshPro experimental support. Go to CurvedUITMP.cs to learn how to enable it.
#if CURVEDUI_TMP
foreach(TextMeshProUGUI tmp in GetComponentsInChildren<TextMeshProUGUI>(true)){
if(tmp.GetComponent<CurvedUITMP>() == null){
tmp.gameObject.AddComponent<CurvedUITMP>();
tmp.SetAllDirty();
}
}
#endif
}
/// <summary>
/// Maps a world space vector to a curved canvas.
/// Operates in Canvas's local space.
/// </summary>
/// <param name="pos">World space vector</param>
/// <returns>
/// A vector on curved canvas in canvas's local space
/// </returns>
public Vector3 VertexPositionToCurvedCanvas(Vector3 pos)
{
return CanvasToCyllinder(pos);
}
/// <summary>
/// Converts a point in Canvas space to a point on Curved surface in world space units.
/// </summary>
/// <param name="pos">Position on canvas in canvas space</param>
/// <returns>
/// Position on curved canvas in world space.
/// </returns>
public Vector3 CanvasToCurvedCanvas(Vector3 pos)
{
pos = VertexPositionToCurvedCanvas(pos);
if (float.IsNaN(pos.x) || float.IsInfinity(pos.x)) return Vector3.zero;
else return transform.localToWorldMatrix.MultiplyPoint3x4(pos);
}
/// <summary>
/// Returns a normal direction on curved canvas for a given point on flat canvas. Works in canvas' local space.
/// </summary>
/// <param name="pos"></param>
/// <returns></returns>
public Vector3 CanvasToCurvedCanvasNormal(Vector3 pos)
{
//find the position in canvas space
pos = VertexPositionToCurvedCanvas(pos);
// find the direction to the center of cyllinder on flat XZ plane
return transform.localToWorldMatrix.MultiplyVector((pos - new Vector3(0, 0, -GetCyllinderRadiusInCanvasSpace())).ModifyY(0)).normalized;
}
/// <summary>
/// Returns the radius of curved canvas cyllinder, expressed in Cavas's local space units.
/// </summary>
public float GetCyllinderRadiusInCanvasSpace()
{
float ret;
if (PreserveAspect)
{
ret = ((transform as RectTransform).rect.size.x / ((2 * Mathf.PI) * (angle / 360.0f)));
}
else
ret = ((transform as RectTransform).rect.size.x * 0.5f) / Mathf.Sin(Mathf.Clamp(angle, -180.0f, 180.0f) * 0.5f * Mathf.Deg2Rad);
return angle == 0 ? 0 : ret;
}
/// <summary>
/// Tells you how big UI quads can get before they should be tesselate to look good on current canvas settings.
/// Used by CurvedUIVertexEffect to determine how many quads need to be created for each graphic.
/// </summary>
public Vector2 GetTesslationSize(bool UnmodifiedByQuality = false)
{
Vector2 canvasSize = GetComponent<RectTransform>().rect.size;
float ret = canvasSize.x;
float ret2 = canvasSize.y;
if (Angle != 0)
{
ret = Mathf.Min(canvasSize.x / 4, canvasSize.x / (Mathf.Abs(angle).Remap(0.0f, 360.0f, 0, 1) * baseCircleSegments));
ret2 = Mathf.Min(canvasSize.y / 4, canvasSize.y / (Mathf.Abs(angle).Remap(0.0f, 360.0f, 0, 1) * baseCircleSegments));
}
return new Vector2(ret, ret2) / (UnmodifiedByQuality ? 1 : Mathf.Clamp(Quality, 0.01f, 10.0f));
}
/// <summary>
/// The measure of the arc of the Canvas.
/// </summary>
public int Angle
{
get { return angle; }
set
{
if (angle != value)
SetUIAngle(value);
}
}
/// <summary>
/// Multiplier used to deremine how many segments a base curve of a shape has.
/// Default 1. Lower values greatly increase performance. Higher values give you sharper curve.
/// </summary>
public float Quality
{
get { return quality; }
set
{
if (quality != value)
{
quality = value;
SetUIAngle(angle);
}
}
}
/// <summary>
/// Calculated radius of the curved canvas.
/// </summary>
public float SavedRadius
{
get
{
if (savedRadius == 0)
savedRadius = GetCyllinderRadiusInCanvasSpace();
return savedRadius;
}
}
/// <summary>
/// If enabled, CurvedUI will try to preserve aspect ratio of original canvas.
/// </summary>
public bool PreserveAspect
{
get { return preserveAspect; }
set
{
if (preserveAspect != value)
{
preserveAspect = value;
SetUIAngle(angle);
}
}
}
#endregion
}
}
CurvedUISettings代码注释:
代码方法 | 注释 |
---|---|
属性定义 ([SerializeField] ) | 这些属性被标记为可序列化,可以在Unity编辑器中直接被设置。 |
生命周期方法 (Start , OnEnable , OnDisable , Update ) | 在Unity的生命周期中被调用,用于初始化、启用/禁用时的更新和每帧的更新。 |
私有方法 (SetUIAngle , CanvasToCyllinder , CanvasToCyllinderVertical ) | 用于设置UI角度和将画布坐标转换为圆柱坐标。 |
公共方法 (AddEffectToChildren , VertexPositionToCurvedCanvas , CanvasToCurvedCanvas , CanvasToCurvedCanvasNormal , GetCyllinderRadiusInCanvasSpace , GetTesslationSize ) | 提供了对外部访问类的属性和功能的方式,例如添加效果到子对象、转换坐标、获取圆柱半径和细分大小。 |
属性访问器 (Angle , Quality , SavedRadius , PreserveAspect ) | 这些属性提供了对类内部字段的控制和访问,允许在Unity编辑器中直接修改这些值。 |
TMP集成 (#if CURVEDUI_TMP ) | 这部分代码检查是否定义了CURVEDUI_TMP 预处理器指令,如果是,则包含额外的TextMeshPro支持。 |
Canvas组件要求 ([RequireComponent(typeof(Canvas))] ) | 这个类需要附加到具有Canvas组件的游戏对象上。 |
组件菜单 ([AddComponentMenu("CurvedUI/CurvedUISettings")] ) | 这允许用户通过Unity编辑器的组件菜单添加这个脚本。 |
错误检查 | 在Update 方法中,如果画布尺寸为0,会输出错误信息。 |
CurvedUIVertexEffect
- 在每一个需要UI弯曲效果的节点上都添加CurvedUIVertexEffect节点
CurvedUIVertexEffect组件
CurvedUIVertexEffect代码如下:
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
#if CURVEDUI_TMP
using TMPro;
#endif
namespace CurvedUI
{
#if UNITY_5_1
///Pre 5.2 Unity uses BaseVertexEffect class, works on quads.
public partial class CurvedUIVertexEffect : BaseVertexEffect {
public override void ModifyVertices(List<UIVertex> verts) {
if (!this.IsActive())
return;
if (mySettings == null) {
FindParentSettings();
}
if (mySettings == null || !mySettings.enabled)
return;
if (tesselationRequired || curvingRequired || savedCurvedVerts == null || savedCurvedVerts.Count == 0) {
ModifyVerts(verts);
savedCurvedVerts = new List<UIVertex>(verts);
curvingRequired = false;
}
int initialCount = verts.Count;
verts.AddRange(savedCurvedVerts);
verts.RemoveRange(0, initialCount);
}
//pre 5.2 specific variables
List<UIVertex> savedCurvedVerts;
#else
///Post 5.2 Unity uses BaseMeshEffect class, which works on triangles.
///We need to convert those to quads to be used with tesselation routine.
public partial class CurvedUIVertexEffect : BaseMeshEffect
{
#if UNITY_5_2_0 || UNITY_5_2_1 // this method used different arguments pre unity 5.2.2
public override void ModifyMesh (Mesh mesh) {
VertexHelper vh = new VertexHelper(mesh);
#else
public override void ModifyMesh(VertexHelper vh)
{
#endif
if (!this.IsActive())
return;
if (mySettings == null)
{
FindParentSettings();
}
if (mySettings == null || !mySettings.enabled)
return;
//check for changes in text font material that would mean a retesselation in required to get fresh UV's
CheckTextFontMaterial();
//if curving or tesselation is required, we'll run the code to calculate vertices.
if (tesselationRequired || curvingRequired || SavedVertexHelper == null || SavedVertexHelper.currentVertCount == 0)
{
//Debug.Log("updating: tes:" + tesselationRequired + ", crv:" + curvingRequired, this.gameObject);
//Get vertices from the vertex stream. These come as triangles.
SavedVerteees = new List<UIVertex>();
vh.GetUIVertexStream(SavedVerteees);
// calls the old ModifyVertices which was used on pre 5.2.
ModifyVerts(SavedVerteees);
//create or reuse our temp vertexhelper
if (SavedVertexHelper == null)
SavedVertexHelper = new VertexHelper();
else
{
#if UNITY_5_2_0 || UNITY_5_2_1
SavedVertexHelper = new VertexHelper();
#else
SavedVertexHelper.Clear();
#endif
}
//Save our tesselated and curved vertices to new vertex helper. They can come as quads or as triangles.
if (SavedVerteees.Count % 4 == 0)
{
for (int i = 0; i < SavedVerteees.Count; i += 4)
{
SavedVertexHelper.AddUIVertexQuad(new UIVertex[]{
SavedVerteees[i + 0], SavedVerteees[i + 1], SavedVerteees[i + 2], SavedVerteees[i + 3],
});
}
}
else
{
SavedVertexHelper.AddUIVertexTriangleStream(SavedVerteees);
}
//download proper vertex stream to a list we're going to save
SavedVertexHelper.GetUIVertexStream(SavedVerteees);
curvingRequired = false;
}
//copy the saved verts list to current VertexHelper
#if UNITY_5_2_0 || UNITY_5_2_1
vh = new VertexHelper();
vh.AddUIVertexTriangleStream(SavedVerteees);
vh.FillMesh(mesh);
#else
vh.Clear();
vh.AddUIVertexTriangleStream(SavedVerteees);
#endif
}
//Post 5.2 specific variables
VertexHelper SavedVertexHelper; //used int 5.2 and later
List<UIVertex> SavedVerteees;
#endif
#region SAVED VARIABLES
//public settings
[Tooltip("Check to skip tesselation pass on this object. CurvedUI will not create additional vertices to make this object have a smoother curve. Checking this can solve some issues if you create your own procedural mesh for this object. Default false.")]
public bool DoNotTesselate = false;
//settings
bool tesselationRequired = true;
bool curvingRequired = true;
//saved variables and references
float angle = 90;
bool TransformMisaligned = false;
Canvas myCanvas;
CurvedUISettings mySettings;
//internal
Matrix4x4 CanvasToWorld;
Matrix4x4 CanvasToLocal;
Matrix4x4 MyToWorld;
Matrix4x4 MyToLocal;
[SerializeField][HideInInspector] Vector3 savedPos;
[SerializeField][HideInInspector] Vector3 savedUp;
[SerializeField][HideInInspector] Vector2 savedRectSize;
[SerializeField][HideInInspector] Color savedColor;
[SerializeField][HideInInspector] Vector4 savedTextUV0;
[SerializeField][HideInInspector] float savedFill;
List<UIVertex> tesselatedVerts;
//my components references
Graphic myGraphic;
Image myImage;
Text myText;
#if CURVEDUI_TMP
TextMeshProUGUI myTMP;
#endif
#endregion
#region LIFECYCLE
protected override void OnEnable()
{
//find the settings object and its canvas.
FindParentSettings();
//If there is an update to the graphic, we cant reuse old vertices, so new tesselation will be required
myGraphic = GetComponent<Graphic>();
if (myGraphic)
{
myGraphic.RegisterDirtyMaterialCallback(TesselationRequiredCallback);
myGraphic.SetVerticesDirty();
}
myText = GetComponent<Text>();
if (myText)
{
myText.RegisterDirtyVerticesCallback(TesselationRequiredCallback);
Font.textureRebuilt += FontTextureRebuiltCallback;
}
#if CURVEDUI_TMP
myTMP = GetComponent<TextMeshProUGUI>();
#endif
}
protected override void OnDisable()
{
//If there is an update to the graphic, we cant reuse old vertices, so new tesselation will be required
if (myGraphic)
myGraphic.UnregisterDirtyMaterialCallback(TesselationRequiredCallback);
if (myText)
{
myText.UnregisterDirtyVerticesCallback(TesselationRequiredCallback);
Font.textureRebuilt -= FontTextureRebuiltCallback;
}
}
/// <summary>
/// Subscribed to graphic componenet to find out when vertex information changes and we need to create new geometry based on that.
/// </summary>
void TesselationRequiredCallback()
{
tesselationRequired = true;
}
/// <summary>
/// Called by Font class to let us know font atlas has ben rebuilt and we need to update our vertices.
/// </summary>
void FontTextureRebuiltCallback(Font fontie)
{
if (myText.font == fontie)
tesselationRequired = true;
}
void Update()
{
#if CURVEDUI_TMP // CurvedUITMP handles updates for TextMeshPro objects.
if (myTMP) return;
#endif
//Find if the change in transform requires us to retesselate the UI
if (!tesselationRequired)
{ // do not perform tesselation required check if we already know it is, god damnit!
if ((transform as RectTransform).rect.size != savedRectSize)
{
//the size of this RectTransform has changed, we have to tesselate again! =(
//Debug.Log("tess required - size");
tesselationRequired = true;
}
else if (myGraphic != null)//test for color changes if it has a graphic component
{
if (myGraphic.color != savedColor)
{
tesselationRequired = true;
savedColor = myGraphic.color;
//Debug.Log("tess req - color");
}
else if (myImage != null)
{
if (myImage.fillAmount != savedFill)
{
tesselationRequired = true;
savedFill = myImage.fillAmount;
}
}
}
}
if (!tesselationRequired && !curvingRequired) // do not perform a check if we're already tesselating or curving. Tesselation includes curving.
{
//test if position in canvas's local space has been changed. We would need to recalculate vertices again
Vector3 testedPos = mySettings.transform.worldToLocalMatrix.MultiplyPoint3x4(this.transform.position);
if (!testedPos.AlmostEqual(savedPos))
{
}
//test this object's rotation in relation to canvas.
Vector3 testedUp = mySettings.transform.worldToLocalMatrix.MultiplyVector(this.transform.up).normalized;
if (!savedUp.AlmostEqual(testedUp, 0.0001))
{
bool testedEqual = testedUp.AlmostEqual(Vector3.up.normalized);
bool savedEqual = savedUp.AlmostEqual(Vector3.up.normalized);
//special case - if we change the z angle from or to 0, we need to retesselate to properly display geometry in cyllinder
if ((!testedEqual && savedEqual) || (testedEqual && !savedEqual))
tesselationRequired = true;
savedUp = testedUp;
curvingRequired = true;
//Debug.Log("crv req - tested up: " + testedUp);
}
}
////if we find we need to make a change in the mesh, set vertices dirty to trigger BaseMeshEffect firing.
if (myGraphic && (tesselationRequired || curvingRequired)) myGraphic.SetVerticesDirty();
}
#endregion
#region CHECKS
void CheckTextFontMaterial()
{
//we check for a sudden change in text's fontMaterialTexture. This is a very hacky way, but the only one working reliably for now.
if (myText)
{
if (myText.cachedTextGenerator.verts.Count > 0 && myText.cachedTextGenerator.verts[0].uv0 != savedTextUV0)
{
//Debug.Log("tess req - texture");
savedTextUV0 = myText.cachedTextGenerator.verts[0].uv0;
tesselationRequired = true;
}
}
}
public CurvedUISettings FindParentSettings(bool forceNew = false)
{
if (mySettings == null || forceNew)
{
mySettings = GetComponentInParent<CurvedUISettings>();
if (mySettings == null) return null;
else
{
myCanvas = mySettings.GetComponent<Canvas>();
angle = mySettings.Angle;
myImage = GetComponent<Image>();
}
}
return mySettings;
}
#endregion
#region VERTEX OPERATIONS
void ModifyVerts(List<UIVertex> verts)
{
if (verts == null || verts.Count == 0) return;
//update transformation matrices we're going to use in curving the verts.
CanvasToWorld = myCanvas.transform.localToWorldMatrix;
CanvasToLocal = myCanvas.transform.worldToLocalMatrix;
MyToWorld = transform.localToWorldMatrix;
MyToLocal = transform.worldToLocalMatrix;
//tesselate the vertices if needed and save them to a list,
//so we don't have to retesselate if RectTransform's size has not changed.
if (tesselationRequired || !Application.isPlaying)
{
TesselateGeometry(verts);
// Save the tesselated vertices, so if the size does not change,
// we can use them when redrawing vertices.
tesselatedVerts = new List<UIVertex>(verts);
//save the transform properties we last tesselated for.
savedRectSize = (transform as RectTransform).rect.size;
tesselationRequired = false;
}
//lets get some values needed for curving from settings
angle = mySettings.Angle;
float radius = mySettings.GetCyllinderRadiusInCanvasSpace();
Vector2 canvasSize = (myCanvas.transform as RectTransform).rect.size;
int initialCount = verts.Count;
if (tesselatedVerts != null)
{ // use saved verts if we have those
UIVertex[] copiedVerts = new UIVertex[tesselatedVerts.Count];
for (int i = 0; i < tesselatedVerts.Count; i++)
{
copiedVerts[i] = CurveVertex(tesselatedVerts[i], angle, radius, canvasSize);
}
verts.AddRange(copiedVerts);
verts.RemoveRange(0, initialCount);
}
else
{ // or just the mesh's vertices if we do not
UIVertex[] copiedVerts = new UIVertex[verts.Count];
for (int i = 0; i < initialCount; i++)
{
copiedVerts[i] = CurveVertex(verts[i], angle, radius, canvasSize);
}
verts.AddRange(copiedVerts);
verts.RemoveRange(0, initialCount);
}
}
#endregion
#region CURVING
/// <summary>
/// Map position of a vertex to a section of a circle. calculated in canvas's local space
/// </summary>
UIVertex CurveVertex(UIVertex input, float cylinder_angle, float radius, Vector2 canvasSize)
{
Vector3 pos = input.position;
//calculated in canvas local space version:
pos = CanvasToLocal.MultiplyPoint3x4(MyToWorld.MultiplyPoint3x4(pos));
// pos = mySettings.VertexPositionToCurvedCanvas(pos);
if (mySettings.Angle != 0)
{
float theta = (pos.x / canvasSize.x) * cylinder_angle * Mathf.Deg2Rad;
radius += pos.z; // change the radius depending on how far the element is moved in z direction from canvas plane
pos.x = Mathf.Sin(theta) * radius;
pos.z += Mathf.Cos(theta) * radius - radius;
}
//4. write output
input.position = MyToLocal.MultiplyPoint3x4(CanvasToWorld.MultiplyPoint3x4(pos));
return input;
}
#endregion
#region TESSELATION
void TesselateGeometry(List<UIVertex> verts)
{
Vector2 tessellatedSize = mySettings.GetTesslationSize();
//find if we are aligned with canvas main axis
TransformMisaligned = !(savedUp.AlmostEqual(Vector3.up.normalized));
#if !UNITY_5_1 /// Convert the list from triangles to quads to be used by the tesselation
TrisToQuads(verts);
#endif
//do not tesselate text verts. Text usually is small and has plenty of verts already.
#if CURVEDUI_TMP
if (myText == null && myTMP == null) {
#else
if (myText == null && !DoNotTesselate)
{
#endif
// Tesselate quads and apply transformation
int startingVertexCount = verts.Count;
for (int i = 0; i < startingVertexCount; i += 4)
ModifyQuad(verts, i, tessellatedSize);
// Remove old quads
verts.RemoveRange(0, startingVertexCount);
}
}
void ModifyQuad(List<UIVertex> verts, int vertexIndex, Vector2 requiredSize)
{
// Read the existing quad vertices
UIVertex[] quad = new UIVertex[4];
for (int i = 0; i < 4; i++)
quad[i] = verts[vertexIndex + i];
// horizotal and vertical directions of a quad. We're going to tesselate parallel to these.
Vector3 horizontalDir = quad[2].position - quad[1].position;
Vector3 verticalDir = quad[1].position - quad[0].position;
//To make sure filled image is properly tesselated, were going to find the bigger side of the quad.
if (myImage != null && myImage.type == Image.Type.Filled)
{
horizontalDir = (horizontalDir).x > (quad[3].position - quad[0].position).x ? horizontalDir : quad[3].position - quad[0].position;
verticalDir = (verticalDir).y > (quad[2].position - quad[3].position).y ? verticalDir : quad[2].position - quad[3].position;
}
// Find how many quads we need to create
int horizontalQuads = 1;
int verticalQuads = 1;
// Tesselate vertically only if the recttransform (or parent) is rotated
// This cuts down the time needed to tesselate by 90% in some cases.
if (TransformMisaligned)
verticalQuads = Mathf.CeilToInt(verticalDir.magnitude * (1.0f / Mathf.Max(1.0f, requiredSize.y)));
horizontalQuads = Mathf.CeilToInt(horizontalDir.magnitude * (1.0f / Mathf.Max(1.0f, requiredSize.x)));
bool oneVert = false;
bool oneHori = false;
// Create the quads!
float yStart = 0.0f;
for (int y = 0; y < verticalQuads || !oneVert; ++y)
{
oneVert = true;
float yEnd = (y + 1.0f) / verticalQuads;
float xStart = 0.0f;
for (int x = 0; x < horizontalQuads || !oneHori; ++x)
{
oneHori = true;
float xEnd = (x + 1.0f) / horizontalQuads;
//Add new quads to list
verts.Add(TesselateQuad(quad, xStart, yStart));
verts.Add(TesselateQuad(quad, xStart, yEnd));
verts.Add(TesselateQuad(quad, xEnd, yEnd));
verts.Add(TesselateQuad(quad, xEnd, yStart));
//begin the next quad where we ened this one
xStart = xEnd;
}
//begin the next row where we ended this one
yStart = yEnd;
}
}
void TrisToQuads(List<UIVertex> verts)
{
int addCount = 0;
int vertsInTrisCount = verts.Count;
UIVertex[] vertsInQuads = new UIVertex[vertsInTrisCount / 6 * 4];
for (int i = 0; i < vertsInTrisCount; i += 6)
{
// Get four corners from two triangles. Basic UI always comes in quads anyway.
vertsInQuads[addCount++] = (verts[i + 0]);
vertsInQuads[addCount++] = (verts[i + 1]);
vertsInQuads[addCount++] = (verts[i + 2]);
vertsInQuads[addCount++] = (verts[i + 4]);
}
//add quads to the list and remove the triangles
verts.AddRange(vertsInQuads);
verts.RemoveRange(0, vertsInTrisCount);
}
UIVertex TesselateQuad(UIVertex[] quad, float x, float y)
{
UIVertex ret = new UIVertex();
//1. calculate weighting factors
float[] weights = new float[4]{
(1-x) * (1-y),
(1-x) * y,
x * y,
x * (1-y),
};
//2. interpolate all the vertex properties using weighting factors
Vector4 uv0 = Vector2.zero, uv1 = Vector2.zero;
Vector3 pos = Vector3.zero;
for (int i = 0; i < 4; i++)
{
uv0 += quad[i].uv0 * weights[i];
uv1 += quad[i].uv1 * weights[i];
pos += quad[i].position * weights[i];
//normal += quad[i].normal * weights[i]; // normals should be recalculated to take the curve into account;
//tan += quad[i].tangent * weights[i]; // tangents should be recalculated to take the curve into account;
}
//4. return output
ret.position = pos;
//ret.color = Color32.Lerp(Color32.Lerp(quad[3].color, quad[1].color, y), Color32.Lerp(quad[0].color, quad[2].color, y), x);
ret.color = quad[0].color; //used instead to save performance. Color lerps are expensive.
ret.uv0 = uv0;
ret.uv1 = uv1;
ret.normal = quad[0].normal;
ret.tangent = quad[0].tangent;
return ret;
}
#endregion
#region PUBLIC
/// <summary>
/// Force Mesh to be rebuild during canvas' next update loop.
/// </summary>
public void SetDirty()
{
TesselationRequired = true;
}
/// <summary>
/// Force vertices to be tesselated again from original vertices.
/// Set by CurvedUIVertexEffect when updating object's visual property.
/// </summary>
public bool TesselationRequired
{
get { return tesselationRequired; }
set { tesselationRequired = value; }
}
/// <summary>
/// Force vertices to be repositioned on the curved canvas.
/// set by CurvedUIVertexEffect when moving UI objects on canvas.
/// </summary>
public bool CurvingRequired
{
get { return curvingRequired; }
set { curvingRequired = value; }
}
#endregion
}
#region EXTENSION METHODS
public static class CalculationMethods
{
/// <summary>
///Direct Vector3 comparison can produce wrong results sometimes due to float inacuracies.
///This is an aproximate comparison.
/// <returns></returns>
public static bool AlmostEqual(this Vector3 a, Vector3 b, double accuracy = 0.01)
{
return Vector3.SqrMagnitude(a - b) < accuracy;
}
public static float Remap(this float value, float from1, float to1, float from2, float to2)
{
return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
}
public static float RemapAndClamp(this float value, float from1, float to1, float from2, float to2)
{
return value.Remap(from1, to1, from2, to2).Clamp(from2, to2);
}
public static float Remap(this int value, float from1, float to1, float from2, float to2)
{
return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
}
public static double Remap(this double value, double from1, double to1, double from2, double to2)
{
return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
}
public static float Clamp(this float value, float min, float max)
{
return Mathf.Clamp(value, min, max);
}
public static float Clamp(this int value, int min, int max)
{
return Mathf.Clamp(value, min, max);
}
public static int Abs(this int value)
{
return Mathf.Abs(value);
}
public static float Abs(this float value)
{
return Mathf.Abs(value);
}
/// <summary>
/// Returns value rounded to nearest integer (both up and down).
/// </summary>
/// <returns>The int.</returns>
/// <param name="value">Value.</param>
public static int ToInt(this float value)
{
return Mathf.RoundToInt(value);
}
public static int FloorToInt(this float value)
{
return Mathf.FloorToInt(value);
}
public static int CeilToInt(this float value)
{
return Mathf.FloorToInt(value);
}
public static Vector3 ModifyX(this Vector3 trans, float newVal)
{
trans = new Vector3(newVal, trans.y, trans.z);
return trans;
}
public static Vector3 ModifyY(this Vector3 trans, float newVal)
{
trans = new Vector3(trans.x, newVal, trans.z);
return trans;
}
public static Vector3 ModifyZ(this Vector3 trans, float newVal)
{
trans = new Vector3(trans.x, trans.y, newVal);
return trans;
}
public static Vector2 ModifyVectorX(this Vector2 trans, float newVal)
{
trans = new Vector3(newVal, trans.y);
return trans;
}
public static Vector2 ModifyVectorY(this Vector2 trans, float newVal)
{
trans = new Vector3(trans.x, newVal);
return trans;
}
/// <summary>
/// Resets transform's local position, rotation and scale
/// </summary>
/// <param name="trans">Trans.</param>
public static void ResetTransform(this Transform trans)
{
trans.localPosition = Vector3.zero;
trans.localRotation = Quaternion.identity;
trans.localScale = Vector3.one;
}
public static T AddComponentIfMissing<T>(this GameObject go) where T : Component
{
if (go.GetComponent<T>() == null)
{
return go.AddComponent<T>();
}
else return go.GetComponent<T>();
}
/// <summary>
/// Checks if given component is preset and if not, adds it and returns it.
/// </summary>
public static T AddComponentIfMissing<T>(this Component go) where T : Component
{
return go.gameObject.AddComponentIfMissing<T>();
}
#endregion
}
}
CurvedUIVertexEffect代码注释:
代码结构 | 注释 |
---|---|
条件编译指令 (#if UNITY_EDITOR , #if CURVEDUI_TMP ) | 这些指令用于在不同的编译环境下包含不同的代码,例如在Unity编辑器中或当CURVEDUI_TMP 定义时。 |
类定义 (public partial class CurvedUIVertexEffect ) | 这个类是一个部分类(partial class),意味着它可以分布在多个文件中。它继承自BaseVertexEffect (Unity 5.1 之前)或BaseMeshEffect (Unity 5.2 及以后)。 |
生命周期方法 (OnEnable , OnDisable , Update ) | 这些方法在Unity的生命周期中被调用,用于初始化、禁用和每帧更新。 |
顶点操作 (ModifyVerts , CurveVertex , TesselateGeometry ) | 这些方法用于修改UI元素的顶点数据,使其适应曲面UI的效果。 |
扩展方法 (CalculationMethods 类中的方法) | 这些是静态方法,提供了一些辅助数学计算,例如线性映射(Remap )和向量的近似比较(AlmostEqual )。 |
属性和变量: 类中定义了许多公共和私有的属性和变量,用于存储设置、变换矩阵、顶点数据等。 | |
顶点效果处理: | 根据Unity版本,类实现了不同的方法来处理顶点效果。对于Unity 5.1之前的版本,使用BaseVertexEffect 和ModifyVertices 方法;对于Unity 5.2及以后,使用BaseMeshEffect 和ModifyMesh 方法。 |
文本处理: | 类中包含了对Text和TextMeshProUGUI组件的支持,用于处理文本元素的弯曲效果。 |
脏标记 (SetDirty , TesselationRequired , CurvingRequired ) | 提供了方法和属性来标记UI元素需要重新计算顶点。 |
辅助方法: | 类中包含了一些辅助方法,例如FindParentSettings 用于查找父CurvedUISettings 组件,CheckTextFontMaterial 用于检查文本字体材质是否发生变化。 |
最终效果:
切角边框流光效果Shader代码解析
实现思路
- 首先是利用UV实现扇形的遮罩,这也是极坐标常用的计算方法。
- 极坐标扇形遮罩形状如下
- 极坐标扇形遮罩形状如下
- 边框贴图R和B通道:
- 如图:
- 如图:
- 利用这两个通道的边框图,实现切角边缘光流光效果。
- 利用两个图的辉光强度不同,做lerp插值,实现边缘光宽度调整功能
Shader属性
- Shader属性代码如下:
...
Properties
{
[Header(ShineFlow)]
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_MaskTex ("Mask Texture", 2D) = "white" {}
[HDR]_ShineColor("Shine Color", Color) = (1,1,1,1)
_WidthShine ("Shine Soft", Range(0, 1)) = 0.5
_sharpening ("Shine sharpening", Range(0, 1)) = 0.5
_Speed("Shine Speed 速度", Range(-20,20)) = 1
[Space(20)]
[Header(Rectangle)]
_linewidth("Line Width描边宽度", Range(0, 1)) = 0.003
[HideInInspector] _StencilComp ("Stencil Comparison", Float) = 8
[HideInInspector] _Stencil ("Stencil ID", Float) = 0
[HideInInspector] _StencilOp ("Stencil Operation", Float) = 0
[HideInInspector] _StencilWriteMask ("Stencil Write Mask", Float) = 255
[HideInInspector] _StencilReadMask ("Stencil Read Mask", Float) = 255
[HideInInspector] _ColorMask ("Color Mask", Float) = 15
}
...
在片段着色器中输出
- 通过返回某一计算结果的值得到的画面效果
atan2(uv.y, uv.x)
- 将UV通过乘2减1操作;可将UV在单位0到1的二维平面中的位置从左下角移到中心位置。
- 片段着色器中的代码如下:
...
half4 frag(v2f i) : SV_Target
{
half2 centerUV =(2.0*i.uv-1);
float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
return shinenumber;
}
ENDCG
...
- atan2(y,x) 求出的θ取值范围是 ( − π , π )
float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126)
- shinenumber 的范围值在0到2之间
atan2(centerUV.g, centerUV.r)的效果如下图:
加入UV旋转矩阵
- 代码如下:
...
half4 frag(v2f i) : SV_Target
{
half2 centerUV =(2.0*i.uv-1);
float time = _Time.y * _Speed;
float2x2 RotationMatrix = float2x2
(
cos(time), -sin(time),
sin(time),cos(time)
);
centerUV = mul(RotationMatrix,centerUV);
float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
return shinenumber;
}
ENDCG
...
利用smoothstep实现扇形遮罩
- 代码如下:
...
half4 frag(v2f i) : SV_Target
{
half2 centerUV =(2.0*i.uv-1);
float time = _Time.y * _Speed;
float2x2 RotationMatrix = float2x2
(
cos(time), -sin(time),
sin(time),cos(time)
);
centerUV = mul(RotationMatrix,centerUV);
float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
half widthshine = smoothstep(_WidthShine,1,saturate(shinenumber));
return widthshine;
}
ENDCG
...
- atan2(centerUV.g, centerUV.r) 求出的centerUV取值范围是 ( − π , π ),
- abs(atan2(centerUV.g, centerUV.r) / 3.14126)求出的centerUV取值范围是 ( −1 , 1 )
- 1减去范围值( −1 , 1 ),就是(0,2)的范围值
- 利用smoothstep的特性,将上述的范围值限制到(_WidthShine,1)的范围内,这样在画面中显示的有缩放扇形宽度的功能。
- smoothstep结构详解如下:
float smoothstep (float min, float max, float x)
{
float t = saturate ((x - min) / (max - min));
return t * t * (3.0 - (2.0 * t));
}
如果 x 比min 小,返回 0;如果 x 比max 大,返回 1
如果 x 处于范围 [min,max]中,则返回 0 和 1 之间的值,按值在min和max间的比例平滑过渡。
- 补充:线性过渡可以直接使用saturate((x - min)/(max - min))
扇形遮罩效果如下图
采样边框贴图R和B通道:
- 如图:
- 利用通道的不同,做描边粗细区分。
片段着色输出代码如下:
...
half4 frag(v2f i) : SV_Target
{
half2 centerUV =(2.0*i.uv-1);
float time = _Time.y * _Speed;
float2x2 RotationMatrix = float2x2
(
cos(time), -sin(time),
sin(time),cos(time)
);
centerUV = mul(RotationMatrix,centerUV);
float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
half widthshine = smoothstep(_WidthShine,1,saturate(shinenumber));
half sharpflow = smoothstep(0,abs(_sharpening-1),widthshine);
// fixed4 maincolor = tex2D(_MainTex,i.uv);
fixed4 maskcolor = i.color * tex2D(_MaskTex,i.uv);
float small = maskcolor.r;
float big = maskcolor.b;
float3 linecolor = lerp(small,big,_linewidth) * _ShineColor.rgb;
float3 shine = linecolor * sharpflow;
return float4 (shine.rgb,1);
}
...
- 采样_MaskTex,也就是上面展示的R和B通道贴图。
- 利用lerp实现线框宽度从细变粗的插值计算
- 代码如下:
float3 linecolor = lerp(small,big,_linewidth) * _ShineColor.rgb;
整合扇形遮罩和mask贴图
- 将上述得出的扇形遮罩(主要效果是做循环流光)和mask贴图进行相乘操作
- 效果如图:
Curved_Shine.Shader文件
Shader完整代码如下:
Shader"xukaibo/GUI/Curved_Shine"
{
Properties
{
[Header(ShineFlow)]
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_MaskTex ("Mask Texture", 2D) = "white" {}
[HDR]_ShineColor("Shine Color", Color) = (1,1,1,1)
_WidthShine ("Shine Soft", Range(0, 1)) = 0.5
_sharpening ("Shine sharpening", Range(0, 1)) = 0.5
_Speed("Shine Speed 速度", Range(-20,20)) = 1
[Space(20)]
[Header(Rectangle)]
_linewidth("Line Width描边宽度", Range(0, 1)) = 0.003
[HideInInspector] _StencilComp ("Stencil Comparison", Float) = 8
[HideInInspector] _Stencil ("Stencil ID", Float) = 0
[HideInInspector] _StencilOp ("Stencil Operation", Float) = 0
[HideInInspector] _StencilWriteMask ("Stencil Write Mask", Float) = 255
[HideInInspector] _StencilReadMask ("Stencil Read Mask", Float) = 255
[HideInInspector] _ColorMask ("Color Mask", Float) = 15
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Lighting Off
Cull Off
ZTest Always
ZWrite Off
Blend One One
ColorMask [_ColorMask]
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// #pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
sampler2D _MaskTex,_MainTex;
half4 _ShineColor;
float _Speed,_WidthShine ,_sharpening,_linewidth;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
fixed4 color : COLOR;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
UNITY_VERTEX_OUTPUT_STEREO
};
v2f vert(appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.color = v.color;
return o;
}
half4 frag(v2f i) : SV_Target
{
half2 centerUV =(2.0*i.uv-1);
float time = _Time.y * _Speed;
float2x2 RotationMatrix = float2x2
(
cos(time), -sin(time),
sin(time),cos(time)
);
centerUV = mul(RotationMatrix,centerUV);
float shinenumber = 1 - abs(atan2(centerUV.g, centerUV.r) / 3.14126);
half widthshine = smoothstep(_WidthShine,1,saturate(shinenumber));
half sharpflow = smoothstep(0,abs(_sharpening-1),widthshine);
fixed4 maskcolor = i.color * tex2D(_MaskTex,i.uv);
float small = maskcolor.r;
float big = maskcolor.b;
float3 linecolor = lerp(small,big,_linewidth) * _ShineColor.rgb;
float3 shine = linecolor * sharpflow;
return float4 (shine.rgb,1);
}
ENDCG
}
}
}
通过网盘分享的文件:CurvedUI.unitypackage
链接: https://pan.baidu.com/s/1Q_ga8EoZPI1LEXbXIXV0Qg?pwd=6urn 提取码: 6urn
–来自百度网盘超级会员v6的分享