對於一個3D引擎來講,最核心的部分應該算是場景組織(scene graph)了,若是這部分你都沒有設計好, 那麼就別期望開發一個成熟的3D引擎了。爲了開發3d引擎,因此我首先就研究這方面的內容,對一個3D的場景來講,又不少的物體,最簡單的組織方法就是把他們用一個List鏈接起來,而後在繪製沒一幀的時候依次送入渲染器(render)進行處理。
這顯然不是一個頗有效的方法,當處理一個普通的遊戲場景都會顯得很是慢的。實際上雖然一個場景中的物品不少,可是一般可見的指是以小部分,如何可以用很小的計算代價排除那些不可見的物品呢,這種方法叫作剔除隱藏面,減小繪製元素(Hidden Surface Complexity Reduction)。爲了實現這樣的方法,牽涉到空間排序(Spatial Sorting),最基本的方法要算二叉空間分割樹(BSP)了,DOOM是第一個使用了二叉樹的商業遊戲。二叉樹的構造簡單地說就是對於要處理的一組對象,選擇一個平面,將該組對象分紅兩組(若是由某個對象與該平面相交則用這個平面將這個對象分紅兩個對象)做爲該結點的兩個兒子,而後分別對兩組對象用相同的方法,直到知足一個特定的條件(一般是到結點上只有一個對象)爲止。
二叉樹確實是一種頗有效的場景組織結構,由於,當給出視錐(view frustum)之後,在穿過(traverse)這棵樹的時候,若是發現視錐(frustum)與結點所表明的平面不相交,那麼這個結點上有一棵子樹必然不可見,那麼這個子樹就不用送入渲染器了,當遇到Leaf的時候,就能夠得到所需的多邊形數據,能夠送入渲染器處理。
雖然二叉樹已是很是有效的方法,可是僅僅依靠二叉樹仍是不能知足遊戲的要求,由於如今的遊戲的場景是在是很大很複雜,又不少的物品,按照二叉樹的方法凡是與view frustum相交的Leaf必然要送入渲染器,由於view frustum是很大的,因此會有不少的Leaf與他相交,這就意味着渲染器仍是要處理不少的數據,若是你確實可以看到這麼多的物體,那也沒有辦法,可是一般,好比不少室內的場景,雖然在你的frustum裏面會由不少物體,可是你真正可以看到的仍是不多的一部分,好比一個封閉的房間。
所以被稱爲Portal的技術被引入到遊戲中來,之因此可以使用Portal技術,那是由於不少室內場景自身的限制條件所致。咱們引入region的概念,一個region就是一個相對封閉的空間,好比一個房間,region與region之間都是經過Portal(好比門或窗)相鏈接,所以,若是你處於一個region當中,你就只能看到這個region中的物體,若是你可以看到其餘region中的物體,那麼你必定是經過Portal看到的,因此處理的過程以下(考慮Portal是單向的狀況,若是兩個region能夠經過一個門相互看到,我就是用兩個單向的Portal)。
void CRegion::Draw(LPRender lpRender_)
{
if (m_bVisited) return; // 防止兩個相鄰的region的Portal造成死循環
m_bVisited = TRUE;
for (int i=0;i< m_NumOfPortals;i++)
{
if (m_aPortals[i].m_bOpen)
{
// 若是Portal在view frustum中
if (!lpRender->Cull(m_aPortals[i]))
m_aPortals[i].m_pRegion->Draw(lpRender_);
}
m_apObjects->Draw(lpRender_);
}
m_bVisited = FALSE;
}
一般咱們使用二叉樹的方法來組織region,理想的狀況下每一個二叉樹樹的Leaf就是一個region,經過二叉樹的遍歷能夠很容易的找到照相機(camera)所在的region。不過我以爲實際作場景的時候不會這麼理想,因該是一個region可能被劃分紅了幾個leaf,不過只要保證每一個leaf必定屬於某個region,咱們就能夠對每一個leaf增長一個region的引索(index),一樣能夠很方便的找到所在的region。
Portal引擎的一個不太好的地方就是,你必須手動設定許多Portal,設計場景的會有一些限制,不然得不到很好的效果。在瞭解了這些技術之後我又去看了「Genesis3D」的源代碼,只看了場景組織的部分,我先把個人理解說一下。
編輯器
Trace.h Trace.c vis.h vis.c world.h world.c |
Genesis3D有以下幾個概念:
Model // Model[0]表示場景全部中不動的部分,
// Model[i](i>0)表示場景中的活動物體(好比:門,升降臺)
// Model[0]對應一個二叉樹
// Model中還有FirstLeaf,NumOfLeafs來記錄對應的Leafs
// Model結構中有一個int Area[2]的結構,
// 對於自己是活動門的Model,正好能夠記錄連通的兩個Area
Cluster // 不敢確定,推測是一種區域的概念,比Area要大
// 並且Cluster之間沒有動態的連通關係,只有臨街關係。
Area // 至關於咱們上面所說的Region的概念,
// Genesis3D的一個場景中最多容許256個Area,
// 這能夠從它的world結構中的AreaConnection[256][256]看出,
// 1表示連通,0表示不通
// Area之間的連通性經過Model[i](i>0)來控制
// int VisFrame表示Area是否可見
Node // BSP上的結點
// int VisFrame表示Node是否可見
Leaf // 劃分世界的二叉樹的葉子,
// 每一個Leaf上都有一個Area的index
// 每一個Leaf上都有一個Cluster的index
// 以及一個Polygon List的指針
Actor // 活動的人
所以我能夠基本推斷若干Leaf構成一個Area,若干Area又能夠構成一個Cluster?(猜想)對於二叉樹上的每一個Node都設置了一個VisFrame,用於判斷是該結點表明的子樹是否可見。咱們能夠看到它的渲染過程:
RenderScene(...)
{
Vis_VisWorld(...); // 檢測並設定可見性
RenderWorldModel(...); // Render場景不動的部分就是Model[0]
RenderSubModels(...); // Render場景中活動的部分
RenderActors(...); // Render全部的人物
}
下面咱們來分析每一個過程:
Vis_VisWorld(...)
{
將全部的結點設置爲不可見 // 它用的方法很巧妙,這個留給讀者本身去看了
找到Camera所在的Leaf,假設爲Leaf[E]
Leaf[E].VisFrame=可見
Area[Leaf[E].AreaIndex].VisFrame=可見
// 經過一下這個遞歸過程設定全部Area的可見性
// 經過AreaConnection[][]來判斷,
// 凡是跟Area[Leaf[E].AreaIndex]可以連通的都設定爲可見
// 具體方法比較簡單,留給讀者本身去看了
Vis_Flood_r(Area[Leaf[E].AreaIndex])
for (int i=0;i< Model[0].NumOfLeafs;i++)
{
// 我就是根據這裏的順序,推測Cluster是比Area更大的區域
// 不然就應該先判斷Area了
if (Cluster[Leaf[E].ClusterIndex]與Cluster[Leaf[i].ClusterIndex ]不相通)
continue;
// 若是Leaf[i]所在的Area不可見,那麼Leaf[i]不可見
if (Area[Leaf[i].AreaIndex] != 可見)
continue;
Leaf[i].VisFrame = 可見
// 既然Leaf[i]可見,那麼i的全部父結點都應該可見,
// 這個方法也很簡單,留給讀者本身去看了
MarkVisibleParents(i);
// 下面的過程是將Leaf所包含的全部surface設定爲可見
// 我不清楚他爲何要作這一步
...
}
for (i = 1;i>NumOfModels;i++)
{
// 判斷Model[i]是否可見的方法是,
// 求Model[i]的Axis-Aligned Bouding Box的Center
// 遍歷Model[0]的二叉樹,找到Center所在的Leaf
// 若是該Leaf可見,那麼該Model可見
// 不然該Model不可見
if (ModelVisible(Model[i]))
Model[i].VisFrame = 可見
}
}
RenderWorldModel(...); // 渲染場景不動的部分就是Model[0]
{
遍歷Model[0]對應的二叉樹,
除了通常用Frustum來剪枝之外,
一旦發現Node.VisFrame不可見,
那麼該Node表明的整個子樹都被揀選(Cull)掉。
若是Leaf.VisFrame不可見,
那麼Leaf中的全部Polygon都被揀選(Cull)掉
}
RenderSubModels(...); // 渲染場景中活動的部分
{
for (i = 1;i< NumOfModels;i++)
{
if (Model[i].VisFrame = 可見)
繪製Model[i]
}
}
RenderActors(...); // 渲染全部的人物
{
for (i=0;i>NumOfActors;i++)
{
Actor[i]的AABB的Center所在的Leaf若是可見
繪製Actor[i]的PolygonList,不然不繪製。
}
}
由於Actor不會同時屬於兩個Area,因此只要找到Actor的Center所在的Leaf是否可見就能夠判斷Actor是否可見了。如今有些遊戲使用其它的組織方法,好比Oni中就使用了八叉空間分割樹(Octtree),比起二叉樹、Portal技術由很大的優點,在2000年遊戲開發者年會中「Hidden Surface Reduction and Collision Detection Based on Oct Trees」一文(pease.doc)就比較詳細的介紹了Bungie公司的這個方法,我以爲很值得一試。
我在看了peace.doc之後決定採用oni的作法,使用他們介紹的那種八叉樹+光線追蹤(Raycasting)的組織結構。由於在思考二叉樹+Portal的引擎時有不少問題難以解決,我以爲難點在於構造含有Portal的二叉樹結構,地圖編輯器很難作,Genesis3D的源代碼並不包含地圖編輯器的部分,因此你沒法得知它是如何構造它的二叉樹的。給出一個靜止的場景部分,劃分二叉樹並不難,可是若是你但願可以構造含有Portal的region就比較麻煩了。
1)首先,基本上不太會有一個Leaf剛好等於一個region,實際劃分可能出現一個Leaf與若干region相交,我最後的結論是能夠用如下規則來劃分,若是一個Leaf屬於某一個region,那麼該Leaf就不用再劃分了,若是它與n個(n>1)個region相交,那麼就要將該Leaf繼續劃分下去。如此應該能夠保證每一個Leaf必定屬於某個region,那麼在渲染的時候,只要找到照相機所在的Leaf就能夠經過該Leaf上記錄的region索引,找到所需處理的region了(Genesis3D裏面的Leaf結構就能夠找到他所謂的Area)。若是是這種思路,那麼下面問題就必需要解決。
2)Region如何識別或者劃分,計算機自動(不太可能,這種region的概念徹底是人定的),手工識別(如何手工識別,在一個複雜的場景中選擇一個個面,而後還必須構成封閉的空間才能定爲region,這樣恐怕也不現實)我還想過,全部的模型都有3DS MAX來作,每次美工確保作一個Region(好比一個房間),咱們本身作一個工具去識別包圍該region的多面體,還必須可以手工加少數輔助對該region進行Portal的指定和識別,而後在地圖編輯器中僅僅導入這樣的結構,構造實際場景的時候只是設定一下region的位置,而後對於每一個Portal設定他們指向的region代號。 看似可行,可是實際上識別或者指定region和portal真的是很困難的,至少是很是複雜的事情。每當你想到一點作法,還會發現對其它的一些問題解決不方面,一直找不到關於劃分region,設定Portal的文章,因此我以爲作一個二叉樹+Portal的引擎,在地圖編輯器方面就難以完成。
在vanly的ftp上面有Quake引擎的分析,他們的作法是將場景劃分紅二叉樹之後,對於每一個Leaf都預先算好它的PVS。在渲染的時候,找到照相機所在的Leaf,而後查表獲得預先算好的該Leaf的PVS,而後再繪製PVS中的Leaf。這裏它沒有介紹如何計算這個PVS,並且它如何壓縮使得巨大的PVS表格只變成20K也沒有說。還有它並無考慮會開關的Portal。因此我感受仍是沒有什麼進展。
最後只有oni的八叉樹+光線追蹤還算有但願,他不須要將處於切分平面上的物體分割,並且不要指定region和portal,對於美工建模來講限制不多,能夠自由發揮,對於程序來講,地圖編輯器由於不要什麼識別功能,只要根據現有的數據劃分出八叉樹就能夠了,負擔也比較輕,只是它的消隱過程麻煩一些,也有些缺陷,可是感受代價比二叉樹+Portal要低,至少咱們感受基本能夠實現,而二叉樹+Portal的引擎還沒什麼好的解決方法。工具