《最長的一幀》理解

概況

宗旨:瞭解OSG在一幀時間,也就是仿真循環的一個畫面當中做了什麼。

while(!viewer.done())
        viewer.frame();

osgViewer::ViewerBase::frame()函數:
viewerInit():完成視景器的初始化工作;
realize():完成窗口和場景的設置工作;

advance():一幀經歷的時間、幀數以及棄用對象的刪除;
eventTraversal():執行用戶設置的EventCallback,爲所有的用戶交互和系統事件提供一個響應的機制;
它必須在每一幀的仿真過程中,取出已經發生的所有事件,摒棄那些對場景不會有助益的(例如,在視口以外發生的鼠標移動事件和胡亂點擊),依次交付給各個事件處理器,最後清空現有的事件隊列,等待下一幀的到來。
updateTraversal():處理用戶的更新回調對象之外,還要負責更新攝像機的位置,並且更新分頁數據庫DatabasePager 和圖像庫ImagePager 的內容。
更新回調與事件回調最大的不同在於:每當一個用戶交互或系統事件產生時,每一個節點(以及Drawable 對象)的事件回調都會被調用一次;而節點(以及Drawable 對象)的更新回調只會在每幀中被調用一次。這一區別決定了我們應當在什麼時候使用事件回調,以及在什麼時候使用更新回調。
renderingTraversals():場景的渲染遍歷工作。

osgViewer::View::init()

兩個重要的類成員變量:_eventQueue和_cameraManipulator;

_eventQueue 存儲視景器的事件隊列。OSG中代表事件的類是osgGA::GUIEventAdapter,它可以用於表達各種類型的鼠標、鍵盤、觸壓筆和窗口事件。在用戶程序中,我們往往通過繼承osgGA::GUIEventHandler 類,並重寫handle函數的方法, 獲取實時的鼠標/ 鍵盤輸入, 並進而實現相應的用戶代碼。

_eventQueue 除了保存一個GUIEventAdapter 的鏈表之外,還提供了一系列對鏈表及其元素的操作函數,這其中,createEvent 函數的作用是分配和返回一個新的GUIEventAdapter事件的指針。隨後,這個新事件的類型被指定爲 FRAME 事件,即每幀都會觸發的一個事件。

_cameraManipulator是視景器中所用的場景漫遊器的實例。通常我們都會使用setCameraManipulator 來設置這個變量的內容, 例如軌跡球漫遊器(TrackballManipulator)可以使用鼠標拖動來觀察場景,而駕駛漫遊器(DriveManipulator)則使用類似於汽車駕駛的效果來實現場景的漫遊。

osgViewer::Viewer::realize()

這裏寫圖片描述
1、視景器Viewer 的主/從攝像機均需要使用setGraphicsContext 設置對應的圖形設備上下文,實際上也就是對應的顯示窗口;
2、GraphicsContext 的創建由平臺相關的抽象接口類WindowingSystemInterface 負責,對於Win32 平臺而言,這個類是由GraphicsWindowWin32.cpp 的Win32WindowingSystem 類具體實現的,它創建的顯示窗口設備即osgViewer::GraphicsWindowWin32 的實例。

OSG 的視景器包括四種線程模型,可以使用setThreadingModel 進行設置,不同的線程模型在仿真循環運行時將表現出不同的渲染效率和線程控制特性。

osgViewer:: Viewer::advance()

至此正式進入仿真循環。

1、獲取上一次記錄的參考時間(Reference Time);
2、根據當前時刻,重新記錄參考時間,並因此得到兩次記錄之間的差值,即一幀經歷的時間;
3、記錄已經經過的幀數;
4、有的時候我們需要將幀速率,參考時間等內容予以記錄並顯示給用戶,此時需要通過ViewerBase::getStats 函數獲得osg::Stats 對象,用以進行幀狀態的保存和顯示;
5、如果需要的話,使用Referenced::getDeleteHandler()來處理osg::Referenced 對象被棄用之後的刪除工作。

osgViewer::Viewer::eventTraversal()

總結一下OSG 視景器、攝像機與場景的關係:
視景器
視景器包括幾個最主要的組件:
漫遊器_cameraManipulator,用於實現交互式的場景漫遊;
事件處理器組_eventHandlers,負責處理視景器的事件隊列_eventQueue,主要是鍵盤/鼠標等事件的處理;
場景_scene,它包括視景器所對應的場景圖形根節點,以及用於提高節點和圖像數據處理速度的兩個分頁數據庫;
攝像機_camera_slaves,前者爲場景的主攝像機,後者爲從攝像機組,不過OSG 並沒有規定一定要使用主攝像機來顯示場景,它的更重要的作用是爲OSG 世界矩陣的計算提供依據。

