0、环境配置和简介
OSG要配置的库还挺多的,自己编译起来,说实话很麻烦,当时搞了很久,后面用的别人弄好的库,具体配置方法的链接放这里:环境搭建。
OsgRecipes项目我是在Windows,用cmake-gui编译的,当时也是挺多问题的,后面是问AI一个个解决的。
这个OsgRecipes项目用到的第三方库很多,目前还在探索中,其实本人也刚接触OSG不久,感觉这个项目会比较有意思,所以记录一下思路和问题,输出倒逼自己输入,同时也算是边玩边学~
编译好后,打开解决方案,会发现里面都是cookbook(循序渐进的感觉,有点像LearnOpenGL,感兴趣的话可以看下,我之前实践这个项目的文章LearnOpenGL),我去搜了下,为啥叫cookbook菜谱,其实也算是去烹饪去使用OSG这个菜系的方法,从而做出自己想要的菜,想要的效果。
1、cookbook_02_01代码解析
其实很多程序运行起来都是全屏的,很不方便调试,这里我们改一下窗口的大小,首先在类PickHandler添加一个bool成员变量m_first,表示是否第一次设置大小,避免重复设置。具体代码如下
if (m_first)
{
m_first = false;
osgViewer::Viewer* viewer = dynamic_cast<osgViewer::Viewer*>(&aa); //获取窗口句柄
osgViewer::Viewer::Windows ws;
viewer->getWindows(ws);
if (!ws.empty())
{
osgViewer::Viewer::Windows::iterator iter = ws.begin();
for (; iter != ws.end(); iter++) //遍历所有关联窗口设置大小
{
(*iter)->setWindowRectangle(500, 100, 900, 800);
}
}
}
欧克,先简要分析下observer_ptr.cpp的main函数,主要出现了五六种类。
首先类osg::Geode,它就像画布,比如这里在osg::Geode画布上添加了osgText::Text文本元素,以及两个几何体。
然后类osg::Camera,顾名思义决定了我们能看到的东西,这里的话是创建了一个平视显示(HUD)相机,用来显示文本对象的。
类osg::Group的话,就像电脑中的文件夹,可以包含其他的Group和Geode,就像文件夹包含子文件夹和文件。而类ObserveShapeCallback继承的NodeCallback类似于监控摄像头,物体变化时用来实时响应,这里的话文本是和几何体建立了联系。
类osgViewer::Viewer就好比是电影院的放映机,负责协调所有组件并且将最终画面呈现出来,管理着渲染循环和事件处理等功能。
然后看下具体创建文本的函数,代码如下
osgText::Text* createText( const osg::Vec3& pos, const std::string& content, float size )
{
osg::ref_ptr<osgText::Text> text = new osgText::Text;
text->setDataVariance( osg::Object::DYNAMIC ); //变化模式设为动态
text->setFont( g_font.get() );
text->setCharacterSize( size );
text->setAxisAlignment( osgText::TextBase::XY_PLANE );
text->setPosition( pos );
text->setText( content );
return text.release();
}
其实这里的指针osg::ref_ptr类似于STL的std::shared_ptr,是强引用会自动计数,而后面将会出现的指针osg::observer_ptr是弱引用,仅观察不会计数,类似于STL的std::weak_ptr。然后看下创建HUD相机的函数
osg::Camera* createHUDCamera( double left, double right, double bottom, double top )
{
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setReferenceFrame( osg::Transform::ABSOLUTE_RF ); //使用绝对坐标系,不受父节点变换影响
camera->setClearMask( GL_DEPTH_BUFFER_BIT );
camera->setRenderOrder( osg::Camera::POST_RENDER ); //后渲染,覆盖在主场景上
camera->setAllowEventFocus( false );
camera->setProjectionMatrix( osg::Matrix::ortho2D(left, right, bottom, top) ); //2D视口范围
camera->getOrCreateStateSet()->setMode( GL_LIGHTING, osg::StateAttribute::OFF );
return camera.release();
}
这里的left, right, bottom, top对应的数据,一开始会让我比较费解,这个其实是将这里设置的矩形视口范围映射到标准设备坐标(NDC)中,比如这里的
(0,800,0,600)会将x∈[0,800]映射到NDC的[-1,1],y∈[0,600]同理。因此设置里面的文本对象位置时,setPosition的参数或者文本位置这些是和(0,800,0,600)有很大关联的。
接下来我们看下ObserveShapeCallback类,主要代码如下
virtual void operator()( osg::Node* node, osg::NodeVisitor* nv )
{
std::string content;
if ( _drawable1.valid() ) content += "Drawable 1; ";
if ( _drawable2.valid() ) content += "Drawable 2; ";
if ( _text.valid() ) _text->setText( content );
traverse(node, nv);
}
除了对文本进行一个赋值外,里面的traverse(node, nv)在这里是可以省略的,因为它是用来遍历子节点的,而这里只是修改了当前节点的文本内容。
我们再看下事件类RemoveShapeHandler,首先看下它的基类PickHandler,它的基类是osgGA::GUIEventHandler,如同OSG的场控系统。看下虚函数handle的具体实现,主要代码如下
//同时按住ctrl键和鼠标左键,释放时触发
if ( ea.getEventType()!=osgGA::GUIEventAdapter::RELEASE ||
ea.getButton()!=osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON ||
!(ea.getModKeyMask()&osgGA::GUIEventAdapter::MODKEY_CTRL) )
return false;
osgViewer::View* viewer = dynamic_cast<osgViewer::View*>(&aa);
if ( viewer )
{
osg::ref_ptr<osgUtil::LineSegmentIntersector> intersector =
new osgUtil::LineSegmentIntersector(osgUtil::Intersector::WINDOW, ea.getX(), ea.getY());
osgUtil::IntersectionVisitor iv( intersector.get() );
viewer->getCamera()->accept( iv ); //通过访问器模式,执行鼠标射线碰撞检测
if ( intersector->containsIntersections() )
{
//获取首个命中结果
osgUtil::LineSegmentIntersector::Intersection result = *(intersector->getIntersections().begin());
doUserOperations( result );
}
}
这里实现的事件是同时按住CTRL键和鼠标左键,松掉时会触发后续的碰撞检测:鼠标射线和物体的是否存在交集,若存在则调用子类的doUserOperations。紧接着,我们看下其子类RemoveShapeHandler实现的doUserOperations方法
virtual void doUserOperations( osgUtil::LineSegmentIntersector::Intersection& result )
{
//输出信息,方便理解result.nodePath
for (unsigned i = 0; i < result.nodePath.size(); ++i) {
std::cout << "Node " << i << ": "
<< result.nodePath[i]->className() << std::endl;
}
std::cout << "Hit Drawable: " << result.drawable->className() << std::endl;
if ( result.nodePath.size()>0 ) //场景节点路径
{
osg::Geode* geode = dynamic_cast<osg::Geode*>( result.nodePath.back() );
if ( geode ) geode->removeDrawable( result.drawable.get() );
}
}
首先这里的输出有两种,如果点击几何体的话是,点击文本的话是
。这是因为点击操作的起点,是当前渲染的视口相机节点,对应的Node0,Node1的话对应的类osg::Group对应root,作为场景图的入口,后面便是实际点击到的叶节点,而文本会多一层,是因为它在HUD相机对象中。相应的,就能理解后续为什么可以移除掉点击的对象了~
2、cookbook_02_01项目拓展
后续想着既然文本和几何体是绑定在一起的,那能不能将文本修改为动态变化的?
于是就将固定的文本改成了相机视角中心与对应几何体中心的距离。主要修改下operator(),代码如下
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
if (!_text || !_camera) return;
//获取相机视角中心的世界坐标
osg::Vec3 eye, center, up;
_camera->getViewMatrixAsLookAt(eye, center, up);
std::stringstream ss;
ss.precision(2);
ss.setf(std::ios::fixed);//定点表示法,而非科学计数法
float dist1 = 0.f, dist2 = 0.f;
if (_drawable1.valid())
{
osg::ShapeDrawable* shape1 = dynamic_cast<osg::ShapeDrawable*>(_drawable1.get());
if (shape1)
{
osg::Vec3 center1 = shape1->getBound().center();
dist1 = (center1 - center).length(); //欧式距离
ss << "Box距离: " << dist1 << "; ";
}
}
if (_drawable2.valid())
{
osg::ShapeDrawable* shape2 = dynamic_cast<osg::ShapeDrawable*>(_drawable2.get());
if (shape2)
{
osg::Vec3 center2 = shape2->getBound().center();
dist2 = (center2 - center).length();
ss << "距离: " << dist2;
}
}
osgText::Font* font = osgText::readFontFile("simhei.ttf"); //不设置的话,中文会乱码
if (font) {
_text->setFont(font);
}
std::wstringstream wss;
wss << L"立方体距离: " << dist1 << "; ";
wss << L"球体距离: " << dist2;
std::wstring ws = wss.str();
wchar_t* result = new wchar_t[ws.size() + 1];
wcscpy(result, ws.c_str());
_text->setText(result);
_text->setCharacterSize(20.0f);
_text->setBackdropType(osgText::Text::OUTLINE);
//traverse(node, nv);
}
其实一开始老是出现中文乱码的情况,后面把std::string的result换成wchar_t*,并且设置成simhei.ttf的字体才解决。最终效果如下
最后,水平有限,欢迎指正和交流~觉得有用的话点点赞或收藏吧~
喜欢的话,点点关注吧~ 作者会一直更新的!