Unity在Canvas上画线(Draw Line)实现

# 前言

目前Unity官方提供的UI扩展包中包含了UILineRenderer组件,本篇实现与UILineRenderer实现一致,主要讲解其基本使用与实现过程。不想看的同学可以直接下载官方扩展包。

代码库:unity-ui-extensionsicon-default.png?t=L892https://bitbucket.org/UnityUIExtensions/unity-ui-extensions/wiki/browse/

# 正文

canvas上的渲染均通过CanvasRenderer,unity-ui扩展包实现了一套ui基础类UIPrimitiveBase,我们要讲的UILineRenderer正是继承于UIPrimitiveBase实现的Canvas上画线功能。

 添加UILineRenderer组件,设置Points值即可根据输入点绘制出线段。Points数组为UILineRenderer子级UI坐标点。下面是UILineRenderer的一个使用范例。

public void DrawLines(List<Vector3> worldPositionList)
{
	//ui摄像机 为了将世界坐标转换为本组件下坐标。
	//注意:这个摄像机与worldPositionList相关联,本例中worldPositionList是被ui摄像机渲染的对象的世界坐标。
	var uiCamera = GameObject.FindWithTag(Tag.GuiCamera).GetComponent<Camera>();
	//UILineRenderer组件
	var lineRenderer = GetComponent<UILineRenderer>();
	//与LineRenderer同级RectTransform
	var myRectTransform = GetComponent<RectTransform>();
	var pointsList = new List<Vector2>();
	for (var i = 0; i < worldPositionList.Count; i++)
	{
		var screenPoint = uiCamera.WorldToScreenPoint(worldPositionList[i]);
		RectTransformUtility.ScreenPointToLocalPointInRectangle(myRectTransform, screenPoint, uiCamera,
			out var localPoint);
		pointsList.Add(localPoint);
	}
    //设置坐标,触发UILineRenderer上的CanvasRenderer重绘
	lineRenderer.Points = pointsList.ToArray();
}

## 发生了什么?

### 生成线段

首先本文会忽略不详细讲解Line List,Bezier Mode这两个参数带来的变化,Line List将点列表按照两两成对的方式对待点列表,Bezier Mode会在处理点之前先将点按照贝塞尔曲线方式处理点,形成曲线。我们这里主要讲解一个基础的流程。

在我们设置了坐标点数组后,UILineRenderer调用SetAllDirty方法后回调到我们今天要讲的重点方法OnPopulateMesh,此方法会传入一个VertextHelper类,我们将需要设置的顶点数据塞进这个vh中即可。

 假设有3个点P1,P2,P3,传递给UILineRenderer后,根据这3个点来绘制连线。如果线只有一个像素就很简单,依次将3个点连成线即可,但是UILineRenderer有lineThickness属性,可以设置线的宽度。

private UIVertex[] CreateLineSegment(Vector2 start, Vector2 end, SegmentType type, UIVertex[] previousVert = null)
{
	Vector2 offset = new Vector2((start.y - end.y), end.x - start.x).normalized * lineThickness / 2;

	Vector2 v1 = Vector2.zero;
	Vector2 v2 = Vector2.zero;
	if (previousVert != null) {
		v1 = new Vector2(previousVert[3].position.x, previousVert[3].position.y);
		v2 = new Vector2(previousVert[2].position.x, previousVert[2].position.y);
	} else {
		v1 = start - offset;
		v2 = start + offset;
	}

	var v3 = end + offset;
	var v4 = end - offset;
	//Return the VDO with the correct uvs
	switch (type)
	{
		case SegmentType.Start:
			return SetVbo(new[] { v1, v2, v3, v4 }, startUvs);
		case SegmentType.End:
			return SetVbo(new[] { v1, v2, v3, v4 }, endUvs);
		case SegmentType.Full:
			return SetVbo(new[] { v1, v2, v3, v4 }, fullUvs);
		default:
			return SetVbo(new[] { v1, v2, v3, v4 }, middleUvs);
	}
}

 遍历点数组,每两个点形成一条直线,创建一个由四边形来表示一条线段。这个函数比较难理解地方是如何根据点P1,P2以及线宽来计算出两边的点来的,就是上面offset是如何计算出来的。首先我们将上面的3个点把所有辅助线画出来如下。