攝像機是 OSG 視圖顯示的核心器件,沒有攝像機就沒有辦法將場景圖形的實景展現給用戶。它包括:
1、視口(Viewport),它指示了攝像機顯示窗口的位置和尺寸。
2、圖形上下文(GraphicsContext),通常這也就是平臺相關的圖形顯示窗口(即GraphicsWindow,對於Win32 系統而言,它實際上是通過CreateWindowEx 這個熟悉的API來創建的),不過也可能是離屏渲染的設備(例如PixelBufferWin32)。

osg仿真環境與WindowsAPI
首先使用 Viewer::getContexts 函數找到視景器中所有已有的GraphicsWindow 圖形窗口,然後執行GraphicsWindowWin32::checkEvents 函數。請求分發消息直接指向了GraphicsWindowWin32 的實例。TranslateMessage和DisoatchMessage的工作當然也很明確:通知Windows 執行窗口的消息回調函數,進而執行用戶交互和系統消息的檢查函數GraphicsWindowWin32::handleNativeWindowingEvent。而這個函數負責把WM_ 消息轉化並傳遞給osgGA::EventQueue 消息隊列。之後,使用EventQueue::takeEvents 函數,把當前GraphicsWindow 圖形窗口對象gw 的事件隊列保存到指定的變量gw_events 中。

下一步,遍歷剛剛所得的所有事件,對於每一個GUIEventAdapter事件對象event:
1、首先處理Y 軸的方向問題,通常的GUI 窗口系統都會將屏幕左上角定義爲(0, 0),右下角定義爲(Xmax, Ymax),但是OSG 的視口座標系定義爲左下角(0, 0),右上角(Xmax, Ymax),此時,有必要對每個event 對象的鼠標座標值做一步轉換。
2、對於符合條件的攝像機,設置爲_cameraWithFocus。

注意: 場景節點的回調對象必須繼承自 osg::NodeCallback,並重寫NodeCallback::operator()函數以實現回調的具體內容。由此有所不同的是,Drawable 對象的事件回調必須繼承自Drawable::EventCallback,並具現EventCallback::event 函數的內容;其更新回調則必須繼承Drawable::UpdateCallback 並具現UpdateCallback::update 函數。

osgViewer::Viewer::updateTraversal()

OSG 更新回調的作用與事件回調有類似之處:由專門的訪問器對象_ updateVisitor 的負責場景圖形更新遍歷;所有的節點和Drawable 幾何體對象都可以使用setUpdateCallback 設置更新回調;通過具體實現NodeCallback::operator()或者Drawable::UpdateCallback::update 函數,可以在回調對象中添加自定義的工作。
OSG更新遍歷流程:
1、獲取函數的起始時刻。
2、使用預設的更新訪問器_updateVisitor,訪問場景圖形的根節點並遍歷其子節點,實現各個節點和Drawable 對象的更新回調。
3、使用DatabasePager::updateSceneGraph 函數以及ImagePager::updateSceneGraph 函數,分別更新場景的分頁數據庫和分頁圖像庫
4、處理用戶定義的更新工作隊列_updateOperations。
5、執行主攝像機_camera 以及從攝像機組_slaves 的更新回調(但是不會遍歷到它們的子節點),注意攝像機回調的執行時機與場景節點還是有所區別的。
6、根據漫遊器_cameraManipulator 的位置姿態矩陣,更新主攝像機_camera 的觀察矩陣。
7、使用View::updateSlaves 函數更新從攝像機組_slaves 中所有攝像機的投影矩陣,觀察矩陣和場景篩選設置(CullSettings)。
8、獲取函數的結束時刻,將相關的時刻信息保存到記錄器中。

從攝像機組_slaves 的更新

osgViewer::View::updateSlave():從攝像機組_slaves 的更新。從攝像機組與主攝像機的關係:從攝像機組從本質上繼承了主攝像機的投影矩陣,觀察矩陣和場景篩選設置,但是可以在使用View::addSlave 添加從攝像機時,設置投影矩陣與觀察矩陣的偏置值,還可以使用CullSettings::setInheritanceMask 設置CullSettings(場景篩選) 的繼承掩碼。OSG目前支持多種場景篩選方式。

更新場景的分頁數據庫和分頁圖像庫

