一种简单的路口网格生成方法(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度,再加上道路一般的长度,就可以得到当前道路的左侧的两个点a1a2
同理下一条道路得到右侧两个点b1b2

然后计算a1-a2b1-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。

不过下一步,直线道路的生成也要做相应的修改,使其能够和路口对接上。


更新:最新发现,当两条相邻道路是水平的时候,交点求不到。有待解决。已解决,通过添加临时道路的方式。


补一下生成网格之后的效果

道路加上材质后的效果
路口加上材质后的效果
道路Mesh
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值