目录
[1. 需求的提出](#1. 需求的提出)
[2. 具体实现](#2. 具体实现)
[2.1. 禁止场景跟随鼠标转动](#2.1. 禁止场景跟随鼠标转动)
[2.2. 矩形框前置绘制](#2.2. 矩形框前置绘制)
[3. 附加说明](#3. 附加说明)
[3.1. 颜色设置说明](#3.1. 颜色设置说明)
1. 需求的提出
有时需要在屏幕通过按住键盘上的某个键如Ctrl键且按住鼠标左键,拖出一个矩形,实现框选三维物体,如下效果:
现在的问题是:
- 在osg中,拖动鼠标时,物体会随鼠标一起转动,这样框选是不行的,至少是不友好的,我们需要的是,按住鼠标框选时,物体不能随鼠标一起转动。
- 如何根据鼠标拖动的起始点和终止点,绘制出这个矩形框?矩形框要在所有三维物体的前面而不能被三维物体遮挡且要是透明的,能透过它看到背后的三维物体,否则框选就失去了意义。
- 按住鼠标右键,矩形框消失。
2. 具体实现
2.1. 禁止场景跟随鼠标转动
对第1节中提到的第1个问题,默认情况下osgViewer::Viewer 事件处理器在鼠标左键按下并拖动时,整个场景会随鼠标一起转动。为了不让转动,可以通过改写osgViewer::Viewer 的osgGA::GUIEventHandler事件处理器,重载如下方法:
cpp
virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa, osg::Object* obj, osg::NodeVisitor*nv)
当按住键盘上的某个键如Ctrl键且按住鼠标左键,让该函数返回true,这样后续的流程就不会处理鼠标拖动事件,三维物体也就不会跟随鼠标旋转了。
2.2. 矩形框前置绘制
矩形框要绘制在所有三维物体的前面而不能被三维物体遮挡,这就要用到三维中的HUD技术(Head Up Display)。所谓HUD节点,说白了就是无论三维场景中的内容怎么改变,它都能在屏幕上固定位置显示的节点。实现要点:
- 关闭光照,不受场景光照影响,所有内容以同一亮度显示。
- 关闭深度测试。
- 调整渲染顺序,使它的内容最后绘制。
- 设定参考贴为绝对型:setReferenceFrame(osg::Transform:ABSOLUTE_RF)。
- 使其不受父节点变换的影响:setMatrix(osg::Matrix::identity())。
- 使用平行投影,设定虚拟投影窗口的大小,这个窗口的大小决定了后面绘制的图形和文字的尺度比例。
实现代码如下:
cpp
#include<osgViewer/Viewer>
#include<osg/ShapeDrawable>
#include<osgDB/readFile>
#include<osg/BlendFunc>
class selectBoxEventHandler: public osgGA::GUIEventHandler
{
public:
selectBoxEventHandler(osg::ref_ptr<osg::Camera> spHudCamera)
{
m_spHudCamera = spHudCamera;
}
private:
virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa, osg::Object* obj, osg::NodeVisitor*nv)
{
m_pViewer = (osgViewer::Viewer*)(&aa);
if (m_pViewer == nullptr)
{
return false;
}
auto width = m_pViewer->getCamera()->getViewport()->width();
auto height = m_pViewer->getCamera()->getViewport()->height();
/* 设置HUD相机为正投影,这样绘制的矩形框和鼠标拖动的框选框大小就一样了
且要设置正投影的区域和视图窗体一样大小,因为鼠标可以在窗体任何位置进行框选
*/
m_spHudCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, width, 0, height));
auto eventType = ea.getEventType();
switch (eventType)
{
case osgGA::GUIEventAdapter::KEYDOWN:
{
if ((osgGA::GUIEventAdapter::KEY_Control_L == ea.getKey())
|| (osgGA::GUIEventAdapter::KEY_Control_R == ea.getKey())) // Ctrl键被按下
{
m_ctrlKeyPressed = true;
}
}
break;
case osgGA::GUIEventAdapter::KEYUP:
{
if ((osgGA::GUIEventAdapter::KEY_Control_L == ea.getKey())
|| (osgGA::GUIEventAdapter::KEY_Control_R == ea.getKey())) // Ctrl键被释放
{
m_ctrlKeyPressed = false;
}
}
break;
case osgGA::GUIEventAdapter::PUSH: // 鼠标左键按下
{
auto buttonMask = ea.getButtonMask();
auto bIsMouseBtn = buttonMask & osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON;
if (bIsMouseBtn)
{
m_fStartPosX = ea.getX();
m_fStartPosY = ea.getY();
m_bPush = true;
}
else if (buttonMask & osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON) // 鼠标右键按下,则删除选择框
{
if (m_spOldNode != nullptr)
{
m_spHudCamera->removeChild(m_spOldNode);
}
}
}
break;
case osgGA::GUIEventAdapter::RELEASE: // 释放鼠标左键
{
m_bPush = false;
}
break;
case osgGA::GUIEventAdapter::DRAG: // 拖动鼠标
{
auto buttonMask = ea.getButtonMask();
auto bIsMouseBtn = buttonMask & osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON;
if (bIsMouseBtn && m_ctrlKeyPressed && m_bPush)
{
m_fEndPosX = ea.getX();
m_fEndPosY = ea.getY();
auto pSelectBox = createSelectBox(m_fStartPosX, m_fStartPosY, m_fEndPosX, m_fEndPosY);
if (m_spOldNode != nullptr)
{
m_spHudCamera->removeChild(m_spOldNode);
}
m_spHudCamera->addChild(pSelectBox);
m_spOldNode = pSelectBox;
return true;
}
}
} // end swith
return false;
}
osg::Geode* createSelectBox(float fStartPosX, float fStartPosY, float fEndPosX, float fEndPosY)
{
osg::Geode* pGeode = new osg::Geode();
auto pQuardGeomerty = new osg::Geometry();
pGeode->addChild(pQuardGeomerty);
osg::Vec3Array* pVertArray = new osg::Vec3Array;
pVertArray->push_back(osg::Vec3(fStartPosX, fStartPosY, 0.0));
pVertArray->push_back(osg::Vec3(fStartPosX, fEndPosY, 0.0));
pVertArray->push_back(osg::Vec3(fEndPosX, fEndPosY, 0.0));
pVertArray->push_back(osg::Vec3(fEndPosX, fStartPosY, 0.0));
pQuardGeomerty->setVertexArray(pVertArray);
osg::Vec4Array* pColorArray = new osg::Vec4Array;
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.4));
/* pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.40));
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.40));
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.40));*/
pQuardGeomerty->setColorArray(pColorArray);
//pQuardGeomerty->setColorBinding(osg::Geometry::AttributeBinding::BIND_PER_VERTEX);
pQuardGeomerty->setColorBinding(osg::Geometry::AttributeBinding::BIND_PER_PRIMITIVE_SET);
pQuardGeomerty->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4));
pGeode->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); // 关闭光照
pGeode->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); // 开启透明,否则就不能透过选择框看到后面的牛
pGeode->addDrawable(pQuardGeomerty);
return pGeode;
}
private:
/* 鼠标按下的起始坐标点 */
float m_fStartPosX{0.0};
float m_fStartPosY{ 0.0 };
float m_fEndPosX{ 0.0 };
float m_fEndPosY{ 0.0 };
bool m_ctrlKeyPressed{false}; // Ctrl键被按下
bool m_bPush{false}; // 鼠标左键是否被按下
osgViewer::Viewer* m_pViewer{nullptr};
osg::ref_ptr<osg::Camera> m_spHudCamera; // 用于HUD的相机
osg::ref_ptr<osg::Node> m_spOldNode; // 上次鼠标框选绘制出的矩形框
};
int main(int argc, char *argv[])
{
osgViewer::Viewer viewer;
auto cowNode = osgDB::readNodeFile(R"(E:\osg\OpenSceneGraph-Data\cow.osg)");
if (nullptr == cowNode)
{
OSG_WARN << "node is null!";
return 1;
}
auto spRoot = new osg::Group();
osg::ref_ptr<osg::Camera> spHudCamera = new osg::Camera;
spHudCamera->setClearMask(GL_DEPTH_BUFFER_BIT); // 关闭深度缓冲
// 设置渲染顺序为后渲染,即始终在其它绘制物体的上面,防止被其它绘制的物体遮挡
spHudCamera->setRenderOrder(osg::Camera::RenderOrder::POST_RENDER);
spHudCamera->setAllowEventFocus(false); // 不接受任何焦点事件,即不响应键盘、鼠标事件
spHudCamera->setReferenceFrame(osg::Transform::ReferenceFrame::ABSOLUTE_RF); // 设置参考帧为绝对帧
spHudCamera->setViewMatrix(osg::Matrix::identity()); // 设置相机视图矩阵为单位矩阵,这样就矩形框选框就不受相机旋转等变换影响
spRoot->addChild(cowNode);
spRoot->addChild(spHudCamera);
viewer.setSceneData(spRoot);
viewer.addEventHandler(new selectBoxEventHandler(spHudCamera));
return viewer.run();
}
3. 附加说明
3.1. 颜色设置说明
2.2节代码对颜色的设置,也可以按如下代码一样达到同样的效果:
cpp
osg::Vec4Array* pColorArray = new osg::Vec4Array;
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.4));
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.40));
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.40));
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.40));
pQuardGeomerty->setColorArray(pColorArray);
pQuardGeomerty->setColorBinding(osg::Geometry::AttributeBinding::BIND_PER_VERTEX);
也就是说设置一个顶点的颜色且颜色绑定方式为BIND_PER_PRIMITIVE_SET和分别设置4个顶点颜色,颜色绑定方式为BIND_PER_VERTEX效果相同。关于BIND_PER_PRIMITIVE_SET和BIND_PER_VERTEX的具体含义和不同点,请参考:osg图元绑定方式总结博文。
3.2.矩形框显示和隐藏的另一种实现
上面矩形框的显示和隐藏是通过removeChild和addChild函数来实现的,即将新的矩形框节点加入到相机作为其子节点之前,删除上次创建的矩形框节点。也可以通过osg::Node的setNodeMask函数来实现,如下为更改后的代码:
cpp
#include<osgViewer/Viewer>
#include<osg/ShapeDrawable>
#include<osgDB/readFile>
#include<osg/BlendFunc>
#define HIDE_SELECT_BOX 0X0
#define SHOW_SELECT_BOX ~HIDE_SELECT_BOX
class selectBoxEventHandler : public osgGA::GUIEventHandler
{
public:
selectBoxEventHandler(osg::ref_ptr<osg::Camera> spHudCamera)
{
m_spHudCamera = spHudCamera;
}
private:
virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa, osg::Object* obj, osg::NodeVisitor* nv)
{
m_pViewer = (osgViewer::Viewer*)(&aa);
if (m_pViewer == nullptr)
{
return false;
}
auto width = m_pViewer->getCamera()->getViewport()->width();
auto height = m_pViewer->getCamera()->getViewport()->height();
/* 设置HuD相机为正投影,这样绘制的矩形框和鼠标拖动的框选框大小就一样了
且要设置正投影的区域和视图窗体一样大小,因为鼠标可以在窗体任何位置进行框选
*/
m_spHudCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, width, 0, height));
auto eventType = ea.getEventType();
switch (eventType)
{
case osgGA::GUIEventAdapter::KEYDOWN:
{
if ((osgGA::GUIEventAdapter::KEY_Control_L == ea.getKey())
|| (osgGA::GUIEventAdapter::KEY_Control_R == ea.getKey())) // Ctrl键被按下
{
m_ctrlKeyPressed = true;
}
}
break;
case osgGA::GUIEventAdapter::KEYUP:
{
if ((osgGA::GUIEventAdapter::KEY_Control_L == ea.getKey())
|| (osgGA::GUIEventAdapter::KEY_Control_R == ea.getKey())) // Ctrl键被按下
{
m_ctrlKeyPressed = false;
}
}
break;
case osgGA::GUIEventAdapter::PUSH: // 鼠标左键按下
{
auto buttonMask = ea.getButtonMask();
auto bIsMouseBtn = buttonMask & osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON;
if (bIsMouseBtn)
{
m_fStartPosX = ea.getX();
m_fStartPosY = ea.getY();
m_bPush = true;
}
else if (buttonMask & osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON) // 鼠标右键按下,则删除选择框
{
if (m_spRectGeometry != nullptr)
{
m_spRectGeometry->setNodeMask(HIDE_SELECT_BOX);
}
}
}
break;
case osgGA::GUIEventAdapter::RELEASE: // 释放鼠标左键
{
m_bPush = false;
}
break;
case osgGA::GUIEventAdapter::DRAG: // 拖动鼠标
{
auto buttonMask = ea.getButtonMask();
auto bIsMouseBtn = buttonMask & osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON;
if (bIsMouseBtn && m_ctrlKeyPressed && m_bPush)
{
m_fEndPosX = ea.getX();
m_fEndPosY = ea.getY();
if (nullptr != m_spRectGeometry)
{
auto pVertArray = (osg::Vec3Array*)m_spRectGeometry->getVertexArray();
(*pVertArray)[0].set(m_fStartPosX, m_fStartPosY, 0.0);
(*pVertArray)[1].set(m_fStartPosX, m_fEndPosY, 0.0);
(*pVertArray)[2].set(m_fEndPosX, m_fEndPosY, 0.0);
(*pVertArray)[3].set(m_fEndPosX, m_fStartPosY, 0.0);
// m_spRectGeometry->setVertexArray(pVertArray);
m_spRectGeometry->dirtyDisplayList(); // 告知底层,外层顶点数据更改了,否则不会用新的坐标绘制矩形
m_spRectGeometry->setNodeMask(SHOW_SELECT_BOX);
}
else
{
auto spSelectBox = createSelectBox(m_fStartPosX, m_fStartPosY, m_fEndPosX, m_fEndPosY);
m_spRectGeometry = spSelectBox->asGeode()->getChild(0)->asGeometry();
m_spHudCamera->addChild(spSelectBox);
}
return true;
}
}
} // end swith
return false;
}
osg::Geode* createSelectBox(float fStartPosX, float fStartPosY, float fEndPosX, float fEndPosY)
{
osg::Geode* pGeode = new osg::Geode();
auto pQuardGeomerty = new osg::Geometry();
pGeode->addChild(pQuardGeomerty);
osg::Vec3Array* pVertArray = new osg::Vec3Array;
pVertArray->push_back(osg::Vec3(fStartPosX, fStartPosY, 0.0));
pVertArray->push_back(osg::Vec3(fStartPosX, fEndPosY, 0.0));
pVertArray->push_back(osg::Vec3(fEndPosX, fEndPosY, 0.0));
pVertArray->push_back(osg::Vec3(fEndPosX, fStartPosY, 0.0));
pQuardGeomerty->setVertexArray(pVertArray);
osg::Vec4Array* pColorArray = new osg::Vec4Array;
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.4));
/* pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.40));
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.40));
pColorArray->push_back(osg::Vec4(1.0, 1.0, 1.0, 0.40));*/
pQuardGeomerty->setColorArray(pColorArray);
//pQuardGeomerty->setColorBinding(osg::Geometry::AttributeBinding::BIND_PER_VERTEX);
pQuardGeomerty->setColorBinding(osg::Geometry::AttributeBinding::BIND_PER_PRIMITIVE_SET);
pQuardGeomerty->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4));
pGeode->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); // 关闭光照
pGeode->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); // 开启透明,否则就不能透过选择框看到后面的牛
pGeode->addDrawable(pQuardGeomerty);
return pGeode;
}
private:
/* 鼠标按下的起始坐标点 */
float m_fStartPosX{ 0.0 };
float m_fStartPosY{ 0.0 };
float m_fEndPosX{ 0.0 };
float m_fEndPosY{ 0.0 };
bool m_ctrlKeyPressed{ false }; // Ctrl键被按下
bool m_bPush{ false }; // 鼠标左键是否被按下
osgViewer::Viewer* m_pViewer{ nullptr };
osg::ref_ptr<osg::Camera> m_spHudCamera; // 用于HUD的相机
osg::ref_ptr<osg::Geometry> m_spRectGeometry; // 矩形框
};
int main(int argc, char *argv[])
{
osgViewer::Viewer viewer;
auto cowNode = osgDB::readNodeFile(R"(E:\osg\OpenSceneGraph-Data\cow.osg)");
if (nullptr == cowNode)
{
OSG_WARN << "node is null!";
return 1;
}
auto spRoot = new osg::Group();
osg::ref_ptr<osg::Camera> spHudCamera = new osg::Camera;
spHudCamera->setClearMask(GL_DEPTH_BUFFER_BIT); // 开启深度缓冲
// 设置渲染顺序为后渲染,即始终在其它绘制物体的上面,防止被其它绘制的物体遮挡
spHudCamera->setRenderOrder(osg::Camera::RenderOrder::POST_RENDER);
spHudCamera->setAllowEventFocus(false); // 不接受任何焦点事件,即不响应键盘、鼠标事件
spHudCamera->setReferenceFrame(osg::Transform::ReferenceFrame::ABSOLUTE_RF); // 设置参考帧为绝对帧
spHudCamera->setViewMatrix(osg::Matrix::identity()); // 设置相机视图矩阵为单位矩阵,这样就矩形框选框就不受相机旋转等变换影响
spRoot->addChild(cowNode);
spRoot->addChild(spHudCamera);
viewer.setSceneData(spRoot);
viewer.addEventHandler(new selectBoxEventHandler(spHudCamera));
return viewer.run();
}
说明:
- 在鼠标拖动坐标改变重新设置矩形框坐标时,记得调用:
cpp
// 告知底层,外层顶点数据更改了,否则不会用新的坐标绘制矩形
m_spRectGeometry->setVertexArray(pVertArray);
或调用:
cpp
m_spRectGeometry->dirtyDisplayList(); // 告知底层,外层顶点数据更改了,否则不会用新的坐标绘制矩形
在C++中,只要把指针指向的内容改了,也就是立马改了,但在上述代码中如果以为只把顶点数据改了,新矩形就会呈现出来是错误的,不调用上述代码中的某一种,新矩形不会绘制,因为只改了顶点数据,但osg还没执行重绘。
- 上述采用节点的setNodeMask函数,通过设置节点掩码来实现矩形框的显示或隐藏,其实最好的方法是采用osg::Switch来控制矩形框的显示和隐藏,在此不再详述列出代码,关于这两者的详述,参见:osg利用setNodeMask和Switch隐藏节点用法说明博文。