這最短的一幀,我們主要以資料處理為中心一路下去看看大緻處理流程,也是非常粗淺的認識,詳細分析請詳閱最長的一幀教程,之是以起名最短的一幀,就是想沿一條最短路徑穿越過去,故命名之。
我們把整個場景渲染過程可以看做是一個産品加工的過程,首先是原料,然後是原料經過哪些加工機器以及加工工序,最後加工成什麼。是以我們也采用該思路,我們抛開旁枝末節,看看場景資料如果經過中間的處理最後渲染到螢幕的,我們隻去剖析和資料關系最為緊密的環節,抛開其它的不看,這樣内容将呈幾何級下降,過程相對比較清晰。
我們的原料就是一頭牛,我們要用這頭牛加工成香噴噴的牛肉罐頭,哈哈,是不是流口水了?廢話不說了,以後也不說了,一切從簡,單刀直入,逐一剖析。
建立工廠,也就是建立一個視景器,當然,裡面有已經買好了所有的加工裝置,但我們這裡去繁從簡,結合我們的标題“以資料為主線”,所有的分析絕不離開資料半步(話不離牛),這樣我們不會被牽來牽去的最後搞的暈頭轉向,是以我們就不去分析裡面的裝置了。
購買原料(牛),這個誰都知道,osgDB::readNodeFile(“cow.osg”)。
放料,将場景給視景器,viewer.setSceneData(cow);
下面我們就看看怎麼放料,都把牛放到什麼地方。直接進入上面的函數可以看到,這頭牛直接到了View::setSceneData(node);我們繼續看,我們看到View裡有一個Scene成員,一個Scene就是一顆場景樹,這也就是該視圖主相機對應的場景樹,進入該函數後我們看到這頭牛先被送給了這個Scene,_scene->setSceneData(node);往下看我們看到了
View::assignSceneDataToCameras(),根據函數名稱我們不難看出這是要把這頭牛指派給相機,進入該函數,我們發現确實是這樣,首先它把牛給了場景漫遊器,因為場景漫遊器在進行計算的時候要用到這個牛,_cameraManipulator->setNode(sceneData);我們知道,一個視圖有一個主相機,這個主相機一個最重要的工作就是用來實作視圖變換,視圖變換是離不開場景的,是以需要将這頭牛給這個主相機,_camera->addChild(sceneData);這裡我們看到,場景對象是作為子節點加入到這個主相機的。而我們知道,一個相機也就是一個變換節點,這樣以來,把這頭牛作為子節點加在這個相機下,隻要我們通過變換這個相機節點的位置、姿态等就很容易實作場景的視圖變換。别忘了,我們一個視圖可以有多個相機的,除了這個主相機之外,可能還有若幹從屬相機,從屬相機可以有自己的場景樹,也可以直接用主相機的場景樹,如果是沿用主相機的場景樹(多個相機觀察的是同一個場景),我們就要把場景賦給主相機的同時賦給所有從屬相機,要不它們照誰去啊?是以接下來就是把這頭牛給所有的從屬相機(别忘了前提條件是這些相機也要照這頭牛),
for(unsigned i=0; i<getNumSlaves(); ++i)
{
…..
if(sceneData) slave._camera->addChild(sceneData);
……
}
分析到這裡,我們基本已經清楚了料是如何放的,都放到了哪裡。
接下來我們就看看如何加工。
還是那句話,話不離牛,我們一下子可以漂洋過海來到void ViewerBase::frame(double simulationTime),這裡是什麼?我不說大家也都知道,這就是整個加工流水線。我們來看看吧。裡面開始打掃衛生的工作我們就不去看了,直接看流水線上的三大環節:
eventTraversal();
updateTraversal();
renderingTraversals();
看過最長的一幀的對他們應該很熟悉(可能對osg全部已經很熟悉,呵呵),我這裡就簡單說一句,它們分别是事件周遊、更新周遊和渲染周遊(廢話,看名字都看出來了,^_^)。我們先不進去,先來簡單分析一下要不要進去。首先看eventTraversal(),我們這裡為了用最簡單的過程看牛的加工,就當場景中沒有任何事件發生(不管中間是否有人把牛從切割機上搬到了地方又搬回來,也不管有沒有人偷吃了牛尾巴),隻是渲染,是以我們不管它,但是,不看不等于說它跟牛就沒關系哦,我們隻是想把過程放到最重要的處理過程,要記住一點就夠了,如果要對牛有任何互動操作,比如把牛從一個地方挪到另一個地方(通過拖拽器),我們都知道這個過程離不開事件處理器,他們的所有相關處理都在這裡完成,鑒于裡面的内容相對我們牛的處理來說比較雜亂,不太集中,是以就不對它進行詳細分析了。
updateTraversal(),該函數跟牛關系還是非常密切的,而且它裡面也不複雜,操作相對集中,我們進去簡單看看。首先我們看到最關鍵的一個就是_scene->updateSceneGraph(*_updateVisitor);前面我們知道這個Scene裡有我們的牛,看看它裡面做了什麼,首先我們可以看到,一個視圖有一個更新通路器_updateVisitor,後面我們會看到它有什麼用。函數開始是對分頁資料庫的操作處理,這裡跟我們的牛沒什麼關系,暫且不管,直接看牛,
if (getSceneData())
{
……
getSceneData()->accept(updateVisitor);
}
到這裡需要我們去updateVisitor看看對牛做了什麼,這個updateVisitor就是一個osgUtil::UpdateVisitor,就是我們前面提到的那個每個視圖所擁有的_updateVisitor,它采用通路者模式對節點進行更新通路。下面我們來看看
virtual void apply(osg::Node& node)
{ handle_callbacks_and_traverse(node); }
繼續到
inline void handle_callbacks_and_traverse(osg::Node& node)
{
handle_callbacks(node.getStateSet());
osg::NodeCallback* callback= node.getUpdateCallback();
if(callback) (*callback)(&node,this);
elseif (node.getNumChildrenRequiringUpdateTraversal()>0) traverse(node);
}
到這裡就已經很明了了,它就是執行了所有場景節點的回調,對于我們這裡來說就是執行了牛的回調(事實上我們也沒給牛設定回調,是以也就沒執行了)。記住了,我們設定的節點回調就是在這被執行的哦。 還沒完,接下來我們看到
if (_camera.valid()&& _camera->getUpdateCallback()) _camera->accept(*_updateVisitor);
for(unsigned int i=0; i<getNumSlaves(); ++i)
{
osg::View::Slave&slave = getSlave(i);
osg::Camera* camera= slave._camera.get();
if(camera && slave._useMastersSceneData && camera->getUpdateCallback())
{
camera->accept(*_updateVisitor);
}
}
updateVisitor通路主相機和所有的從屬相機,執行相機的回調。這裡你可能會問,直接對所有的相機執行一次通路不就行了嗎?反正場景也是相機的子節點。人家沒那麼做肯定是有道理的,看看代碼就知道了,通路場景和通路相機的時候,周遊模式不同,通路場景是要所有的節點都通路的,而通路相機隻需要通路相機本身,不需要周遊,所有要分開通路。離開牛了,不說了,馬上回歸主題。
在離開這個updateTraversal之前,還有一點不得不說,那就是下面的
if (_cameraManipulator.valid())
{
……
_camera->setViewMatrix(_cameraManipulator->getInverseMatrix());
}
我們知道,場景的視圖變換、投影變換以及場景篩選等都是通過相機來實作的,那現在我就告訴你,這就是實作的最關鍵的一個入口,設定主相機的觀察矩陣。這也就是你的場景漫遊器進行各種滑鼠鍵盤處理後的結果被采用的地方。沒有這裡,你的牛在生産線上你都看不到它在哪個機器上,你也不知道該把那些廢料(牛腸子、牛毛等等)扔掉(場景篩選啊,^_^)。
好了,到此為止,我們的updateTraversal已經結束了,還記得下面是什麼吧?renderingTraversals,不用想了,很明顯我們的旅程還沒完,而又隻剩下它了,是以它是必看不可的了。它也是這三個裡面最大個兒的了。
還是那句話,話不離牛,函數内其他亂七八糟的跟加工牛罐頭關系不甚密切的我們不管,這樣我們首先來看裡面的
Scenes scenes;
getScenes(scenes);
for(Scenes::iteratorsitr = scenes.begin();
sitr!= scenes.end();
++sitr)
{
……..
if(scene->getSceneData())
{
scene->getSceneData()->getBound();
}
}
首先擷取所有的場景,并計算所有場景的場景節點的包圍球。接下來是
Cameras cameras;
getCameras(cameras);
它擷取了目前的所有的活動相機,不活動不用管。
接下來看
for(Cameras::iterator camItr= cameras.begin();
camItr!= cameras.end();
++camItr)
{
osg::Camera* camera= *camItr;
Renderer*renderer = dynamic_cast<Renderer*>(camera->getRenderer());
……..
renderer->cull();
……..
}
每個相機有一個renderer,我們知道,場景中不可見的、太小的或被遮擋的節點都是要裁剪掉的,也即是我們的牛毛、牛角都不能做罐頭要扔掉,這裡就是完成這個工作的,也就是便利所有相機,通過各自相機的渲染器對象來完成場景的篩選工作。一個相機對應一個渲染器,渲染器主要就是對外提供場景的篩選與渲染操作接口,每個renderer中一般有兩個SceneView成員,之是以有兩個就是為了實作渲染背景雙緩沖,到後面我們會看到,具體的場景裁剪與繪制是通過SceneView對象來完成的,同時這兩個成員還會被放在一個可用場景圖隊列中,每次從該隊列中擷取隊首的場景圖進行場景的篩選會繪制,完成後再傳到隊尾,如此循環往複,一幀一幀持續進行。我們下面就進入renderer的cull函數來看看具體篩選過程是怎樣完成的。
首先正如上面所說的,從可用場景圖隊列擷取隊頭的場景圖:
osgUtil::SceneView* sceneView = _availableQueue.takeFront();
然後對該場景圖進行全局渲染狀态更新設定:
updateSceneView(sceneView);
接下來通過該場景圖對場景進行篩選:
sceneView->cull();
我們進去看看都是怎麼篩選的吧。
進去後我們會發現很多非常陌生的東西,像渲染資訊、渲染舞台、狀态圖等,這些我們不去一一解釋,雖然非常有用,但解釋過多就又會變得雜亂,想對這些有詳細了解,可以參看最長的一幀,上面有對這些對象的詳細解釋和分析。如此以來我們就可以跳過絕大多數代碼直接看cullStage,這就是對整個渲染舞台的裁剪的核心所在。進入這個函數後,去掉旁枝末節,我們會看到cullVisitor->traverse(*_camera);一個場景圖有一個裁剪通路器對象負責場景周遊裁剪處理,看到這個,可能你差不多快要明白了接下來會發生什麼,場景篩選我們都知道,最主要的就是判斷節點包圍盒與相機平截頭體的關系,具體點就是節點包圍盒是否在相機平截頭體内或部分在内,否則就被篩選出去了。好了,長話短說,你肯定知道我們這個牛其實是個Group節點,那我們就看看CullVisitor中對Group的處理吧,
void CullVisitor::apply(Group&node)函數中的第一行是
if (isCulled(node)) return;
進去看看吧,裡面可能會有我們想要的。
inline bool isCulled(const osg::Node& node)
{
returnnode.isCullingActive()&& getCurrentCullingSet().isCulled(node.getBound());
}
喔,似乎和我們要的差不多,因為出現了node.getBound()。趕緊再進一層看看。
CullingSet::isCulled(const BoundingSphere&bs)中終于出現了我們最終想要的,那就是if (!_frustum.contains(bs)) return true;至于它的裡面就純屬幾何問題了,我們不再去追究。好了,我們看到,如果被裁剪掉了,就直接傳回了,啥也不做了,那如果沒有被裁剪掉呢?接着看:
StateSet* node_state = node.getStateSet();
if(node_state) pushStateSet(node_state);
handle_cull_callbacks_and_traverse(node);
擷取該節點的渲染狀态,用于建構渲染狀态樹,同時接着向下周遊。
到此為止,你肯定還有疑問,被裁剪掉的節點,我們是一股腦連它的肉帶它的皮都扔了不要了,留下來的現在隻是留下了渲染狀态,充其量是把牛筋牛血留下來了,牛肉跑哪去了?呵呵,你知道的,osg中最終儲存模型頂點資訊的是drawable,場景樹中也隻有葉節點擁有它們,是以,找它們還要看看CullVisitor對Geode的處理。
看看void CullVisitor::apply(Geode& node),第一行同樣是
if (isCulled(node)) return;
往下就不一樣了,pass,pass,去掉一堆亂麻我們看到了addDrawableAndDepth(drawable,&matrix,depth);
你應該猜到了這是幹什麼的了,進去小看一眼,裡面最後一行有一個
_currentStateGraph->addLeaf(createOrReuseRenderLeaf(drawable,_projectionStack.back().get(),matrix,depth));
簡單講這就是根據這個沒有被裁剪掉而留下來的Drawable建立一個渲染葉,并加狀态圖用于繪制的肉了,也就是我們提出牛筋牛皮牛血牛内髒等留下來的牛腱子牛腩等做罐頭用的好肉了。好了,到這裡我們的場景篩選也就結束了。
雖然也有點累,但這短短的一幀還沒完,至少肉還沒做成熟的裝罐啊。你看,最後還有_drawQueue.add(sceneView);這就是将篩選後的場景圖加入繪制隊列裡。
廢話少說,sceneView->cull();完了,我們看看場景繪制,場景繪制也是通過SceneView來完成的,那就是SceneView->draw()了,從哪可以看出呢?在我們上面的rederingTraversals裡,場景篩選下面是
for(itr = contexts.begin();
itr!= contexts.end();
++itr)
{
if(_done) return;
if(!((*itr)->getGraphicsThread())&& (*itr)->valid())
{
……
(*itr)->runOperations();
}
}
進入runOperations()我們會看到
for(CameraVector::iterator itr = camerasCopy.begin();
itr!= camerasCopy.end();
++itr)
{
osg::Camera* camera= *itr;
if(camera->getRenderer())(*(camera->getRenderer()))(this);
}
這是一個操作符重載,也就是renderer的
virtual void operator () (osg::GraphicsContext* context);
進去看看,我們看到了draw();再進去,有沒有看到sceneView->draw()?
好,現在我們進入SceneView->draw()不看不知道,一看吓一跳,到最後才發現這裡的雜草灌木最多。不管了,直接找最明顯的吧,我猜應該是
_renderStage->draw(_renderInfo,previous);
這裡面雜草也不少,直接找一棵最顯眼的:
drawInner( useRenderInfo, previous, doCopyTexture);
到這裡說實話我實在不想進去再找什麼了,裡面我發現都是雜草了,已經沒有很明顯能一下子抓住我的眼球了,有興趣自己去一個一個看吧,反正這也到了繪制的差不多的最後了,呵呵。
這一幀其實真的很短了,去掉了太多太多。去掉的作為專題留着以後寫插叙吧。探究源碼,其樂無窮,不管你信不信,我反正信了。