start参数即P1点,end参数即P2点。我们最终绘制的线段是a2,a1这两边的线段。offset即P1->a2的偏移*-1或者P1->a1的偏移*1

Vector2 offset = new Vector2((start.y - end.y), end.x - start.x).normalized * lineThickness / 2;

其实上面就是求就是一个P1到P2向量一个旋转矩阵,旋转度数为90度,旋转矩阵为:

 右乘P1->P2的向量即得到(start.y-end.y,end.x-start.x),标准化后乘以线宽除以2即是我们要得到的偏移量。各自2个顶点加减偏移得到4个顶点后放进UIVertex数组,就可以形成一个由两个三角形组成的四边形,顺便提下2个三角形顶点顺序如下:

public void AddUIVertexQuad(UIVertex[] verts)
{
  int currentVertCount = this.currentVertCount;
  for (int index = 0; index < 4; ++index)
	this.AddVert(verts[index].position, verts[index].color, verts[index].uv0, verts[index].uv1, verts[index].normal, verts[index].tangent);
  this.AddTriangle(currentVertCount, currentVertCount + 1, currentVertCount + 2);
  this.AddTriangle(currentVertCount + 2, currentVertCount + 3, currentVertCount);
}

### 处理接缝

所有点处理好后,如果直接绘制线段,我们会发现如下现象:

在P2点会存在接缝的情况,下一步就是处理接缝的逻辑

// Add the line segments to the vertex helper, creating any joins as needed
for (var i = 0; i < segments.Count; i++) {
	if (!lineList && i < segments.Count - 1) {
		var vec1 = segments [i] [1].position - segments [i] [2].position;
		var vec2 = segments [i + 1] [2].position - segments [i + 1] [1].position;
		var angle = Vector2.Angle (vec1, vec2) * Mathf.Deg2Rad;

		// Positive sign means the line is turning in a 'clockwise' direction
		var sign = Mathf.Sign (Vector3.Cross (vec1.normalized, vec2.normalized).z);

		// Calculate the miter point
		var miterDistance = lineThickness / (2 * Mathf.Tan (angle / 2));
		var miterPointA = segments [i] [2].position - vec1.normalized * miterDistance * sign;
		var miterPointB = segments [i] [3].position + vec1.normalized * miterDistance * sign;

		var joinType = LineJoins;
		if (joinType == JoinType.Miter) {
			// Make sure we can make a miter join without too many artifacts.
			if (miterDistance < vec1.magnitude / 2 && miterDistance < vec2.magnitude / 2 && angle > MIN_MITER_JOIN) {
				segments [i] [2].position = miterPointA;
				segments [i] [3].position = miterPointB;
				segments [i + 1] [0].position = miterPointB;
				segments [i + 1] [1].position = miterPointA;
			} else {
				joinType = JoinType.Bevel;
			}
		}

		if (joinType == JoinType.Bevel) {
			if (miterDistance < vec1.magnitude / 2 && miterDistance < vec2.magnitude / 2 && angle > MIN_BEVEL_NICE_JOIN) {
				if (sign < 0) {
					segments [i] [2].position = miterPointA;
					segments [i + 1] [1].position = miterPointA;
				} else {
					segments [i] [3].position = miterPointB;
					segments [i + 1] [0].position = miterPointB;
				}
			}

			var join = new UIVertex[] { segments [i] [2], segments [i] [3], segments [i + 1] [0], segments [i + 1] [1] };
			vh.AddUIVertexQuad (join);
		}
	}

	vh.AddUIVertexQuad (segments [i]);
}

 处理的方式如下图所示:

 代码中的miterDistance即图中H->P2P1的距离,首先求出vec1与vec2如图所示得到角度,根据三角函数就求得距离。两个向量叉乘判断方向来加或者减这个偏移量。

走完这一步就完成了点的处理。

 # 结语

这个组件用起来其实很简单,设置点数组即可,主要对这个偏移计算比较感兴趣,就写成了文章记录一下,用到了旋转矩阵,叉乘判断方向这些数学基础运用到实际开发中。

猜你喜欢

转载自blog.csdn.net/weixin_36719607/article/details/120498039