使用回調類實現對場景圖形節點的更新。本節將講解如何使用回調來實現在每幀的更新遍歷(update traversal)中進行節點的更新。

回調概覽
用戶可以使用回調來實現與場景圖形的交互。回調可以被理解成是一種用戶自定義的函數,根據遍歷方式的不同(更新update,揀選cull,繪制draw),回調函數將自動地執行。回調可以與個別的節點或者選定類型(及子類型)的節點相關聯。在場景圖形的各次遍歷中,如果遇到的某個節點已經與用戶定義的回調類和函數相關聯,則這個節點的回調將被執行。
創建一個更新回調
更新回調將在場景圖形每一次運行更新遍歷時被執行。與更新回調相關的代碼可以在每一幀被執行,且實現過程是在揀選回調之前,因此回調相關的代碼可以插入到主仿真循環的viewer.update()和viewer.frame()函數之間。而OSG的回調也提供了維護更為方便的接口來實現上述的功能。善於使用回調的程序代碼也可以在多線程的工作中更加高效地運行。
從前一個教程展開來說,如果我們需要自動更新與坦克模型的炮塔航向角和機槍傾角相關聯的DOF(自由度)節點,我們可以采取多種方式來完成這一任務。譬如,針對我們將要操作的各個節點編寫相應的回調函數:包括一個與機槍節點相關聯的回調,一個與炮塔節點相關聯的回調,等等。這種方法的缺陷是,與不同模型相關聯的函數無法被集中化,因此增加了代碼閱讀、維護和更新的復雜性。另一種(極端的)方法是,只編寫一個更新回調函數,來完成整個場景的節點操作。本質上來說,這種方法和上一種具有同樣的問題,因為所有的代碼都會集中到仿真循環當中。當仿真的復雜程度不斷增加時,這個唯一的更新回調函數也會變得愈發難以閱讀、維護和修改。關於編寫場景中節點/子樹回調函數的方法,並沒有一定之規。在本例中我們將創建單一的坦克節點回調,這個回調函數將負責更新炮塔和機槍的自由度節點。
為了實現這一回調,我們需要在節點類原有的基礎上添加新的數據。我們需要獲得與炮塔和機槍相關聯的DOF節點的句柄,以更新炮塔旋轉和機槍俯仰的角度值。角度值的變化要建立在上一次變化的基礎上。因為回調是作為場景遍歷的一部分進行初始化的,我們所需的參數通常只有兩個:一個是與回調相關聯的節點指針,一個是用於執行遍歷的節點訪問器指針。為了獲得更多的參數數據(炮塔和機槍DOF的句柄,旋轉和俯仰角度值),我們可以使用節點類的userData數據成員。userData是一個指向用戶定義類的指針,其中包含了關聯某個特定節點時所需的一切數據集。而對於用戶自定義類,只有一個條件是必需的,即,它必須繼承自osg::Referenced類。Referenced類提供了智能指針的功能,用於協助用戶管理內存分配。智能指針記錄了分配給一個類的實例的引用計數值。這個類的實例只有在引用計數值到達0的時候才會被刪除。有關osg::Referenced的更詳細敘述,請參閱本章后面的部分。基於上述的需求,我們向坦克節點添加如下的代碼:
- class tankDataType : public osg::Referenced
- {
- public:
- //公有成員
- protected:
- osgSim::DOFTransform* tankTurretNode;
- osgSim::DOFTransform* tankGunNode;
- double rotation; //(弧度值)
- double elevation; //(弧度值)
- };
為了正確實現tankData類,我們需要獲取DOF節點的句柄。這一工作可以在類的構造函數中使用前一教程所述的findNodeVisitor類完成。findNodeVisitor將從一個起始節點開始遍歷。本例中我們將從表示坦克的子樹的根節點開始執行遍歷,因此我們需要向tankDataType的構造函數傳遞坦克節點的指針。因此,tankDataType類的構造函數代碼應當編寫為:(向特定節點分配用戶數據的步驟將隨后給出)
- tankDataType::tankDataType(osg::Node* n)
- {
- rotation = 0;
- elevation = 0;
- findNodeVisitor findTurret("turret");
- n->accept(findTurret);
- tankTurretNode = dynamic_cast <osgSim::DOFTransform*> (findTurret.getFirst());
- findNodeVisitor findGun("gun");
- n->accept(findGun);
- tankGunNode = dynamic_cast< osgSim::DOFTransform*> (findGun.getFirst());
- }
我們也可以在tankDataType類中定義更新炮塔旋轉和機槍俯仰的方法。現在我們只需要簡單地讓炮塔和機槍角度每幀改變一個固定值即可。對於機槍的俯仰角,我們需要判斷它是否超過了實際情況的限制值。如果達到限制值,則重置仰角為0。炮塔的旋轉可以在一個圓周內自由進行。
- void tankDataType::updateTurretRotation() //控制坦克的炮塔將旋轉不同的角度
- {
- rotation += 0.01;
- tankTurretNode->setCurrentHPR( osg::Vec3(rotation,0,0) );
- }
- void tankDataType::updateGunElevation() //控制坦克的槍管在y方向上的仰角,控制槍管的升降
- {
- elevation += 0.01;
- tankGunNode->setCurrentHPR( osg::Vec3(0,elevation,0) );
- if (elevation > 0.5)
- elevation = 0.0;
- }
將上述代碼添加到類的內容后,我們新定義的類如下所示:
- class tankDataType : public osg::Referenced
- {
- public:
- tankDataType(osg::Node*n);
- void updateTurretRotation();
- void updateGunElevation();
- protected:
- osgSim::DOFTransform* tankTurretNode;
- osgSim::DOFTransform* tankGunNode;
- double rotation; //(弧度值)
- double elevation; //(弧度值)
- };
下一個步驟是創建回調,並將其關聯到坦克節點上。為了創建這個回調,我們需要重載“()”操作符,它包括兩個參數:節點的指針和節點訪問器的指針。在這個函數中我們將執行DOF節點的更新。因此,我們需要執行tankData實例的更新方法,其中tankData實例使用坦克節點的userData成員與坦克節點相關聯。坦克節點的指針可以通過使用getUserData方法來獲取。由於這個方法的返回值是一個osg::Referenced基類的指針,因此需要將其安全地轉換為tankDataType類的指針。為了保證用戶數據的引用計數值是正確的,我們使用模板類型osg::ref_ptr<tankDataType>指向用戶數據。整個類的定義如下:
- class tankNodeCallback : public osg::NodeCallback
- {
- public:
- virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
- {
- osg::ref_ptr<tankDataType> tankData = dynamic_cast<tankDataType*> (node->getUserData() );
- if(tankData)
- {
- tankData->updateTurretRotation();
- tankData->updateGunElevation();
- }
- traverse(node, nv);
- }
- };
下一步的工作是“安裝”回調:將其關聯給我們要修改的坦克節點,以實現每幀的更新函數執行。因此,我們首先要保證坦克節點的用戶數據(tankDataType類的實例)是正確的。然后,我們使用osg::Node類的setUpdateCallback方法將回調與正確的節點相關聯。代碼如下所示:
- // 初始化變量和模型,建立場景
- osg::ref_ptr<osg::Node> tankNode = osgDB::readNodeFile("t72-tank_des.flt");
- tankDataType* tankData = new tankDataType(tankNode);
- tankNode->setUserData( tankData );
- tankNode->setUpdateCallback(new tankNodeCallback);
創建了回調之后,我們進入仿真循環。仿真循環的代碼不用加以改變。當我們調用視口類實例的frame()方法時,我們即進入一個更新遍歷。當更新遍歷及至坦克節點時,將觸發tankNodeCallback類的操作符“()”函數。
完整的源程序代碼如下:
- #include <osgViewer/Viewer>
- #include <osgDB/ReadFile>
- #include <osg/NodeVisitor>
- #include <osg/Node>
- #include <osg/Group>
- #include <osgSim/DOFTransform>
- #include <osgUtil/Optimizer>
- #include <osg/NodeVisitor>
- #include <iostream>
- #include <vector>
- //模型中使用DOF節點,以便清晰表達坦克的某個部分。例如炮塔節點可以旋轉,機槍節點可以升高
- class findNodeVisitor : public osg::NodeVisitor
- {
- public:
- findNodeVisitor();
- findNodeVisitor(const std::string &searchName) ;
- virtual void apply(osg::Node &searchNode);
- virtual void apply(osg::Transform &searchNode);
- void setNameToFind(const std::string &searchName);
- osg::Node* getFirst();
- typedef std::vector<osg::Node*> nodeListType;
- nodeListType& getNodeList() { return foundNodeList; }
- private:
- std::string searchForName;
- nodeListType foundNodeList;
- };
- findNodeVisitor::findNodeVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN),searchForName()
- {
- }
- findNodeVisitor::findNodeVisitor(const std::string &searchName):osg::NodeVisitor(TRAVERSE_ALL_CHILDREN),searchForName(searchName)
- {
- }
- void findNodeVisitor::setNameToFind(const std::string &searchName)
- {
- searchForName = searchName;
- foundNodeList.clear();
- }
- osg::Node* findNodeVisitor::getFirst()
- {
- return *(foundNodeList.begin());
- }
- void findNodeVisitor::apply(osg::Node &searchNode)
- {
- if (searchNode.getName() == searchForName)
- {
- foundNodeList.push_back(&searchNode);
- }
- traverse(searchNode);
- }
- void findNodeVisitor::apply(osg::Transform &searchNode)
- {
- osgSim::DOFTransform* dofNode =
- dynamic_cast<osgSim::DOFTransform*> (&searchNode);
- if (dofNode)
- {
- dofNode->setAnimationOn(false);
- }
- apply ( (osg::Node&) searchNode);
- traverse(searchNode);
- }
- class tankDataType : public osg::Referenced
- {
- public:
- tankDataType(osg::Node*n);
- void updateTurretRotation();
- void updateGunElevation();
- protected:
- osgSim::DOFTransform* tankTurretNode;
- osgSim::DOFTransform* tankGunNode;
- double rotation; //(弧度值)
- double elevation; //(弧度值)
- };
- tankDataType::tankDataType(osg::Node* n)
- {
- rotation = 0;
- elevation = 0;
- findNodeVisitor findTurret("turret");
- n->accept(findTurret);
- tankTurretNode = dynamic_cast <osgSim::DOFTransform*> (findTurret.getFirst());
- findNodeVisitor findGun("gun");
- n->accept(findGun);
- tankGunNode = dynamic_cast< osgSim::DOFTransform*> (findGun.getFirst());
- }
- void tankDataType::updateTurretRotation() //控制坦克的炮塔將旋轉不同的角度
- {
- rotation += 0.02;
- tankTurretNode->setCurrentHPR( osg::Vec3(rotation,0,0) );
- }
- void tankDataType::updateGunElevation() //控制坦克的槍管在y方向上的仰角,控制槍管的升降
- {
- //elevation += 0.02; //控制坦克的槍管在y方向上的升降
- tankGunNode->setCurrentHPR( osg::Vec3(0,elevation,0) );
- if (elevation > 0.5)
- elevation = 0.0;
- }
- class tankNodeCallback : public osg::NodeCallback
- {
- public:
- virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
- {
- osg::ref_ptr<tankDataType> tankData = dynamic_cast<tankDataType*> (node->getUserData() );
- if(tankData)
- {
- tankData->updateTurretRotation();
- tankData->updateGunElevation();
- }
- traverse(node, nv);
- }
- };
- int main(void)
- {
- // 初始化變量和模型,建立場景
- osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer();
- osg::ref_ptr<osg::Group> root = new osg::Group();
- osg::ref_ptr<osg::Node> tankNode = osgDB::readNodeFile("t72-tank_des.flt");
- tankDataType* tankData = new tankDataType(tankNode);
- tankNode->setUserData( tankData );
- tankNode->setUpdateCallback(new tankNodeCallback);
- root->addChild(tankNode.get());
- //優化場景數據
- osgUtil::Optimizer optimizer;
- optimizer.optimize(root.get());
- //設置場景數據
- viewer->setSceneData(root.get());
- //初始化並創建窗口
- viewer->realize();
- //開始渲染
- viewer->run();
- return 0;
- }
最終的效果圖如下所示:
[]
