1 OSG基礎知識
Ø OSG是Open Scene Graphic 的縮寫,OSG於1997年誕生於以爲滑翔機愛好者之手,Don burns 爲了對滑翔機的飛行進行模擬,對openGL的庫進行了封裝,osg的雛形就這樣誕生了,1998年Don burns 遇到了同樣喜歡滑翔機和計算機圖形學的Robert Osfield ,從此Robert Osfield加入了osg小組的開發並一直擔任開發小組的組長。
Ø OSG不但有openGL的跨平臺的特性和較高的渲染性能,還提供了一系列可供3D程序開發者使用的功能接口,包括2D和3D數據文件的加載、紋理字體支持、細節層次(LOD)控制、多線程數據分頁處理等。OSG廣泛應用於飛行仿真等領域,包括Flightgear,及美國軍方投資的仿真項目Delta3d等
1.1 計算機繪圖的基本知識
Ø 首先要先回顧一下,在顯示世界中,我們是如何作畫的。
Ø 在現實世界中,繪製一副畫,我們需要的東西就是彩筆、白紙。通過選擇不同顏色的彩筆,在白紙上移動,就可以將白紙上的不同的點描繪上不同的顏色,而所有這些點連接起來,從人的宏觀視野看來,就構成了一副對人有意義的畫作。
Ø 類比到計算機的實際中來。在計算機的世界裏。作畫的過程又是怎樣的呢?
Ø 同樣,繪製虛擬的圖像,也需要“彩筆”和“白紙”。在計算機的世界裏,“彩筆”就是Direct3D之類的繪圖API函數,而“白紙”就是存儲數據的內存。我們在內存中劃分出一塊區域,其中的數據就是對一個真實世界的模擬。一個數據就描述真實世界中一個點的屬性。在我們作畫前,他們都只有一個初始值,就像白紙在作畫前只有白色一樣。而在作畫後,每一個數據都有了獨特的意義,將整片數據連接在一起看,就是一副有意義的圖景。作畫的過程就是對內存中的每一個數據進行賦值的過程,相當於用彩筆給白紙上的一個點進行着色。選擇不同的API函數,可以畫出不同的形狀。
1.2 OSG程序框架
Ø 一個最簡單的OSG程序如下所示,當然在如果是在VS下面進行編輯的話要進行一些設置,要設置OSG的lib和include目錄。
1 #include<osgDB/ReadFile>
2
3 #include<osgViewer/Viewer>
4
5 void main()
6
7 {
8
9 osgViewer::Viewer viewer;
10
11 viewer.setSceneData(osgDB::readNodeFile("glider.osg"));
12
13 viewer.realize();
14
15 viewer.run();
16
17 }
18
osgViewer::Viewer viewer 申請了一個viewer,可以理解爲申請一個觀察器,該觀察可以查看模型
viewer.setSceneData(osgDB::readNodeFile("glider.osg")) 這裏是設置觀察器Viewer中的數據,換句話說,有了觀察器,就可以添加模型了
viewer.realize() 這個語句表達的意思非常多,事實上可以定位到Viewer.cpp的realize函數,會發現裏面的操作非常多,可以理解爲這是在渲染前的最後一步,會檢查和設置圖形上下文,屏幕啊什麼的,會讓你以前的設置,對Viewer的設置都生效。
viewer.run(); 這一句的意思就是渲染了,如果要解釋它的意思的話,可以用下面的幾個語句來替代:
while(!viewer.done()){viewer.frame();}.意思也就是說,只要viewer沒有結束,那麼就繪製它的每一個幀[frame]。
1.3 OSG簡單模型控制
1.3.1 添加模型
在OSG當中模型是使用osg::Group和osg::Node來裝載在一起的,比如同時需要加入兩個模型,模型A了模型B,AB各自是一個NODE,那麼可以使用以下語句來做到,首先使用一個Group,然後Group->addChild(A),同樣,之後要Group->addChild(B)。然後再把Group添加到viewer當中就可以了。如圖3.1所示AB之間的關係。在這裏要申明的是NODE是Group的父類,在類中都有相應的方法可以轉到對方,故Node與Group
是通用的,Node也可以被當作Group來用。
圖 31 AB都加入到Group當中
簡單示例代碼如下:
1 #include<osgDB/ReadFile>
2
3 #include<osgViewer/Viewer>
4
5 #include<osg/Node>
6
7 void main()
8
9 {
10
11 osgViewer::Viewer viewer;
12
13 osg::Group * root=new osg::Group();
14
15 root->addChild(osgDB::readNodeFile("glider.osg"));
16
17 root->addChild(osgDB::readNodeFile("osgcool.osg"));
18
19 viewer.setSceneData(root);
20
21 viewer.realize();
22
23 viewer.run();
24
25 }
26
則運行結果爲:
圖 32示例運行結果
1.3.2 刪除結點
如果我們不需要某個結點了,比如圖3.2我們看那個小飛機很不爽,我們想把它從場景中刪除掉。不知道於某種目的,反正現在要刪除掉,可能是開始想看見它現在不想看見它了。可以通過removeChild方法,除多個孩子也可以通過removeChildren方法,裏面的參數有些需要索引值,有些需要結點本身的指針,讀者可以自己嘗試。這裏要注意的是,如果要刪除一個結點,那麼該結點下的所有結點都會被刪除。如果一個結點被加入到一個組中兩次,那麼這兩次是分別存在的,刪除一次還有另一次。刪除操作不能說不是個危險的操作,有些時候,尤其在有移動結點等等混在一起時,刪除操作有時候會發生一些比較奇怪的現象。在內存映象當中,如果一個模型被讀取一次,而用了多次,那麼所佔用的空間是不會改變的。
1.3.3 隱藏模型與結點開關
Ø 隱藏模型
隱藏模型其實模型仍在渲染當中,因此損耗並未減少,只不過隱藏了而已,隱藏的確不是個什麼好操作,但是有時候對小模型確實也很實用。node->setNodeMask可以設置隱藏與顯示。
Ø 節點開關
在OSG當中,專門有一個類來負責打開與關閉結點,該類名爲osg::Switch,裏面有相應的方法來控制它所管理的結點的打開與關閉。
兩個方法都能控制模型的顯示和隱藏,區別在於隱藏模型方法不會讓模型在內存中消失,這樣對於小的物體頻繁的調用會節省一些時間,而對於有些大的模塊在用一次以後可能很久再用第二次,這個時候用節點開關可以將模型銷燬,再次使用再調入內存,以防止佔用更多的資源。
1.3.4 超級指針
超級指針的機制,其實就是引用一個計數器,這個計數器會計算這個箱子被引用的次數,被別人引用一次這個計數器增加一,別人不用一次,即:釋放一次,則計數器減一。當減至0時,內存放掉不用。
們來看使用一個Node的三種方法,對比一下:
Ø //方法一,最好的方法,十分安全,也是OSG中最常用的方法,多少版本它都沒變
osg::ref_ptr<osg::Node>aNode(new osg::Node());
group->addChild(aNode.get());
Ø //方法二,也是非常好的方法,有時候不適用,但也十分安全
group->addChild(new osg::Node());
Ø //方法三,非常危險,但是令許多人無故鋌而走險的方法
osg::Node*anotherNode=new osg::Node();
group->addChild(anotherNode);
方法一:在new::Node()時申請了一個Node的資源,這時在堆內引用該Node的計算器會被置1。在group->addChild(aNode.get())時又引用了一次,會再加1。在這兩次引用都結束時,Node的資源就會被釋放。
方法二:這個方法也是很實用的,但是無法引出Node的指針,也許在別處可以用到,事實上會經常用到。如果已經這樣做了,得到Node指針也不是不可以的,可以使用NodeVisitor來得到Node的指針,也可以使用findChild方法來做這件事。
方法三:這個應該是最常用,但是最爛的方法了,原因在於如果在osg::Node*antherode=new osg::Node()之後發生了錯誤,拋出了異常,誰來釋放Node所佔用的資源呢。而這個異常在後面被捕獲,程序正常的走下去,而內存卻沒有被正常的放掉。
1.3.5 移動/旋轉/縮放模型
移動/旋轉/縮放其實都是對矩陣進行操作,在OSG當中,矩陣可以當作一個特殊的結點加入到root當中,而矩陣下也可以另入結點,而加入的結點就會被這個矩陣處理過,比如移動過/旋轉過/縮放過。在OSG中控制矩陣的類爲osg::MatrixTransform。
Ø 移動
osg::Matrix::translate
Ø 旋轉
osg::Matrix::rotate
Ø 縮放
osg::Matrix::scale
1.4 基本幾何圖元
1.4.1 基本繪製方法
首先來看一些OSG中的最基本的繪製路數。如果我們要繪製一個正方形,繪製有色彩,未貼圖。首先我們必須要申請一個osg::Geometry,把這個Geometry加入到Geode就可以了。在這個Geometry中要設置一些元素,最基本的是頂點Vertex,顏色color,以及頂點的關聯方式和法線normal.就可以了。如圖3.3所示。
圖 33幾何體繪製過程
1.4.2 可繪製的圖元
所有可繪製的圖元包括:
Ø POINTS[點]
Ø LINES[線]
Ø LINE_STRIP[線帶]
Ø LINE_LOOP[閉合線段]
Ø TRIANGLES[三角形]
Ø TRIANGLE_STRIP[三角帶]
Ø TRIANGLE_FAN[三角扇]
Ø QUADS[四方塊]
Ø QUAD_STRIP[四方塊帶]
Ø POLYGON[多邊形]
在OSG中設置直線線寬的專門有一個函數來管理,叫做LineWidth,它本身屬於狀態與屬性類別中的類。事實上也是從那裏派生而來。所有設置狀態的操作都與此類似。
1.4.3 內置幾何類型
如同OpenGL一樣,OSG同樣有一套內置幾何類型,這些幾何類型都在類osg::Shape中,這些shape本身都可以本當成一個Draw結點加入到geode中,然後再人geode中添加到root裏進行渲染。形狀共有九種,分別爲:osg::Box[盒子],osg::Capsule[膠囊形],osg::CompositeShape[組合型],osg::Cone[圓錐形],osg::Cylinder[圓柱形],osg::HeightField[高程形],osg::InfinitePlane[有限面],osg::Sphere[球形],osg::TriangleMesh[三角蒙皮]。
內置幾何類型的渲染過程,如圖4.5所示
圖 34基本幾何圖元的添加過程
這裏要注意的是,一般的形狀態都有特定的因素,比如Box有長寬,圓有半徑,以及各個圖形所畫的精細度都需要指明,這些精細度在球這樣的形狀上意義還是十分巨大的。在OSG中有專門指明精細度的類,名爲:osg::TessellationHints。以球爲例,只需要規定,圓心,半徑和精細度就可以畫出該球。
1.5 交互
1.5.1 交互過程
viewer的主要的功能是控制場景,它是場景的核心類,如果能響應鍵盤時得到viewer,那麼也可以從鍵盤的響應中控制整個場景。viewer中有一個方法,名爲addEventHandler就是專門做這件事情的。他會加入一個事件處理器。於是我們就想,一定要自己寫一個事件處理器才行,這就必須要了解事件處理器的格式,只要有一個接口就可以瞭解它的格式,這個接口就是:osgGA::GUIEventHandler,於是我們可以寫一個類A從該類公有派生出來,即:class A:public osgGA::GUIEventHandler,在裏面處理好各種操作然後加入到viewer當中,即:viewer.addEventHadler(new A(裏面可以有參數));這樣就可以完成操作。
假如類A是一個事件處理類,那麼加入類A可以這樣理解,如圖3.5:
圖 35事件A控制場景過程
1.5.2 事件類型與響應
代碼 值 事件類型
NONE 0 無事件。
PUSH 1 鼠標某鍵按下,在上面代碼28行有用到。
RELEASE 2 鼠標某鍵彈起。
DOUBLECLICK 4 鼠標某鍵雙擊。
DRAG 8 鼠標某鍵拖動。
MOVE 16 鼠標移動。
KEYDOWN 32 鍵盤上某鍵按下。
KEYUP 64 鍵盤上某鍵彈起。
FRAME 128 應該是鼠標每幀。沒用過。
RESIZE 256 窗口大小改變時會有的事件。
SCROLL 512 鼠標輪滾動。
PEN_PRESSURE 1024 手寫板的某事件?
PEN_PROXIMITY_ENTER 2048 手寫板的某事件?
PEN_PROXIMITY_LEAVE 4096 手寫板的某事件?
CLOSE_WINDOWS 8192 關閉窗口。
QUIT_APPLICATION 16384 退出程序。
USER 32768 用戶定義。
至於爲什麼都用2的N次方,主要是因爲它的二進制編碼只有一位是一,判斷事件時很好判斷,只要年哪位是一就可以了。
1.5.3 PICK
pick主要是通過鼠標的點擊來拾取一些物體,或者判斷鼠標所點擊的位置在哪裏。Pick實現的思路如下圖所示:
圖 36pick事件流程
判斷射線與viewer中物體相交的方法爲發出射線並相交。在OSG中有庫函數,osgViewer::View::computeIntersections他共有三個參數:第一個是x屏幕座標,第二個是Y屏幕座標,第三個是存放被交的結點以及相交的座標結點路徑等等相關信息。
判斷相交結點爲我想要的那個結點:只需要判斷存放相交射線交場景的結果集中有沒有要用的結點就可以了。
1.6 漫遊
1.6.1 MatrixManipulator
場景的核心管理器是viewer,而漫遊必須響應事件,比如鼠標動了,場景也在動。響應事件的類是osgGA::GUIEventHandler。我們想把響應事件的類派生一個新類出來,這個類專門用來根據響應控制viewer。這個類就是osgGA::MatrixManipulator,這個類有一些設置矩陣的公共接口,有了這些接口就可以有效的控制viewer了,根據不同的習慣,大家還會設置不同的控制方式,如同OSG自帶的幾個操作器,操作都不盡相同。來看一下漫遊的主要流程如圖6.1:
圖 37一般的場景操作器
操作器必須從osgGA::MatrixManipulator派生而來。osgGA::MatrixManipulator有四個可以控制場景的重要接口:
1 virtual void setByMatrix(const osg::Matrixd&matrix)=0
2
3 virtual void setByInverseMatrix(const osg::Matrixd&matrix)=0
4
5 virtual osg::Matrixd getMatrix()const=0
6
7 virtual osg::Matrixd getInverseMatrix()const=0
8
四個矩陣接口可以有效的向viewer來傳遞矩陣的相關信息。
1.6.2 碰撞檢測
最簡單的碰撞檢測如下圖所示:
圖 38簡單的碰撞檢測原理圖
TravelManipulator.dll中用到的就是如圖所示的原理,黑三角形代表沒有移動之的位置,控制移動的函數是ChangePosition(osg::Vec3&delta),參數意思是要移動的相對於當前點的增量,在黑三角形沒有移動時該函數在計算時先假設一點newPosition爲移動後的點,而後通過連接這兩個點,而後通過判斷與場景的模型是否有交點來判定這個移動可不可以執行,如圖所示,兩者之間有個大盒子,是穿不過去的,所以只有保持在原地。就算沒有這個盒子,移動後的新點又與地面在某種程序上有一個交點,這證明移動是不可行的。這可以防止用戶穿過地板到達地下去。
1.6.3 路徑漫遊
使用path文件的方法如下面示例
1 #include<osgDB/ReadFile>
2
3 #include<osgViewer/Viewer>
4
5 #include<osg/Node>
6
7 #include<osgGA/AnimationPathManipulator>
8
9 void main()
10
11 {
12
13 osgViewer::Viewer viewer;
14
15 viewer.setSceneData(osgDB::readNodeFile("glider.osg"));
16
17 //申請一個操作器,參數爲一個path文件。
18
19 osg::ref_ptr<osgGA::AnimationPathManipulator>amp=new osgGA::AnimationPathManipulator("glider.path");
20
21 //選擇使用這個操作器。
22
23 viewer.setCameraManipulator(amp.get());
24
25 viewer.realize();
26
27 viewer.run();
28
29 }
30
31
我們可以用路徑編輯器編輯path文件,或者可以控制程序中的某個物體的運動軌跡然後保存爲path文件。
1.7 更新&回調
回調的意思就是說,你可以規定在某件事情發生時啓動一個函數,這個函數可能做一些事情。這個函數就叫做回調函數,我們可以使用已有回調函數或者自定義回調函數。
Ø 使用已有回調
已有的回調的類型有很多種,一般很容易就想到的是UpdateCallBack,或者EventCallBack等
Ø 自定義回調
自定義回調爲從一個回調類型派出生自己的回調,然後具有該種回調的特點等等。
NodeVisitor是一個極有用的類,可以訪問結點序列,使用的方法大同小異,NodeVisitor的工作流程如下圖所示:
圖 39NodeVisitor工作流程
在主結點accept之後,結點數據立即傳至NodeVisitor中去,應用apply函數,可以將數據定任一些操作,更多的操作還是需要硬性的製做與調用。
1.8 粒子系統初步
在OSG中提供有專門的粒子系統工具,名字空間爲osgParticle,OSG對經常使用的粒子模擬都做了專門的類,如:ExplosionEffect用於暴炸的模擬,FireEffect用於火的模擬,ExplosionDebrisEffect用於爆炸後四散的顆粒模擬等等。
在OSG中使用粒子系統一般要經歷以下幾個步驟:
第一步:確定意圖(包括粒子的運動方式等等諸多方面)。第二步:建立粒子模版,按所需要的類型確定粒子的角度(該角度一經確定,由於粒子默認使用有Billboard所以站在任何角度看都是一樣的),形狀(圓形,多邊形等等),生命週期等。第三步:建立粒子系統,設置總的屬性,第四步:設置發射器(發射器形狀,發射粒子的數目變化等),第五步:設置操作(旋轉度,風力等等因素)。第六步:加入結點,更新。下圖描述了各個部分是協調工作的方式:
圖 310粒子系統各個部分是協調工作的方式
上圖中各個部分所對應的類如下圖所示
圖 311粒子系統各部分對應的類
1.9 視口&LOD&Imposter
1.9.1 多視口
多視口的原理是自己創建所有的相機,包括主相機,這樣我們可以隨意的添加相機。
首先我們要創建視口必須有以下幾件東西,第一,瞭解整個屏幕的分辯率有多大,這樣可以分辯視口的大小,好分割開來。第二,上下文。我們必須自己手動的打開設置上下文。每個視口的數據也不一定非要與主視口的相同。但是矩陣一般是同步的。也就是說:主視口裏有棟樓,從視口裏可以是平面圖什麼的。瞭解整個屏幕的分辯率可以用這個類:osg::GraphicsContext::WindowingSystemInterface意思是說系統接口,可以獲得當前環境的各種信息。有一方法叫getScreenResolution,可以得到分辯率。之後上下文了,osg::GraphicsContext裏面可以設置窗口大小,緩存什麼的,大多數的東西都在這裏面設置。
1.9.2 LOD
LOD即level of details
LOD比起PagedLOD而言並非十分的常用,有個地方用的特別多,那就是把一個好好的模型加一個視矩壓成一個模型,這個模型比以前的看來就是多了個視矩的控制,遠了看不見,近了能看見。
在模型中加LOD頭結點的方式如下所示:
1 #include<osgDB/Registry>
2
3 #include<osgDB/ReadFile>
4
5 #include<osgDB/ReaderWri ter>
6
7 #include<osgDB/Wri teFile>
8
9 #include<osg/Node>
10
11 #include<osgViewer/Viewer>
12
13 int main()
14
15 {
16
17 osgViewer::Viewer viewer;
18
19 //讀取模型
20
21 osg::Node*node=osgDB::readNodeFile("fountain.osg");
22
23 //隱藏結點
24
25 node->asGroup()->getChild(0)->setNodeMask(0);
26
27 viewer.setSceneDa ta(node);
28
29 //輸出結點到free.os g中
30
31 osgDB::writeNodeFile(*(viewer.getSceneData()),"free.osg",osgDB::Registry
32
33 ::instance()->getOptions());
34
35 return 0;
36
37 }
38
39
1.9.3 Imposter
用動態圖片來替換場景的實用技術:imposter.可以把它法做LOD一樣使用,只不過它
不是變模型變沒有,而是使它換成一張圖
示例代碼如下:設置一個視矩,超過這個視距模型會變爲一張動態圖
1 #include<osgViewer/Viewer>
2
3 #include<osgGA/TrackballManipulator>
4
5 #include<osgSim/Impostor>
6
7 #include<osgDB/ReadFile>
8
9 int main(inta rgc,cha r**a rgv)
10
11 {
12
13 //申請viewer
14
15 osgViewer::Viewer viewer;
16
17 //讀取模型
18
19 osg::Node*node=osgDB::readNodeFile("ceep.ive");
20
21 //申請一個i mpos tor結點
22
23 osgSim::Impos tor*sim=new osgSim::Impostor;
24
25 //在到之內顯示模型,之外顯示貼圖
26
27 sim->addChild(node,0,50000);
28
29 sim->setImpostorThreshold(1000);
30
31 osg::Group*root=new osg::Group;
32
33 root->addChild(sim);
34
35 viewer.setSceneData(root);
36
37 viewer.realize();
38
39 viewer.run();
40
41 return 0;
42
43 }
44
45
1.10 文字&模型陰影
1.10.1 HUD
HUD即head up display
文字在3D場景中顯示往往要經歷以下幾步:讀取字體點陣信息->轉化爲圖像->反走樣->最終圖像。在反走樣期間可以處理可種模糊效果,在最終圖像形成時可以設置如何擺放。OSG中有一個TEXT類,提供可很多文字顯示的方法,比如
1 void setFont(Font*font=0)
2
3 //設置/得到字體,如setFont("fonts/SIMYOU.TTF");
4
5 void setFont(const std::string&fontfile)
6
7 const Font*getFont()const
8
9 //設置/得到字體顯示的寬高
10
11 void setFontResolution(unsigned int width,unsigned int height)
12
13 unsigned int getFontWidth()const
14
15 unsigned int getFontHeight()const
16
17 //設置/得到文字的具體內容
18
等等,可以很方便的調用
1.10.2 陰影
OSG對陰影的支持也相當的好,可以很容易的寫出簡單的陰影效果,可以參考例子osgShadow
OSG有一個專門的shadow類來支持陰影效果,提供了很多接口,如:
1 void setBackdropType(BackdropType type)
2
3 //說明:設置陰影類型。
4
5 void setBackdropOffset(float offset=0.07f)
6
7 void setBackdropOffset(float horizontal,float vertical)
8
9 //說明:設置陰影的離開程度與方向
10
11 void setBackdropColor(const osg::Vec4&color)
12
13 //說明:設置陰影顏色
14
15 void setColorGradientMode(ColorGradientMode mode)
16
17 //說明:設置顏色映射方式,可以得到漸變效果
18
等等
http://www.cnblogs.com/shapherd/archive/2010/08/10/osg.html
PS:本文爲幾個月前整理,參考書:FreeSouth的《QpenSceneGraph程序設計》