更新場景的分頁數據庫和分頁圖像庫:在解讀DatabasePager之前,先了解OpenThreads 庫,其包含了以下幾個最主要的線程處理類:
Thread 類:線程實現類。每定義一個Thread 類,就相當於定義了一個共享進程資源、但是可以獨立調度的線程。通過重寫run()和cancel()這兩個成員函數,即可實現線程運行時和取消時的操作;通過調用start()和cancel(),可以啓動或中止已經定義的進程對象。
Mutex 類:互斥體接口類。OpenThreads 提供了互斥體操作的機制,它有效地避免了各個線程對同一資源的相互競爭,由lock()函數和unlock()函數實現共享資源加解鎖。一個線程類中可以存在多個Mutex 成員,用於在不同的地點或情形下爲共享區域加鎖;但是一定要在適當的時候解鎖,以免造成線程的共享數據無法再訪問。
Condition 類:條件量接口類。它依賴於某個Mutex 互斥體,互斥體加鎖時阻塞所在的線程,解鎖或者超過時限則釋放此線程,允許其繼續運行
Block 類:阻塞器類。顧名思義,這個類的作用就是阻塞線程的執行,使用block()阻塞執行它的線程(注意,不一定是定義它的Thread 線程,而是當前執行了block 函數的線程,包括系統主進程),並使用release()釋放之前被阻塞的線程。
BlockCount 類:計數阻塞器類。它與阻塞器類的使用方法基本相同:block()阻塞線程,release()釋放線程;不過除此之外,BlockCount 的構造函數還可以設置一個阻塞計數值。計數的作用是:每當阻塞器對象的completed()函數被執行一次,計數器就減一,直至減到零就釋放被阻塞的線程。
Barrier 類:線程柵欄類。這是一個對於線程同步頗爲重要的阻塞器接口。每個執行了Barrier::block()函數的線程都將被阻塞;當被阻塞在柵欄處的線程達到指定的數目時,就好比柵欄無法支撐那麼大的強度一樣,柵欄將被衝開,所有的線程將被釋放。重要的是,這些線程是幾乎同時釋放的,也就保證了線程執行的同步性。
注意 BlockCount 與Barrier 的區別,前者是由其它任意線程執行指定次數的completed()函數,即可釋放被阻塞的線程;而後者則是必須阻塞指定個數的線程之後,所有的線程纔會同時被釋放。

updateSceneGraph 函數的工作是更新分頁數據庫的內容,它的內容簡單到只包含了兩個執行函數的內容:
1、DatabasePager::removeExpiredSubgraphs:用於去除已經過期的場景子樹;
我們首先遍歷 DatabasePager::_pagedLODList 這個成員變量,並執行其中每個PagedLOD對象的removeExpiredChildren 函數,取得其中已經過期的子節點並記錄到一個列表裏。將這些過期節點標記爲“可刪除”,並傳遞給_fileRequestQueue->_childrenToDeleteList成員,也就是的“待刪除列表”,同時喚醒DatabaseThread 線程。

下一步,將過期節點從_pagedLODList 中刪除,由於它們已經被傳遞到“待刪除列表”當中,因此ref_ptr 引用計數不會減到零,也就不會在主仿真循環中觸發內存釋放(delete)動作。

最後還要執行 SharedStateManager::prune 函數。這裏的osgDB::SharedStateManager 指的是一個渲染狀態共享管理器,它負責記錄分頁數據庫中各個節點的渲染屬性(StateAttribute),並判斷節點之間是否共享了同一個渲染屬性,從而節省加載和預編譯的時間。prune 函數的工作是從SharedStateManager 中剔除那些沒有被共享的渲染屬性
如果希望啓用 SharedStateManager (默認是關閉的,其性能目前可能沒有想象的那麼好),需要在進入仿真循環之前執行:
osgDB::Registry::instance()->getOrCreateSharedStateManager();

2、DatabasePager::addLoadedDataToSceneGraph:用於向場景圖形中添加新載入的數據。
這裏首先取得“待合併列表_dataToMergeList,並遍歷其中每一個DatabaseRequest 對象。遍歷過程中,首先執行 SharedStateManager::share 函數,將新加載節點_loadedModel 的渲染屬性保存到SharedStateManager 管理器中。隨後執行 DatabasePager::registerPagedLODs,在加載的節點及其子樹中搜索PagedLOD節點,並添加到剛剛提到的_pagedLODList 列表中。最後,判斷DatabaseRequest::_groupForAddingLoadedSubgraph 對象(也就是新加載節點在場景中的父節點)是否合法,並將DatabaseRequest::_loadedModel 添加爲它的子節點。

