Unity解析OpenDRIVE地圖數據,並生成路網模型

一、引言

前置知識:

Unity解析OSM數據,並生成簡單模型

這次參考了兩個開源項目:

第二個項目比較好,是直接用的OpenDRIVE原本的格式。第一個項目用的數據經過處理,直接看不出來是怎麼解析源數據的,生成模型的方法也許可以看看。

以下內容都是對OpenDrive-Unity-Renderer的解讀。本次使用Unity解析OpenDRIVE生成路網的模型如下:

在這裏插入圖片描述
有路網和建築模型,車跑上去效果還行,但是交叉口沒有處理,還是模型重疊的狀態。

二、OpenDrive概述

OpenDRIVE ®是一個開放的文件格式,用於道路網絡的邏輯描述。它是由一組仿真專業人員開發並維護的,並得到了仿真行業的大力支持。它的首次公開露面是在2006年1月31日。

1.爲什麼要使用OpenDRIVE ®?

OpenDRIVE ®是獨立於供應商,並且可以免費使用
OpenDRIVE ®包含了所有的主要功能路網
OpenDRIVE ®是一個具有廣泛的國際用戶羣
OpenDRIVE ®是一個管理良好的格式,發展過程透明

OpenDRIVE被開發出來是爲了創建一種標準的地圖數據格式,方便在各種駕駛仿真模擬器中進行數據交換。

2. OpenDRIVE的特徵:

  • XML格式
  • 層次結構
  • 道路幾何形狀的解析定義:(平面元素,橫向/垂直輪廓,車道寬度等)
  • 各種類型的車道
  • 連接點和連接點組
  • 通道的邏輯互連
  • 標誌和信號,包括 依存關係
  • 信號控制器(例如用於路口)
  • 路面特性(另請參見OpenCRG)
  • 道路和路邊物體
  • 用戶可定義的data beads
  • 等等

3. OSM和OpenDRIVE的比較:

可以看到,OpenDRIVE也是一種XML文件,是一種矢量地圖。只不過,相比OSM地圖,它包含的信息更多,結構也更復雜。

在這裏插入圖片描述
在這裏插入圖片描述
4.文件下載

這裏可以下載OpenDRIVE的文件規範,對每個節點、每個節點的每個屬性都做了詳細的解釋。還可以下載示例的OpenDRIVE數據。

三、OpenDrive重要節點介紹

這是我做的一個XML節點和屬性的導圖。“【】”表示這個節點一般有多個。

在這裏插入圖片描述
在OpenDRIVE中,所有的道路都由一條定義基本幾何圖形(弧線,直線等)的reference line組成。沿着reference line,可以定義道路的各種屬性。例如:高程輪廓線、交通標誌等。

在這裏插入圖片描述

可以在思維導圖中看到,road節點是重點,其中geometry節點就定義了reference line,而lane節點重點定義了各車道的屬性。

在這裏插入圖片描述
通過指定與reference line的橫向距離來創建單獨的車道。reference line通過連接clothoids(又名歐拉螺旋)或多項式來構建。

在這裏插入圖片描述

請注意圓弧段和直線是clothoids的特殊情況。使用clothoids的優點是,沿着reference line的曲率隨路徑長度線性變化,這就是爲什麼大多數道路都是由clothoids構造的。
在這裏插入圖片描述
三種線的曲率變化:

在這裏插入圖片描述

1.建模用到的主要節點及屬性

在項目中直接把用到的屬性劃個重點,接下來我們主要就看看這些屬性是什麼意思。

在這裏插入圖片描述

  • geometry.x

  • geometry.y

  • geometry.length

  • geometry.hdg

  • geometry.arc.curvature

  • laneSection.right.lane[i].width.a

2.geometry節點

一連串道路 geometry的節點在x/y平面(plan view)上定義了道路reference line的layout。這些geometry節點必須按照升序排列(i.e. increasing s-position). 一個子節點包含了具體的geometric元素的數據。OpenDRIVE現在支持五種geometric元素:

  • straight lines
  • spirals
  • arcs
  • cubic polynomials
  • parametric cubic polynomials

