文章目錄
一、引言
前置知識:
這次參考了兩個開源項目:
第二個項目比較好,是直接用的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就是給定點(位於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,所以 也就得到了圖上所示的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_vector
和second_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();
}