這最短的一幀,我們主要以資料處理為中心一路下去看看大致處理流程,也是非常粗淺的認識,詳細分析請詳閱最長的一幀教程,之所以起名最短的一幀,就是想沿一條最短路徑穿越過去,故命名之。
我們把整個情境渲染過程可以看做是一個產品加工的過程,首先是原料,然後是原料經過哪些加工機器以及加工工序,最後加工成什麼。所以我們也採用該思路,我們拋開旁枝末節,看看情境資料如果經過中間的處理最後渲染到螢幕的,我們只去剖析和資料關係最為緊密的環節,拋開其它的不看,這樣內容將呈幾何級下降,過程相對比較清晰。
我們的原料就是一頭牛,我們要用這頭牛加工成香噴噴的牛肉罐頭,哈哈,是不是流口水了?廢話不說了,以後也不說了,一切從簡,單刀直入,逐一剖析。
建立工廠,也就是建立一個視景器,當然,裡面有已經買好了所有的加工裝置,但我們這裡去繁從簡,結合我們的標題“以資料為主線”,所有的分析絕不離開資料半步(話不離牛),這樣我們不會被牽來牽去的最後搞的暈頭轉向,所以我們就不去分析裡面的裝置了。
購買原料(牛),這個誰都知道,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);
到這裡說實話我實在不想進去再找什麼了,裡面我發現都是雜草了,已經沒有很明顯能一下子抓住我的眼球了,有興趣自己去一個一個看吧,反正這也到了繪製的差不多的最後了,呵呵。
這一幀其實真的很短了,去掉了太多太多。去掉的作為專題留著以後寫插敘吧。探究源碼,其樂無窮,不管你信不信,我反正信了。