geometry節點的屬性:在這裏插入圖片描述
這裏的s-coordinate是指:

在這裏插入圖片描述

inertial是指:

在這裏插入圖片描述

我們看一個具體的例子:

在這裏插入圖片描述
這裏的reference line的類型是line,並且起始點的座標是(x,y)=(512.5,-2250),它的heading朝向的弧度約是1.57,line的長度爲583。我們知道了道路參考線的起點、朝向、長度就可以確定這個參考線的位置了。

3.lane的width節點

lanes節點由多個laneSection節點組成。若不定義新的lane section節點,它定義的數值就始終有效,適用於接下來的road(Each lane section is valid until the next lane section is defined)。所以每條road至少有一個從s=0.0m起始的lane section。

在這裏插入圖片描述
一個lane section至少包含left/center/right三種節點中的一個。lane節點被包含在left/center/right節點中。車道用數字ID來區分,這些ID有如下特點:

  • 唯一
  • 連續 (i.e. without gaps),
  • starting from 0 on the reference line
  • 向左側遞增 (positive t-direction)
  • 向右側遞減 (negative t-direction)

每條road的lane的數量是不限的。 reference line被定義爲 lane zero,且不允許有width節點,因爲它的寬度總爲0。
在這裏插入圖片描述
除了center裏面的lane,其他lane至少有一個width節點。和lane section一樣,如果不定義下一個width節點的話,它定義的數值就一直適用於接下來的lane。如果一個lane有多個width節點,它們必須按照升序排列。

width節點的屬性如下:

在這裏插入圖片描述

看看具體數據:

在這裏插入圖片描述

上述的lane的width節點中,只有屬性“a”的值不爲0。觀察了下整個xml文件,發現幾乎所有的width節點的屬性都一樣,只有a有值。那麼a的含義是什麼呢?

查文檔後發現,在給定點處的實際寬度是用三階多項式函數計算的,這個函數看起來像這樣:width=a+bds+cds2+dds3width = a + b*ds + c*ds^2 + d*ds^3其中,width就是給定點(位於reference line上的點?)處的車道寬度。a,b,c,d是常數係數。ds是the distance along the reference line between the start of the entry and the actual position.總體來講,如果車道寬度變化比較複雜,那麼計算也比較複雜。但是在本xml文件中,對車道的寬度做了簡化,所有寬度都爲10,ds相乘的係數都爲0。

在這裏插入圖片描述

四、根據解析得到的數據創建道路模型

對於每條道路,已知它們的參考線起點座標、參考線方向、長度。在這個xml文件中只有兩種參考線類型,line和arc。我們可以把line和arc都用貝塞爾曲線來表示。根據參考線的信息,得到相應貝塞爾曲線上點的座標,從而確定最終的模型頂點,渲染出模型。

(PS:其實我們也可以不用貝塞爾曲線,使用和OSM數據生成模型一樣的方法。因爲不管是line還是arc,實際上都是由線段拼接表示的,我們得到線段的頂點後,完全可以直接用這些頂點生成Mesh。)

按照這個思路,我們可以創建一個腳本BezierCurvePath,用來單獨繪製每條road。在繪製road時,分爲兩個步驟:首先把這條road的參考線轉爲貝塞爾曲線上的點;再根據貝塞爾曲線上的點確定模型頂點,進行渲染。

1.把reference line表示爲Bezier曲線

(1)Bezier曲線介紹

這個網站可以調整控制點,並實時看到Bezier曲線的效果。

搬一個數學總結:

在這裏插入圖片描述

(2)獲取一條road reference line的信息

public List<BezierCurveData> curveDatas = new List<BezierCurveData>();

