OSG——- 对点选物体平移(鼠标点选物体、物体随鼠标移动、屏幕坐标转世界坐标)
之前的一篇博文是有一篇对点选物体进行平移、缩放旋转。那一篇是很简单的调用了OSG中定义的一些dragger,但这些dragger都有坐标轴或者tapbox等在模型上,与我最近要做的事情的需求不同。网上也找了好久,最终还是自己一点一点,参考老师之前写的代码,然后在亲爱的童童师兄的帮助下,实现了这么一个小小的功能。虽然代码写的乱糟糟的。
这里就简单记录一下整个思路吧。
1、构造一个事件处理器,也就是MouseDragHandler,来处理鼠标拖曳事件。
#include "common.h"
class MouseDragHandler : public osgGA::GUIEventHandler
{
public:
MouseDragHandler();
virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa);
void pick(float x, float y);
osg::Vec3 screen2World(float x, float y);
bool PickObject;
osg::ref_ptr<osg::MatrixTransform> picked;
osgViewer::View* view;
protected:
virtual ~MouseDragHandler();
};
这个类继承了osgGA::GUIEventHanlder,在之前的博客中有详细讲过如何处理事件。主要核心就在于对这里的handle函数的重写,以实现用户自定义的事件处理。
(1)鼠标点选物体——Pick函数
要拖曳也一个模型,首先要选中它,所以这里设置了一个pick函数,pick函数中判断鼠标点到了哪个模型,就将这个模型保存到这个类的公有变量 picked里。
现在来看看这个pick(float x, float y),大概的原理就是从当前鼠标点击的位置,向着屏幕内垂直发射一条射线,将射线交到的节点保存下来。这里对Pick函数的详解在前面的博文里面也有记录。
void MouseDragHandler::pick(float x, float y)
{
osgUtil::LineSegmentIntersector::Intersections intersections;
if (view->computeIntersections(x, y, intersections))
{
osgUtil::LineSegmentIntersector::Intersections::iterator hitr = intersections.begin();
osg::NodePath getNodePath = hitr->nodePath;
for (int i = getNodePath.size() - 1; i >= 0; --i)
{
osg::MatrixTransform* mt = dynamic_cast<osg::MatrixTransform*>(getNodePath[i]);
if (mt == NULL)
{
continue;
}
else
{
PickObject = true;
picked = mt;
}
}
}
else
{
PickObject = false;
}
}
函数参数传入了当前鼠标在屏幕的位置,然后构造了一个线段的碰撞检测结果集变量intersections。
使用OSG中的库函数osgViewer::View::computeIntersections来解决焦点计算问题。computeIntersections(x, y, intersections),参数x,y是鼠标在屏幕上的位置,intersections存放了与射线相交的节点以及相交的节点路径等相关信息。这里我只考虑了交到的第一个节点。也就是intersections.begin()。
然后获取这个节点的nodePath。这里一开始我也没有弄清楚,不知道为什么要存放这个节点路径。老师讲了之后就明白了,我们的确是判断交到了第一个节点,但事实上一个节点不一定就是直接存在在场景中的,它可能是挂在root节点下的某一个子节点下的一个子节点。这里我需要找到它的MatrixTransform节点,从而来对这个节点进行平移。所以要遍历所交到的第一个节点的节点路径。
这里的的这句语句osg::MatrixTransform* mt = dynamic_cast<osg::MatrixTransform*>(getNodePath[i]);实际上就是在判断是否是MatrixTransform节点。这个dynamic_cast也很有意思,如果所获得的节点不能动态转换为MatrixTransform,则mt为空,这样就可以帮助我们找到所需的这个mt节点。
若mt不为空,则判断交到了可以移动的模型,置PickObject为true,将mt赋给成员变量picked。
(2)屏幕坐标转世界坐标:screen2World(float x, float y)
这个函数是在看了一遍又一遍的书,然后又参考了别人的代码才写出来的。将场景从三维世界到屏幕窗口需要经历:模型视图变换、投影变换、视口变换。将这些变换过程进行叠加,获得一个MVPW矩阵,这个矩阵决定了任意三维空间中的对象在二维屏幕上表达时的变换方式。它的逆矩阵,决定了二维屏幕上某一点变换到三维世界的过程。
osg::Vec3 MouseDragHandler::screen2World(float x, float y)
{
osg::Vec3 vec3;
osg::ref_ptr<osg::Camera> camera = view->getCamera();
osg::Vec3 vScreen(x, y, 0);
osg::Matrix mVPW = camera->getViewMatrix() * camera->getProjectionMatrix() * camera->getViewport()->computeWindowMatrix();
osg::Matrix invertVPW;
invertVPW.invert(mVPW);
vec3 = vScreen * invertVPW;
return vec3;
}
(3)处理物体随鼠标移动的事件,也就是最重要的handle函数了
bool MouseDragHandler::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
{
view = dynamic_cast<osgViewer::View*>(&aa);
if (!view) return false;
osgGA::EventQueue* queue = view->getEventQueue();
switch (ea.getEventType())
{
case osgGA::GUIEventAdapter::PUSH:
{
if (view)
{
int button = ea.getButton();
if (button == osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON)
{
lbuttonDown = true;
pick(ea.getX(), ea.getY());
if (PickObject)
{
osg::Vec3 vec1 = screen2World(ea.getX(), ea.getY());
first_point = { vec1.x(), vec1.z() };
originPos = picked->getMatrix();
}
}
else
{
lbuttonDown = false;
}
}
return false;
}
case osgGA::GUIEventAdapter::DRAG:
{
if (PickObject&&lbuttonDown)
{
osg::Vec3 vec2 = screen2World(ea.getX(), ea.getY());
last_point = { vec2.x(), vec2.z() };
float dx = last_point.x - first_point.x;
float dy = last_point.y - first_point.y;
std::cout<< dx << " " << dy << std::endl;
if (fabs(dx) + fabs(dy) < 0.06)
return false;
picked->setMatrix(originPos*osg::Matrix::translate(dx, 0, dy));
}
return false;
}
case osgGA::GUIEventAdapter::RELEASE:
{
PickObject = false;
return false;
}
default:
return false;
}
}
这里就用到了屏幕坐标到世界坐标的转变,一开始的时候我直接将两次鼠标的位置做了差值,将差值传到tranlate函数中设置成变换矩阵。模型的偏移和鼠标的偏移就对不上,分析了一下觉得肯定是不能这样子算的。所以将两次获得的鼠标值分别转换到世界坐标,再做差值,就能获得正确的偏移量。
这里我只是实现了物体在x,y方向的平移。
这里事件处理流程是这样的:首先判断鼠标左键按下,通过ea获取当前鼠标位置,判断是否交到了物体、若交到了,将屏幕坐标转成世界坐标,并记录在first_point里,再获取模型的初始位置矩阵,这样每一次模型都是在原有基础上进行变换。之前没有在这里获取模型初始位置originPos,后面的setMatrix也没有乘,所以总是松开鼠标再次点击物体时,物体又会回到最开始的位置。接着判断Drag事件,再次获取当前的鼠标位置,并转成世界坐标保存到last_point中,求出dx,dy,通过picked->setMatrix(originPos*osg::Matrix::translate(dx, 0, dy));来进行空间变化矩阵的设置。这里为什么要把dy放在第三个变量的位置,是因为在OSG中的坐标是x向右,y向屏幕里,z向上。
2、这里MouseDragHandler讲完了,我自己设了个Camera manipulator,因为不想让相机动,OSG会默认设置一个Trackball Manipulator我不需要,也不知道怎么取消。应该是不能取消,需要自己设置吧。懒得贴代码了。就是把之前博文里的简单操作器进行了修改。