最近个人项目中写了一个ugui的画线组件,水一篇博客算鸟。
现在正在做一个项目是纯二维的,我就直接用的ugui去写的,因为本身我已经四年没怎么做ui了,虽然市面上的gui插件都比ugui好用,组件丰富,功能齐全,但是我都没用过也不太会用。所以我就需要什么实现什么算了,下面就来说一下ugui中line的实现方式之一。
我们知道三维引擎中一切渲染皆是几何数据:点线面纹理材质等,ui也一样,都是世界空间中的几何数据。本身unity提供的linerenderer和tailrenderer可以在世界空间画线,那么我们仿写一个ugui版本的就行了,如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UguiTestMeshGraphic : Graphic
{
public float width = 100;
public float height = 10;
[Range(-50, 50)]
public float diff = 50;
protected override void OnPopulateMesh(VertexHelper vh)
{
base.OnPopulateMesh(vh);
vh.Clear();
UIVertex vertex = new UIVertex();
vertex.position = new Vector3(0, 0, 0);
vh.AddVert(vertex);
vertex = new UIVertex();
vertex.position = new Vector3(0, height, 0);
vh.AddVert(vertex);
vertex = new UIVertex();
vertex.position = new Vector3(width, height + diff, 0);
vh.AddVert(vertex);
vertex = new UIVertex();
vertex.position = new Vector3(width, diff, 0);
vh.AddVert(vertex);
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(0, 2, 3);
}
}
直接通过UI.Graphic的OnPopulateMesh函数即可完成对UIMesh的绘制,比我们自己去实现顶点栅格化等图形流水线操作方便多了(我们之前聊过栅格化绘制线段和多边形的做法)。和创建Mesh一样,只需要将顶点坐标、三角拓扑、uvs等参数赋值,再写一个着色器渲染,就ok了,如下:
虽然线段在数学上就是一个公式,没有宽度,但是在图形学里它是有宽度的,也就是一个矩形,我们上面绘制的就可以认为是一个线段(虽然两个端的坐标是错误的)。那么继续扩展线段绘制功能,最好支持多坐标的连线,跟linerenderer一样,如下:
可以看出来一个问题,那就是我们用等高垂点计算法,则B2P模长小于B2B1,则中间BC线段会比起始AB线段“窄”。那么我们还得思考等宽线段绘制的解决方法,其实也不难解决,首先我们假定线段AB和BC的宽度相同,那么它们就存在两个交点B1B2,如下:
可以看得出来正确的B1和B2应该是线段(矩形)AB和BC的两条平行边的交点,那么问题就转化为求直线和直线相交的问题,首先让我们求出线段(矩形)AB的两条平行边,如下:
在二维平面上计算平行线A1B1和A2B2就相对简单。例如求A1和A2坐标,只需要根据A3点顺/逆时针旋转90度就得到了(B1和B2同理),那么实现以下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UguiTestLineGraphic : Graphic
{
[Range(0, 20)]
public float width = 10f;
public Vector2 from;
public Vector2 to;
public RectTransform rectA;
public RectTransform rectB;
protected override void OnPopulateMesh(VertexHelper vh)
{
base.OnPopulateMesh(vh);
Vector2 a = from;
Vector2 b = to;
rectA.anchoredPosition = a;
rectB.anchoredPosition = b;
Vector2[] a1a2 = CalculateVerticalPoints(a, a, b, width);
Vector2[] b1b2 = CalculateVerticalPoints(b, a, b, width);
Vector2 a1 = a1a2[0];
Vector2 a2 = a1a2[1];
Vector2 b1 = b1b2[0];
Vector2 b2 = b1b2[1];
vh.Clear();
UIVertex vertex = new UIVertex();
vertex.position = a2;
vh.AddVert(vertex);
vertex = new UIVertex();
vertex.position = a1;
vh.AddVert(vertex);
vertex = new UIVertex();
vertex.position = b1;
vh.AddVert(vertex);
vertex = new UIVertex();
vertex.position = b2;
vh.AddVert(vertex);
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(0, 2, 3);
}
///
/// 计算A1和A2
/// po代表pa或者pb
/// 和本来的pa,pb分开为了通用
///
///
///
///
///
///
private Vector2[] CalculateVerticalPoints(Vector2 po, Vector2 pa, Vector2 pb, float wid)
{
//先计算出a3
float halfwid = wid / 2f;
Vector2 npapb = (pb - pa).normalized;
Vector2 pa3 = npapb * halfwid;
//再根据矩阵旋转计算出a1,a2
Vector2 pa1 = po + CalculatePointRotate(pa3, 90);
Vector2 pa2 = po + CalculatePointRotate(pa3, 270);
#if UNITY_EDITOR
Debug.LogFormat("po = {0} pa3 = {1} pa1 = {2} pa2 = {3}", po, pa3, pa1, pa2);
#endif
return new Vector2[] { pa1, pa2 };
}
///
/// 计算f旋转ang角度后的t
///
///
///
///
private Vector2 CalculatePointRotate(Vector2 f, float ang)
{
float rad = ang * Mathf.Deg2Rad;
Matrix2x2 m2x2 = new Matrix2x2();
m2x2.m00 = Mathf.Cos(rad);
m2x2.m01 = -Mathf.Sin(rad);
m2x2.m10 = Mathf.Sin(rad);
m2x2.m11 = -Mathf.Cos(rad);
Vector2 t = m2x2 * f;
return t;
}
public class Matrix2x2
{
public float m00;
public float m01;
public float m10;
public float m11;
public static Vector2 operator *(Matrix2x2 m2x2, Vector2 v2)
{
float x = m2x2.m00 * v2.x + m2x2.m01 * v2.y;
float y = m2x2.m10 * v2.x + m2x2.m11 * v2.y;
return new Vector2(x, y);
}
}
}
代码涉及的矩阵运算和向量运算以前已经讲过很多了,不再补充原理说明,如不清楚的同学可以回到我之前博客阅读理解。只需计算出A1A2和B1B2构建UIMesh即可,效果如下:
这样我们就绘制出了UILine,当然我们需要的功能支持顶点列表线段绘制,类似linerenderer的positions参数,我们继续改造,如下:
上图标注了两条线段(矩形)“上下边”的“交点”,同时我们需要处理直线相交的问题:
而且还要处理多个线段矩形(或多边形)组成的拓扑结构:
接下来才能开始写代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UguiLineGraphic : Graphic
{
[Range(0, 20)]
public float width = 10f;
public Vector2[] positions;
protected override void OnPopulateMesh(VertexHelper vh)
{
base.OnPopulateMesh(vh);
if (positions == null || positions.Length < 2)
{
return;
}
vh.Clear();
//如果只有两个顶点
//直接生成直线即可
if (positions.Length == 2)
{
Vector2 a = positions[0];
Vector2 b = positions[1];
Vector2[] a1a2 = CalculateVerticalPoints(a, a, b, width);
Vector2[] b1b2 = CalculateVerticalPoints(b, a, b, width);
Vector2 a1 = a1a2[0];
Vector2 a2 = a1a2[1];
Vector2 b1 = b1b2[0];
Vector2 b2 = b1b2[1];
UIVertex vertex = new UIVertex();
vertex.position = a2;
vh.AddVert(vertex);
vertex = new UIVertex();
vertex.position = a1;
vh.AddVert(vertex);
vertex = new UIVertex();
vertex.position = b1;
vh.AddVert(vertex);
vertex = new UIVertex();
vertex.position = b2;
vh.AddVert(vertex);
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(0, 2, 3);
}
else
{
//如果是多顶点组成的矩形数组
//储存A1A2/B1B2...Z1Z2
List<Vector2[]> ptslist = new List<Vector2[]>();
//先储存A1A2
{
Vector2[] a1a2 = CalculateVerticalPoints(positions[0], positions[0], positions[1], width);
ptslist.Add(a1a2);
}
//再储存B1B2...Y1Y2
{
for (int i = 1; i < positions.Length - 1; i++)
{
Vector2 a = positions[i - 1];
Vector2 b = positions[i];
Vector2 c = positions[i + 1];
Vector2 d = positions[i]; //d=c
Vector2[] a1a2 = CalculateVerticalPoints(a, a, b, width);
Vector2[] b1b2 = CalculateVerticalPoints(b, a, b, width);
Vector2[] c1c2 = CalculateVerticalPoints(c, d, c, width);
Vector2[] d1d2 = CalculateVerticalPoints(d, d, c, width);
Vector2 a1 = a1a2[0];
Vector2 a2 = a1a2[1];
Vector2 b1 = b1b2[0];
Vector2 b2 = b1b2[1];
Vector2 c1 = c1c2[0];
Vector2 c2 = c1c2[1];
Vector2 d1 = d1d2[0];
Vector2 d2 = d1d2[1];
//如果线段AB和DC平行
if (IsVector2Approximiate(b1, d1) && IsVector2Approximiate(b2, d2))
{
//任意储存b1b2或d1d2
ptslist.Add(b1b2);
}
//如果线段AB和DC相交
else
{
Vector2 crossb1 = CalculateLineCross(a1, b1, c1, d1);
Vector2 crossb2 = CalculateLineCross(a2, b2, c2, d2);
//储存交点b1b2
ptslist.Add(new Vector2[] { crossb1, crossb2 });
}
}
}
//最后储存Z1Z2
{
Vector2[] z1z2 = CalculateVerticalPoints(positions[positions.Length - 1], positions[positions.Length - 2], positions[positions.Length - 1], width);
ptslist.Add(z1z2);
}
//再来构建网格
int ptcount = ptslist.Count * 2;
for (int i = 0; i < ptcount; i++)
{
int firstindex = i % ptslist.Count;
int secondindex = i / ptslist.Count;
UIVertex vertex = new UIVertex();
vertex.position = ptslist[firstindex][secondindex];
vh.AddVert(vertex);
}
int quadcount = ptslist.Count - 1;
for (int i = 0; i < quadcount; i++)
{
//quad左上角顺时针
int topleftindex = i;
int bottomleftindex = i + ptslist.Count;
vh.AddTriangle(topleftindex, topleftindex + 1, bottomleftindex);
vh.AddTriangle(topleftindex + 1, bottomleftindex + 1, bottomleftindex);
}
}
}
///
/// 判断两vector2坐标相近
///
///
///
///
private bool IsVector2Approximiate(Vector2 a, Vector2 b)
{
if (Mathf.Approximately(a.x, b.x) && Mathf.Approximately(a.y, b.y))
{
return true;
}
return false;
}
///
/// 计算两射线交点
///
///
///
///
///
///
private Vector2 CalculateLineCross(Vector2 p1, Vector2 p2, Vector2 p4, Vector2 p3)
{
//构建直线参数k和a
float k1 = (p2.y - p1.y) / (p2.x - p1.x);
float a1 = p2.y - k1 * p2.x;
float k2 = (p3.y - p4.y) / (p3.x - p4.x);
float a2 = p3.y - k2 * p3.x;
//根据求解计算交点
float y = (k2 * a1 - k1 * a2) / (k2 - k1);
float x = 0;
if (k1 != 0)
{
x = (y - a1) / k1;
}
else if (k2 != 0)
{
x = (y - a2) / k2;
}
return new Vector2(x, y);
}
///
/// 计算A1和A2
/// po代表pa或者pb
/// 和本来的pa,pb分开为了通用
///
///
///
///
///
///
private Vector2[] CalculateVerticalPoints(Vector2 po, Vector2 pa, Vector2 pb, float wid)
{
//先计算出a3
float halfwid = wid / 2f;
Vector2 npapb = (pb - pa).normalized;
Vector2 pa3 = npapb * halfwid;
//再根据矩阵旋转计算出a1,a2
Vector2 pa1 = po + CalculatePointRotate(pa3, 90);
Vector2 pa2 = po + CalculatePointRotate(pa3, 270);
#if UNITY_EDITOR
Debug.LogFormat("po = {0} pa3 = {1} pa1 = {2} pa2 = {3}", po, pa3, pa1, pa2);
#endif
return new Vector2[] { pa1, pa2 };
}
///
/// 计算f旋转ang角度后的t
///
///
///
///
private Vector2 CalculatePointRotate(Vector2 f, float ang)
{
float rad = ang * Mathf.Deg2Rad;
Matrix2x2 m2x2 = new Matrix2x2();
m2x2.m00 = Mathf.Cos(rad);
m2x2.m01 = -Mathf.Sin(rad);
m2x2.m10 = Mathf.Sin(rad);
m2x2.m11 = -Mathf.Cos(rad);
Vector2 t = m2x2 * f;
return t;
}
public class Matrix2x2
{
public float m00;
public float m01;
public float m10;
public float m11;
public static Vector2 operator *(Matrix2x2 m2x2, Vector2 v2)
{
float x = m2x2.m00 * v2.x + m2x2.m01 * v2.y;
float y = m2x2.m10 * v2.x + m2x2.m11 * v2.y;
return new Vector2(x, y);
}
}
}
效果如下:
这样我们就将多节点线段绘制出来了,下面我们测试一下,比如写个“波浪线”:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UguiTestWaveLine : MonoBehaviour
{
[Range(0, 20)]
public float width = 5;
[Range(0, 20)]
public float height = 5;
[Range(10, 200)]
public int count = 100;
[Range(1, 20)]
public int wave = 10;
private UguiLineGraphic lineGraph;
public bool isRebuild = true;
void Start()
{
lineGraph = GetComponent<UguiLineGraphic>();
}
private void Update()
{
if (isRebuild)
{
Vector2[] poses = new Vector2[count];
for (int i = 0; i < count; i++)
{
Vector2 pos = new Vector2(width * i, Mathf.Sin((float)i / (float)wave * 360f * Mathf.Deg2Rad) * height);
poses[i] = pos;
}
lineGraph.positions = poses;
lineGraph.OnRebuildRequested();
isRebuild = false;
}
}
}
效果如下:
这样我们就完成了ui line的绘制,当然这里只是讲解绘制的原理,并不做特殊的图形渲染效果,如果有需要后面我再凑一篇讲解。