void Start()
	{
		float length_s = 0f,x = 0f, y = 0f;
		float angle = 0f,curvature = 0f,patchWidth = 0f; int lanes = 0;		
		string path = Application.dataPath+ "/XMLFiles/cloverleaf.xml";
		XmlSerializer serializer = new XmlSerializer(typeof(OpenDRIVE));
		string xml = File.ReadAllText(path);

		using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
		{
			openDrive = (OpenDRIVE)serializer.Deserialize(stream);
			
			//這個腳本只能處理一個road,所以設置了一個roadVariable來指定road的index
			
			//reference line起點的x座標
			x = openDrive.roads[roadVariable].plainView.geometry.x;
			//reference line起點的y座標
			y = openDrive.roads[roadVariable].plainView.geometry.y;
			//reference line的長度
			length_s = openDrive.roads[roadVariable].plainView.geometry.length;
			//reference line 的heading角度
			angle = openDrive.roads[roadVariable].plainView.geometry.hdg;
			//生成道路模型的寬度:車道數x10
			cubeThickness = openDrive.roads[roadVariable].lanes.laneSection.right.lane.Length * 10;			
			//如果reference是arc,存儲曲率
			try	{curvature = openDrive.roads[roadVariable].plainView.geometry.arc.curvature;}
			catch (NullReferenceException e){}
		}
		
		CityGenerator(x, y, length_s, angle, curvature);
	}
(3)根據reference line信息生成Bezier曲線數據
A. Line

在這裏插入圖片描述

void CityGenerator(float x, float y, float len, float angle, float curvature)
{	
	float x_end = 0.0f, y_end = 0.0f;	
	
	if (curvature != 0.0f) {......}
	else{
			//根據length和theta角度算出endPos
			x_end = len * Mathf.Cos (angle);
			y_end = len * Mathf.Sin (angle);
			
			//按照長度,將line等分爲3段,這樣就可以得到4個點了
			float add_pointx = x_end / 3, add_pointy = y_end / 3;
			
			float point1x, point2x, point3x, point4x;
			float point1y, point2y, point3y, point4y;
			
			point1x = x;
			point2x = point1x + add_pointx;
			point3x = point2x + add_pointx;
			point4x = point3x + add_pointx;

			point1y = y;
			point2y = point1y + add_pointy;
			point3y = point2y + add_pointy;
			point4y = point3y + add_pointy;
			
			//定義Bezier曲線的4個控制點
			var curve = new BezierCurveData ();
			curve.points = new Vector3[4]
			{
				new Vector3 (point1x, 0.1f, point1y),
				new Vector3 (point2x, 0.1f, point2y),
				new Vector3 (point3x, 0.1f, point3y),
				new Vector3 (point4x, 0.1f, point4y)
			};			
			curveDatas.Add (curve);
	}
}
B. Arc

關於曲率的介紹

先搞懂這些角度、點的含義:

在下面這張圖中,我們可以看到座標系原點在右下角,x軸和z軸垂直,構成了水平面,這和Unity場景中的俯視圖看到的座標系是一樣的。在opendrive的數據中,reference line的起點所在座標系也是平面座標系,是用x和y來表示的,這裏的y和unity中的z等同,都是水平面上與x軸垂直的軸。

在這裏插入圖片描述
我自定義了一個reference line,它的xml節點屬性值如下:起點的座標爲(x,y)=(0,0),hdg=1.57,轉換爲角度爲90度,這條arc的弧長爲1374m。這裏的hdg,指的就是這條弧在起點處的切線,與x軸的夾角。從圖上也可以看出來是90度。

<road id="3" name="R3" length="1374.446786" junction="-1">
	<planView>
		<geometry s="0.0" x="0" y="0" hdg="1.570796" length="1374.446786">
			<!-- hdgDegrees="90.0"-->
			<arc curvature="-0.001143" />
			<!-- radius="875.0"-->
		</geometry>
	</planView>
	<lanes>
		<laneSection s="0.0">
			<right>
				<lane id="-1" type="driving" level="false">
					<width sOffset="0.0" a="10.0" b="0.0" c="0.0" d="0.0" />
				</lane>
			</right>
		</laneSection>
	</lanes>