以下是分析:
osgDB::DatabasePager 類執行的是這一工作:每一幀的更新遍歷執行到updateSceneGraph 函數時,都會自動將“一段時間之內始終不在當前頁面上”的場景子樹去除,並將“新載入到當前頁面”的場景子樹加入渲染,這裏所說的“頁面”往往指的就是用戶的視野範圍。這些分頁和節點管理的工作如果由渲染循環來完成的話,恐怕是費時又費力的,對於場景的顯示速度有較大的影響,因此,DatabasePager 中內置了專用於相關工作處理的DatabaseThread 線程。

在講解 DatabaseThread 線程之前,我們理應先仔細考慮一下,OSG 的分頁數據庫應該使用單獨的線程來處理什麼:
1、刪除過期的場景數據:這一步工作當然也可以在仿真循環中進行,但是這樣做很可能會造成場景渲染的延遲,我們採用線程來處理場景數據的理由也正是因爲如此。過期對象的統一刪除工作在這裏完成,而更新遍歷則負責將檢索到的對象送入相應的過期對象列表。
2、獲取新的數據加載請求:請求加載的可能是新的數據信息,也可能是已有的場景數據(曾經從“當前頁面”中去除,更新又回到“當前頁面”中);可能是本地的數據文件,也可能來自網絡,並需要把下載的數據緩存在本地磁盤上。這些都需要在線程中一一加以判斷。
3、編譯加載的數據:有些數據如果提前進行編譯可以有效地提升效率,例如爲幾何體數據創建顯示列表(Display List),以及將紋理對象提前加載到紋理內存;雖然OSG 同樣可以在渲染時根據用戶需要執行這些工作,但是那樣勢必會造成幀的延遲,對於大型場景的加載來說這種延遲將更爲嚴重。因此預編譯加載的數據是很有必要的。在數據處理線程執行預編譯工作當然不爲過,但是如果系統配置足夠高級的話,也可以選擇由圖形設備線程(GraphicsContext::getGraphicsThread)來完成這些原屬於它們的工作。
4、將加載的數據合併至場景圖形:直接由線程來完成這一工作顯然是不合適的,因爲我們不知道當DatabaseThread 線程試圖操作場景中的節點時,OSG 的渲染器在做些什麼。最好的方法是將讀入的數據先保存在一個列表中,並且由仿真循環負責獲取和執行合併新節點的操作。
osg的分頁數據庫使用單獨的線程處理什麼
左側的圖框表示數據的檢索和輸入,中間的白色圖框表示用於數據存儲的內存空間,而右邊的圖框表示存儲數據的輸出。此外,藍綠色圖框表示可以在DatabaseThread 線程中完成的工作,而橙色圖框表示由線程之外的函數完成的工作。

這幅圖中事實上已經標示出了 DatabasePager 中的幾個重要成員變量。不過在認識它們之前,我們還需要了解一下DatabasePager 類所定義的各種數據結構:
1、DatabasePager::DatabaseThread 類:這是分頁數據庫的核心處理線程,它負責實現場景元素的定期清理,加載以及合併工作;但是讓它一直處於檢查各個數據列表的循環狀態,這未免太過耗費系統資源。因此,這個線程在平常狀態下應當被阻塞,需要時再予以喚醒。
2、DatabasePager::DatabaseRequest 結構體:這個結構體保存了用戶的單個數據請求,包括數據文件名,請求時間,數據加載後存入的節點,以及要進行合併的父節點等;除此之外還有一個重要的編譯映射表_dataToCompileMap,這個映射表負責保存圖形設備ID 與編譯對象(幾何體顯示列表,紋理等)的映射關係。
3、DatabasePager::RequestQueue 結構體:它負責保存和管理一個“數據請求列表”_ requestList,也就是由DatabaseRequest 對象組成的向量組,除此之外還負責對列表中的數據按請求時間排序。上圖中所示的_ dataToCompileList_dataToMergeList 實際上都是RequestQueue 類型的對象,不過它們所保存的“請求列表”事實上是已經完成加載的“待編譯/待合併列表”了。
4、DatabasePager::ReadQueue 結構體:這個結構體繼承自RequestQueue,不過還增加了一個“棄用對象列表”_childrenToDeleteList,也就是osg::Object 對象組成的向量組。它是數據處理線程中最重要的對象之一,除了可以隨時向兩個列表裏追加數據請求和棄用對象之外,這個結構體還包括了一個updateBlock 函數,負責阻塞或者放行DatabaseThread 線程,其根據是:列表中是否存在新的數據請求或棄用對象需要處理,以及用戶是否通過函數設置暫時不要啓用線程(DatabasePager ::setDatabasePagerThreadPause)。

