一种简单的路口网格生成方法(Unity)
1. 前言
最近项目做到了道路生成的这一块。直线道路的生成和道路的shader已经是大体完成了。效果如下。
|
|
|
很明显现在没有路口的形状的。看一看现实的路口就不难发现。
在不考虑右转专用道的情况下(难做),路口大概如下图为红框包含斑马线的部分,蓝框部分就按直线道路生成就好了。
然后我的想法是做一个多角度、多道路、不同宽度都适用的一个方法。
2. 思路
搭建一个简单的场景测试一下。
假设是这样四条道路,首先我要获取不包含斑马线的路口中心的边界。如下红框。
然后向外延申一定距离作为斑马线的网格,如下紫框。
3. 实现
3.1 测试场景的搭建
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
namespace Test
{
[Serializable]
public class TestAttachedNode
{
public Transform transform;
// 该道路的一半路宽
public float halfRoadLength;
}
public class TestGenCrossRoads : MonoBehaviour
{
// 斑马线向外扩展的距离
public float extendLength = 0.5f;
// 路口中心点
public Transform nodeTransforms;
// 周围的道路节点
public List<TestAttachedNode> attachedNodesTransforms;
public bool showCenterLines;
public bool showRoads;
private void OnDrawGizmos()
{
// 绘制中心线
if (showCenterLines)
{
Gizmos.color = Color.white;
foreach (var attachedNode in attachedNodesTransforms)
{
Gizmos.DrawLine(nodeTransforms.position, attachedNode.transform.position);
}
}
// 绘制道路
if (showRoads)
{
// 中心点
var center = nodeTransforms.position;
Gizmos.color = Color.green;
foreach (var attachedNode in attachedNodesTransforms)
{
// 方向
var dir = (attachedNode.transform.position - center).normalized;
// 旋转矩阵
var rotateMat = Matrix4x4.Rotate(Quaternion.Euler(0, 90, 0));
// 逆时针的四个点
Vector3 p1, p2, p3, p4;
p1 = center + (Vector3) (rotateMat * dir) * attachedNode.halfRoadLength;
p2 = attachedNode.transform.position + (Vector3) (rotateMat * dir) * attachedNode.halfRoadLength;
rotateMat = Matrix4x4.Rotate(Quaternion.Euler(0, -90, 0));
p3 = attachedNode.transform.position + (Vector3) (rotateMat * dir) * attachedNode.halfRoadLength;
p4 = center + (Vector3) (rotateMat * dir) * attachedNode.halfRoadLength;
// 绘制
Gizmos.DrawLine(p1, p2);
Gizmos.DrawLine(p2, p3);
Gizmos.DrawLine(p3, p4);
Gizmos.DrawLine(p4, p1);
}
}
}
}
}
这样就可以简单的把道路的样子给可视化出来。
3.2 路口中心的多边形
路口中心的多边形其实就是由每两条相邻道路的边界的交点组成的。
比如上方道路的左侧(我这里将路口center作为起点,往道路的方向延申为前方向,左边界为左侧,有边界为右侧),与左边道路的右侧相交就是第一个顶点。
循环一遍所有道路,得到所有的交点,便是中心多边形的所有顶点。
但是有一个前提是,所有道路的顺序必须是按照顺时针或者逆时针的顺序存储的,不然得到的交点不是我们想要的。因为我前面生成道路信息的时候并没有按角度顺序排序,所以要先进行一个排序。
写一个比较函数,把这个点与(0,0,1)所构成的角度作为排序的依据
public class AttachedNodeComparer : IComparer<TestAttachedNode>
{
private Vector3 center;
public AttachedNodeComparer(Vector3 center)
{
this.center = center;
}
// 按照角度顺序比较
public int Compare(TestAttachedNode nodeA, TestAttachedNode nodeB)
{
if (nodeA != null && nodeB != null) // (编辑器提示我这个可能为空,不然一直有那个波浪线好烦人)
{
if (MyMath.AngleOf(Vector3.forward + center, center, nodeA.transform.position) <
MyMath.AngleOf(Vector3.forward + center, center, nodeB.transform.position))
return 1;
else
return -1;
}
return 0;
}
}
然会对道路进行逆时针排序(这里我弄一个新的List是不想影响到原来存储的数据)
var attachedNodes = new List<TestAttachedNode>();
foreach (var node in attachedNodesTransforms) attachedNodes.Add(node);
// 按角度排序
attachedNodes.Sort(new AttachedNodeComparer(center));
其中AngleOf
是我弄得一个计算角度的函数,如下。
/// <summary>
/// 计算三个点所构成的角度
/// </summary>
/// <param name="p1">点1</param>
/// <param name="p2">点2</param>
/// <param name="p3">点3</param>
/// <param name="chooseMinAngle">是否取最小角度,否则是逆时针计算的正角度</param>
/// <returns></returns>
public static float AngleOf(Vector3 p1, Vector3 p2, Vector3 p3, bool chooseMinAngle = false)
{
// 计算该点的两个向量
var vec1 = p1 - p2;
var vec2 = p3 - p2;
float angle;
if (chooseMinAngle)
{
// 计算角度
angle = Vector3.Angle(vec1, vec2);
}
else
{
// 计算角度
angle = Vector3.SignedAngle(vec1, vec2, Vector3.up);
// 将负的角度改成正数
if (angle < 0) angle = 360 + angle;
}
return angle;
}
定义public List<Vector3> crossPolygon = new List<Vector3>();
用来存储交点结果。
然后就可以用循环来计算所有的交点了。
crossPolygon.Clear();
var attachedNodes = new List<TestAttachedNode>();
foreach (var node in attachedNodesTransforms) attachedNodes.Add(node);
// 中心点
var center = nodeTransforms.position;
// 按角度排序
attachedNodes.Sort(new AttachedNodeComparer());
// 定义旋转矩阵
var rotateMat1 = Matrix4x4.Rotate(Quaternion.Euler(0, -90, 0));
var rotateMat2 = Matrix4x4.Rotate(Quaternion.Euler(0, 90, 0));
// 遍历每一个相邻点
for (var i = 0; i < attachedNodes.Count; i++)
{
// 方向
var dir1 = (attachedNodes[i].transform.position - center).normalized;
// 偏移的两个点
var a1 = center + (Vector3) (rotateMat1 * dir1) * attachedNodes[i].halfRoadLength;
var a2 = attachedNodes[i].transform.position + (Vector3) (rotateMat1 * dir1) * attachedNodes[i].halfRoadLength;
// 方向
var dir2 = (attachedNodes[(i+1)%attachedNodes.Count].transform.position - center).normalized;
// 偏移的两个点
var b1 = center + (Vector3) (rotateMat2 * dir2) *
attachedNodes[(i + 1) % attachedNodes.Count].halfRoadLength;
var b2 = attachedNodes[(i + 1) % attachedNodes.Count].transform.position +
(Vector3) (rotateMat2 * dir2) * attachedNodes[(i + 1) % attachedNodes.Count].halfRoadLength;
crossPolygon.Add(MyMath.Intersection(a1, a2, b1, b2));
}
dir1
是当前道路,由路口中心向道路延申的方向。dir2
是下一条道路由路口中心向道路延申的方向。
然后通过旋转矩阵向左旋转90度,再加上道路一般的长度,就可以得到当前道路的左侧的两个点a1
、a2
。
同理下一条道路得到右侧两个点b1
、b2
。
然后计算a1-a2
和b1-b2
两条直线(不是线段)的交点即可。
Intersection
也是封装的一个函数,如下。
/// <summary>
/// 返回两条直线的交点(在xOz平面上的交点)
/// </summary>
/// <param name="aBegin"></param>
/// <param name="aEnd"></param>
/// <param name="bBegin"></param>
/// <param name="bEnd"></param>
/// <returns></returns>
public static Vector3 Intersection(Vector3 aBegin, Vector3 aEnd, Vector3 bBegin, Vector3 bEnd)
{
// 结果
float x, z;
// 第一条边的k和b
var k1 = (aEnd.z - aBegin.z) / (aEnd.x - aBegin.x);
var b1 = aBegin.z - k1 * aBegin.x;
// 第二条边的k和b
var k2 = (bEnd.z - bBegin.z) / (bEnd.x - bBegin.x);
var b2 = bBegin.z - k2 * bBegin.x;
// 计算交点
// TODO 考虑斜率为0和无穷的情况(既与坐标轴平行或垂直)。我觉得着写法也许可以优化,
if (float.IsPositiveInfinity(k1) || float.IsNegativeInfinity(k1))
{
x = aBegin.x;
z = k2 * x + b2;
}
else if (float.IsPositiveInfinity(k2) || float.IsNegativeInfinity(k2))
{
x = bBegin.x;
z = k1 * x + b1;
}
else if (k1 == 0)
{
z = aBegin.z;
if (float.IsPositiveInfinity(k2) || float.IsNegativeInfinity(k2))
x = bBegin.x;
else
x = (z - b2) / k2;
}
else if (k2 == 0)
{
z = bBegin.z;
if (float.IsPositiveInfinity(k1) || float.IsNegativeInfinity(k1))
x = aBegin.x;
else
x = (z - b1) / k1;
}
else
{
x = (b1 - b2) / (k2 - k1);
z = (b1 - b2) / (k2 - k1) * k1 + b1;
}
return new Vector3(x, aBegin.y, z);
}
然后可以把计算出的结果可视化一下。
// 绘制路口中心多边形
if (showCrossCenter && crossPolygon.Count != 0)
{
Gizmos.color = Color.red;
for (var i = 0; i < crossPolygon.Count; i++)
{
Gizmos.DrawLine(crossPolygon[i], crossPolygon[(i + 1) % crossPolygon.Count]);
}
}
3.3 斑马线部分的扩展
这个也不难,因为已知中心多边形的信息和道路的方向,只要在多边形点上,向道路方向延申一定长度就可以计算到另外两个顶点。
定义public List<List<Vector3>> extendPolygons = new List<List<Vector3>>();
用来存储扩展的每一个四边形(这个可以确定是四边形)
extendPolygons.Clear();
// 遍历每一个相邻点
for (var i = 0; i < attachedNodes.Count; i++)
{
// 方向
var dir = (attachedNodes[i].transform.position - center).normalized;
var p1 = crossPolygon[i];
var p4 = crossPolygon[(i - 1 < 0 ? crossPolygon.Count - 1 : i - 1)];
var p2 = p1 + dir * extendLength;
var p3 = p4 + dir * extendLength;
var tempPolygon = new List<Vector3>
{
p1, p2, p3, p4
};
extendPolygons.Add(tempPolygon);
}
p1
相当于是上图中左侧红点,p4
是右侧红点,p2
是左侧黄点,p3
是右侧黄点。
可视化一下。
// 绘制扩展矩形
if (showCrossExtend && extendPolygons.Count != 0)
{
Gizmos.color = Color.magenta;
foreach (var polygon in extendPolygons)
{
for (var i = 0; i < polygon.Count; i++)
{
Gizmos.DrawLine(polygon[i], polygon[(i + 1) % polygon.Count]);
}
}
}
4. 测试效果
测试部分完整代码。
更新:更新后添加了水平道路的检测和处理,和前面的代码有点不一样。
using UnityEngine;
using UnityEditor;
using MyTools;
using System;
using System.Collections.Generic;
namespace Test
{
[Serializable]
public class TestAttachedNode
{
public Transform transform = null;
public Vector3 Coord {
get
{
if (transform == null) return _coord;
else return transform.position;
}
set => _coord = value;
}
public bool temp = false;
private Vector3 _coord;
public float halfRoadLength;
}
public class TestGenCrossRoads : MonoBehaviour
{
public float extendLength = 0.5f;
public Transform nodeTransforms;
public List<TestAttachedNode> attachedNodesTransforms;
public List<Vector3> crossPolygon = new List<Vector3>();
public List<List<Vector3>> extendPolygons = new List<List<Vector3>>();
public bool showCenterLines;
public bool showRoads;
public bool showCrossCenter;
public bool showCrossExtend;
private void OnEnable() { SceneView.duringSceneGui += OnSceneGUI; }
private void OnDestroy() { SceneView.duringSceneGui -= OnSceneGUI; }
private void OnSceneGUI(SceneView sceneView)
{
sceneView.Repaint();
}
public void Run()
{
crossPolygon.Clear();
extendPolygons.Clear();
var attachedNodes = new List<TestAttachedNode>();
foreach (var node in attachedNodesTransforms) attachedNodes.Add(node);
// 中心点
var center = nodeTransforms.position;
// 按角度排序
attachedNodes.Sort(new AttachedNodeComparer(center));
// 旋转矩阵
var rotateMat1 = Matrix4x4.Rotate(Quaternion.Euler(0, -90, 0));
var rotateMat2 = Matrix4x4.Rotate(Quaternion.Euler(0, 90, 0));
// 处理相邻平行的情况
// 遍历每一个相邻点
for (var i = 0; i < attachedNodes.Count; i++)
{
// 方向1
var dir1 = (attachedNodes[i].Coord - center).normalized;
// 方向2
var dir2 = (attachedNodes[(i+1)%attachedNodes.Count].Coord - center).normalized;
// 是平行 进行处理
if (dir1 == -dir2)
// if (MyMath.AngleOf(dir1,Vector3.zero, dir2, true) > 160)
{
var tempNode = new TestAttachedNode();
// 标记为临时
tempNode.temp = true;
// 宽度为前一个道路的宽度
tempNode.halfRoadLength = attachedNodes[i - 1 < 0 ? attachedNodes.Count - 1 : i - 1].halfRoadLength;
// 新点
tempNode.Coord = center + (Vector3) (rotateMat1 * dir1);
attachedNodes.Insert(i + 1, tempNode);
}
}
var nodeIsTemp = new List<bool>();
// 遍历每一个相邻点
for (var i = 0; i < attachedNodes.Count; i++)
{
// 方向
var dir1 = (attachedNodes[i].Coord - center).normalized;
// 偏移的两个点
var a1 = center + (Vector3) (rotateMat1 * dir1) * attachedNodes[i].halfRoadLength;
var a2 = attachedNodes[i].Coord + (Vector3) (rotateMat1 * dir1) * attachedNodes[i].halfRoadLength;
// 方向
var dir2 = (attachedNodes[(i+1)%attachedNodes.Count].Coord - center).normalized;
// 偏移的两个点
var b1 = center + (Vector3) (rotateMat2 * dir2) *
attachedNodes[(i + 1) % attachedNodes.Count].halfRoadLength;
var b2 = attachedNodes[(i + 1) % attachedNodes.Count].Coord +
(Vector3) (rotateMat2 * dir2) * attachedNodes[(i + 1) % attachedNodes.Count].halfRoadLength;
crossPolygon.Add(MyMath.Intersection(a1, a2, b1, b2));
nodeIsTemp.Add(attachedNodes[i].temp);
}
// 遍历每一个相邻点
for (var i = 0; i < attachedNodes.Count; i++)
{
if(nodeIsTemp[i]) continue;
// 方向
var dir = (attachedNodes[i].Coord - center).normalized;
var p1 = crossPolygon[i];
var p4 = crossPolygon[(i - 1 < 0 ? crossPolygon.Count - 1 : i - 1)];
var p2 = p1 + dir * extendLength;
var p3 = p4 + dir * extendLength;
var tempPolygon = new List<Vector3>
{
p1, p2, p3, p4
};
extendPolygons.Add(tempPolygon);
}
SceneView.RepaintAll();
}
private void OnDrawGizmos()
{
// 绘制中心线
if (showCenterLines)
{
Gizmos.color = Color.white;
foreach (var attachedNode in attachedNodesTransforms)
{
Gizmos.DrawLine(nodeTransforms.position, attachedNode.transform.position);
}
}
// 绘制道路
if (showRoads)
{
// 中心点
var center = nodeTransforms.position;
Gizmos.color = Color.green;
foreach (var attachedNode in attachedNodesTransforms)
{
// 方向
var dir = (attachedNode.transform.position - center).normalized;
// 旋转矩阵
var rotateMat = Matrix4x4.Rotate(Quaternion.Euler(0, 90, 0));
// 逆时针的四个点
Vector3 p1, p2, p3, p4;
p1 = center + (Vector3) (rotateMat * dir) * attachedNode.halfRoadLength;
p2 = attachedNode.transform.position + (Vector3) (rotateMat * dir) * attachedNode.halfRoadLength;
rotateMat = Matrix4x4.Rotate(Quaternion.Euler(0, -90, 0));
p3 = attachedNode.transform.position + (Vector3) (rotateMat * dir) * attachedNode.halfRoadLength;
p4 = center + (Vector3) (rotateMat * dir) * attachedNode.halfRoadLength;
// 绘制
Gizmos.DrawLine(p1, p2);
Gizmos.DrawLine(p2, p3);
Gizmos.DrawLine(p3, p4);
Gizmos.DrawLine(p4, p1);
}
}
// 绘制路口中心多边形
if (showCrossCenter && crossPolygon.Count != 0)
{
Gizmos.color = Color.red;
for (var i = 0; i < crossPolygon.Count; i++)
{
Gizmos.DrawLine(crossPolygon[i], crossPolygon[(i + 1) % crossPolygon.Count]);
}
}
// 绘制扩展矩形
if (showCrossExtend && extendPolygons.Count != 0)
{
Gizmos.color = Color.magenta;
foreach (var polygon in extendPolygons)
{
for (var i = 0; i < polygon.Count; i++)
{
Gizmos.DrawLine(polygon[i], polygon[(i + 1) % polygon.Count]);
}
}
}
}
}
public class AttachedNodeComparer : IComparer<TestAttachedNode>
{
private Vector3 center;
public AttachedNodeComparer(Vector3 center)
{
this.center = center;
}
// 按照角度顺序比较
public int Compare(TestAttachedNode nodeA, TestAttachedNode nodeB)
{
if (nodeA != null && nodeB != null) // (编辑器提示我这个可能为空,不然一直有那个波浪线好烦人)
{
if (MyMath.AngleOf(Vector3.forward + center, center, nodeA.transform.position) <
MyMath.AngleOf(Vector3.forward + center, center, nodeB.transform.position))
return 1;
else
return -1;
}
return 0;
}
}
}
5. 网格生成
顶点有了,网格的话大体分为两部分,一部分是外面的所有扩展的斑马线部分,一个是中心部分。
扩展部分都是单独的四边形,了解一下Mesh生成的基础知识就可以很简单的生成出来。
中心部分不一定是四边形,如果是三条道路就是三角形,四条道路是四边形,五条道路就是五边形,以此类推。可以直接用多边形三角化算法计算网格所需的三角形。(比如之前写过的一篇多边形的网格生成)
甚至可以给扩展部分顶点给上特定的UV,用来显示斑马线。中心部分就用世界坐标采样显示贴图就好了。这部分我也还没做。
做完了应用到项目上就OK。
不过下一步,直线道路的生成也要做相应的修改,使其能够和路口对接上。
更新:最新发现,当两条相邻道路是水平的时候,交点求不到。有待解决。已解决,通过添加临时道路的方式。
补一下生成网格之后的效果
|
|
|