</road>

在下面這段代碼中,startAngle就是hdg的值,roadLength就是弧長,radius根據曲率的絕對值計算得到,值爲875,(在xml的註釋中已經寫出)。如果曲率的值小於0,那麼這條arc的方向就是順時針的。centerX和centerY是arc所在圓的圓心的橫縱座標。

if (curvature != 0.0f) {
	float startAngle = angle;
	float roadLength = len;
	float radius = 1.0f / Mathf.Abs (curvature);
	bool clockwise = curvature < 0.0f;
	float centerX = x - radius * Mathf.Cos (startAngle - HALF_PI) * (clockwise ? -1.0f : 1.0f);
	float centerY = y - radius * Mathf.Sin (startAngle - HALF_PI) * (clockwise ? -1.0f : 1.0f);
	......
}

計算圓心需要考慮startAngle、clockwise的值,我們就用最簡單的圖上的這個arc爲例:startAngle=HALF_PI,clockwise爲true,所以centerX=xradiusCos(0)(1)=x+radiuscenterX=x-radius*Cos(0)*(-1)=x+radius centerY=yradiusSin(0)(1)=ycenterY=y-radius*Sin(0)*(-1)=y也就得到了圖上所示的center點的座標。

在得到如上信息後,我們可以考慮如何創建多個折線段來表示這麼長的弧線,每個折線段可以用一條貝塞爾曲線來表示,該貝塞爾曲線的中間兩個點分別和起點終點重合。可以用下面getNewCurve函數來構建具體的貝塞爾曲線:我們只需要輸入該折線段的起點和終點的vector。

BezierCurveData getNewCurve(Vector3 prev_vector, Vector3 end_vector)
{
	var curve_data = new BezierCurveData();
	curve_data.points = new Vector3[4]{
		prev_vector, prev_vector,
		end_vector, end_vector
	};
	return curve_data;
}

爲了獲得這些小折線段的起點(start_vector )和終點(second_vector),我們可以使用一個while循環,當這些小折線段的總長度沒有達到弧線長度時,就不斷更新start_vectorsecond_vector

在這裏插入圖片描述

	......
	//將 r 減去一個車道寬度(10)
	float r = radius * (clockwise ? 1.0f : -1.0f);	
	
	Vector3 first_vector = new Vector3 ();//定義整個 arc 的第一個折線段起點
	
	if (roadLength < 750) {
		r = -r;
		first_vector = getCurvePart (startAngle, r - 10, centerX, centerY);
	} else
		first_vector = getCurvePart (startAngle, r - 10, centerX, centerY);	
	
	//these are two vectors starting and ending point of the curve
	var end_vector = new Vector3 (x, 0.01f, y);
	var start_vector = first_vector;
	float distance = 0f;//折線段總長度
	
	while (true) {
		//每次增加一點角度,使得second_vector不斷向end_vector靠近。
		startAngle = startAngle + 0.05f;
		var second_vector = getCurvePart (startAngle, r - 10, centerX, centerY);
		var curve_new = new BezierCurveData ();
		
		//循環的結束條件:總長度超過弧長
		distance += Vector3.Distance (start_vector, second_vector);
				
		if (distance >= roadLength) {
			curve_new = getNewCurve (start_vector, end_vector);
			curveDatas.Add (curve_new);
			start_vector = second_vector;
			break;
		}
		curve_new = getNewCurve (start_vector, second_vector);
		curveDatas.Add (curve_new);
		start_vector = second_vector;
	}
}

在每次循環中,遞增startAngle使得second_vector不斷向end_vector靠近,這是通過getCurvePart函數實現的:(start_x,start_y)構成的向量和(center_x,center_y)構成的向量相加,得到我們的目標向量(所代表的的點在arc上)。
在這裏插入圖片描述