osgDB:: DatabasePager::DatabaseThread::run ():現在我們可以進入線程循環體內部瀏覽了。每次循環開始時,數據處理線程都被自動阻塞,避免無謂的系統消耗;直到updateBlock 函數在外部被執行纔會放行,繼續下面的代碼。
updateBlock 函數可能在以下幾種情形下被執行:
1、ReadQueue 對象中的“數據請求列表”被修改,例如新的數據加載請求被傳入,請求被取出,列表被重置。
2、ReadQueue 對象中的“棄用對象列表” 被修改,例如有新的過期對象被送入,對象被刪除,列表被重置。
3、執行了DatabasePager ::setDatabasePagerThreadPause 函數,當線程被重新啓動時,會自動檢查線程是否應當被喚醒。
這之後是過期數據的刪除工作,即取出 _ childrenToDeleteList 中的所有對象,並安全地將它們析構。隨後,使用DatabasePager::ReadQueue::takeFirst 函數,從當前線程對應的ReadQueue 對象(_fileRequestQueue 或_httpRequestQueue)的隊列中取出並清除第一個數據加載請求(DatabaseRequest)。

什麼情形下我們纔會用到DatabasePager?使用 osg::PagedLOD 和osg::ProxyNode 節點的時候。
總結DatabasePager的流程:
這裏寫圖片描述
注意:ProxyNode 和PagedLOD 的區別:ProxyNode 的功能主要是在運行時加載一個或多個模型文件作爲子節點;而PagedLOD 雖然可以實現相同的功能,但它還有另外一項重要的工作,那就是根據用戶的視點範圍來實現場景樹的“修剪”——剔除對場景長期沒有助益的節點,加載用戶可見的節點。這也是這幾日以來我們一直強調的“分頁”的精髓所在了吧。

1、首先,osg::PagedLOD 節點或者osg::ProxyNode 節點使用setFileName 函數,請求運行時加載模型文件爲子節點。
2、在場景的篩選(Cull)過程中,OSG 將自動取出PagedLODProxyNode 中保存的文件名數據,並使用DatabasePager::requestNodeFile 函數將其保存到“數據請求列表”中(RequestQueue::_requestList)。
3、DatabasePager 內置了兩個數據處理線程(DatabaseThread),分別用於處理本地文件和HTTP 數據,線程的主要工作是刪除“已棄用隊列”(RequestQueue::_childrenToDeleteList)中的對象,並從“數據請求列表”中獲取新的請求。
4、線程中如果取得新的數據請求,則嘗試加載新的模型,判斷是否需要預編譯模型,並送入“等待編譯列表”(_dataToCompileList)。預編譯的含義是執行顯示列表的創建,紋理綁定,GLSL 數據綁定等OpenGL 動作,通常情況下預編譯模型可以避免它在顯示時出現幀延遲。
5、對於編譯完成或者無需編譯的數據請求,首先創建其KDTree 包圍體(用於K-Dop Tree碰撞檢測計算),然後送入“等待合併列表”(_dataToMergeList),線程讓出控制權。
6、場景的每次更新遍歷均會執行updateSceneGraph 函數,於其中將一段時間內沒有進入用戶視野的節點送入“已棄用隊列”(注意這一工作只限於PagedLOD 節點的子節點),並將“等待合併列表”中的新數據使用addChild 送入當前的場景圖形。

ImagePager 與DatabasePager 沒什麼大的區別,它主要負責的是紋理圖片文件的運行時加載工作。 ImageSequence 類的主要功能是使用自身包含的Image圖片對象序列,實現一種動畫紋理的效果。與 DatabasePager 相同,ImagePager 也內置了一個處理線程,其中隨時讀取“圖片加載請求”的內容,並根據其中的文件名使用osgDB::readImageFile 加載數據(osg::Image 對象)。加載之後的圖片數據將被加入到申請它的ImageSequence 對象中。ImagePager 本身暫時不具備“分頁”的功能,換句話說,在目前的版本中,它不會負責將長時間不用的圖片刪除。

發佈了82 篇原創文章 · 獲贊 58 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章