Vector3 getCurvePart(float start_angle, float r, float center_x, float center_y)
{
	float start_x = 0.0f,start_y = 0.0f;
	
	start_x = r * Mathf.Cos(start_angle);
	start_y = r * Mathf.Sin(start_angle);
	
	return new Vector3(start_x + center_x, 0.1f, start_y + center_y);
}

2.把Bezier曲線渲染爲Mesh

得到curveData數組後,我們遍歷每一條曲線,生成一個curve遊戲對象,再生成具體的Mesh,作爲curve遊戲對象的子物體。對於line來說,curveData數組只有一個元素,對於arc來說,有很多個元素。

void CreateMesh()
{
	for (int i = 0; i < curveDatas.Count; i++)
	{
		if (curveMeshes.Count > i)
		{	//此函數會在Update中實時調用,已經生成對象後就直接調用對象的CreateMesh函數
			curveMeshes[i].CreateMesh();
		}
		else
		{	//首次遍歷時,先創建curveMesh對象實例
			var curveMesh = BezierCurveMesh.Instantiate(i, this);
			curveMeshes.Insert(i, curveMesh);
			curveMesh.CreateMesh();
		}
	}
}

如果想知道具體是怎麼生成的Mesh,可以參考這個開源項目:unity-procedural-mesh-bezier-curve。本項目的這部分代碼就是引用於它(也可以在本項目中直接查看相關代碼,不過這部分和路網生成關係不大,就不放在這裏解讀了)。

在這裏插入圖片描述

五、沿着道路隨機生成建築物

(1)準備好建築物的prefab共15個

在這裏插入圖片描述
(2)沿着道路隨機擺放建築物

可以觀察到:建築與路面之間有一定的距離;建築不會擺放到道路上去;建築都朝向路面。

關於實現方法,簡單來說就是要分成line和arc類型來處理。在處理每種路面的時候也要考慮線段的方向,從而確定房屋擺放的位置。這部分細節比較多,如果需要優化或者修改,可以詳細看看。

在這裏插入圖片描述

六、給道路添加程序紋理

道路模型沒有按照車道區分,所以多車道其實也是一個模型。而單車道和多車道,外表是不同的。

在這裏插入圖片描述
本項目對於單車道和多車道使用的是同一張圖片,但是對於多車道,在其上繪製了白色的線,作爲新的貼圖。

void Texture1(float x, float y, float len, float angle, float curvature, float patchWidth, int lanes,float []lane)
{
	Texture2D texture = new Texture2D((int)patchWidth, (int)len);
	
	Material m_Material;
	
	//Fetch the Renderer from the GameObject
	Renderer m_Renderer = GameObject.FindGameObjectWithTag("Road").GetComponent<Renderer>();
	m_Renderer.material = new Material(Shader.Find("Standard"));
	m_Material = m_Renderer.material;
	//Make sure to enable the Keywords
	m_Material.EnableKeyword ("_NORMALMAP");
	m_Material.EnableKeyword("_DETAIL_MULX2");
	
	m_Material.SetTexture("_MainTex", textures);
	m_Material.SetFloat ("_Metallic", 0.3f);
	m_Material.SetFloat ("_Metallic/_Smoothness", 0.0f);
	
	//【繪製白色車道線】
	int a1 = (int)lane[0];
	int j = 0,b1=0;
	for (int i= 1; i< lanes; i++) {
		int l=0;
		while(l<len){
			b1 = l+80;//每條短線長 80
			for ( j = l; j < b1; j++)		{
				Color32 color = new Color32(255,255,255,255);
				texture.SetPixel (a1, j, color);
				texture.filterMode = FilterMode.Point;				
			}			
			l=j+100; //短線間間隔 100
		}
		a1 = a1 + (int)lane[i];
	}
	
	m_Material.SetTexture("_DetailAlbedoMap", texture);
	m_Material.SetTextureScale("_MainTex", new Vector2(1, 2));
	materials = m_Material;
	texture.Apply();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章