Pick Drag Drop         How to make drag&drop objects

How to make drag&drop objects

Note: This tutorial includes code and algorithms based on the author's knowledge and may be incorrect or less accurate, so please correct all the problems you find.

Introduction

This is a common question at the Ogre forums, and a very useful tool for those who want to make some kind of editor for their applications. The code snippets that follow are parts of my own application and may not work correctly without small tweakings as I did not tested it outside of my application code.

Moving widgets

First of all, I made an arrow mesh in 3D Studio Max, here is a screenshot of it:

move_arrow.png

We have to use this mesh to move the objects, because we need something to intersect with the ray cast we will be using.

Creating the move arrow axis

In order to move a mesh in all 3 axes, we need 3 arrows like the one on the screenshot. The next code is part of my init method:

//move widgets
 try
 {
     //sceneNodes
     SceneNode* node = mSceneMgr->createSceneNode("move_widget");
     SceneNode* node_x = node->createChildSceneNode("move_widget_x");node_x->showBoundingBox(false);
     SceneNode* node_y = node->createChildSceneNode("move_widget_y");node_y->showBoundingBox(false);
     SceneNode* node_z = node->createChildSceneNode("move_widget_z");node_z->showBoundingBox(false);
     
     node_x->yaw(Degree(90));
     node_y->pitch(Degree(-90));
 
     //Materials
     MaterialPtr blue = MaterialManager::getSingleton().create("blue_widget",ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
     blue->setSelfIllumination(0,0,1);
     blue->setAmbient(0,0,0);
     blue->setSpecular(0,0,0,1);
     blue->setDiffuse(0.5,0.5,0.5,1);
     MaterialPtr red = MaterialManager::getSingleton().create("red_widget",ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
     red->setSelfIllumination(1,0,0);
     red->setAmbient(0,0,0);
     red->setSpecular(0,0,0,1);
     red->setDiffuse(0.5,0.5,0.5,1);
     MaterialPtr green = MaterialManager::getSingleton().create("green_widget",ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
     green->setSelfIllumination(0,1,0);
     green->setAmbient(0,0,0);
     green->setSpecular(0,0,0,1);
     green->setDiffuse(0.5,0.5,0.5,1);
 
     //Entities
     Entity* entityZ = mSceneMgr->createEntity("move_widget_z", "arrow.mesh");
     Entity* entityX = mSceneMgr->createEntity("move_widget_x", "arrow.mesh");
     Entity* entityY = mSceneMgr->createEntity("move_widget_y", "arrow.mesh");
 
     MovablePlane *mPlane;
     mPlane = new MovablePlane("dummy_plane_x");
     mPlane->normal = Vector3::UNIT_Y;
     MeshManager::getSingleton().createPlane("dummy_plane_x",ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,*mPlane, 800, 800, 1, 1, true, 1, 1, 1, Vector3::UNIT_X);
     mSceneMgr->createEntity( "dummy_plane_x", "dummy_plane_x" );
     mSceneMgr->getEntity("dummy_plane_x")->setVisible(false);
 
     mPlane = new MovablePlane("dummy_plane_z");
     mPlane->normal = Vector3::UNIT_X;
     MeshManager::getSingleton().createPlane("dummy_plane_z",ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,*mPlane, 800, 800, 1, 1, true, 1, 1, 1, Vector3::UNIT_Y);
 
     mSceneMgr->createEntity( "dummy_plane_z", "dummy_plane_z" );
     mSceneMgr->getEntity("dummy_plane_z")->setVisible(false);
 
     mPlane = new MovablePlane("dummy_plane_y");
     mPlane->normal = Vector3::UNIT_Z;
     MeshManager::getSingleton().createPlane("dummy_plane_y",ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,*mPlane, 800, 800, 1, 1, true, 1, 1, 1, Vector3::UNIT_Y);
     mSceneMgr->createEntity( "dummy_plane_y", "dummy_plane_y" );
     mSceneMgr->getEntity("dummy_plane_y")->setVisible(false);
 
     //ZZ arrows
     entityZ->setNormaliseNormals(true);    
     entityZ->setCastShadows(false);
     entityZ->setMaterialName("blue_widget");
     node_z->attachObject(entityZ);
 
     //XX arrows
     entityX->setNormaliseNormals(true);
     entityX->setCastShadows(false);
     entityX->setMaterialName("red_widget");
     node_x->attachObject(entityX);
 
     //YY arrows
     entityY->setNormaliseNormals(true);
     entityY->setCastShadows(false);
     entityY->setMaterialName("green_widget");
     node_y->attachObject(entityY);
 }
 catch(Ogre::Exception e){std::cout << "An exception has occured while creating widgets: " << e.getFullDescription().c_str() << std::endl;}

axis_arrow.png

Now we have a {LEX()}SceneNode{LEX} named "move_widget" with 3 other nodes attached, one to each axis. You probably noticed that I also create 3 dummy planes.
So the base concept is, you click on an object, the "move_widget" SceneNode appears, and you click on any of the arrows to drag&drop the mesh in the direction you want. But, this approach has a small flaw: As we are using a raycast, we need something to intersect with the ray. In this case we have the arrows, but what happens when you press the mouse button over the arrow, and when you drag it, the mouse leaves the arrow? Well, you lost it, and you cannot continue dragging the object. So I came out with this hack: Attach 2 huge and invisible planes to the arows, so that - when you loose those - you can intersect this planes instead.

Raycast function

Now, in order to select an object or to move it, I've created a raycast function, that also converts the 2D coordinates to 3D. Using this function you can only select objects by its BoundingBox, maybe later I implement an {LEX()}ODE{LEX} version of this raycast.

Ogre::MovableObject* MyListener::getNode(float mouseScreenX, float mouseScreenY)
 {
     Ray mouseRay = ogreSystem->getCamera()->getCameraToViewportRay(mouseScreenX,mouseScreenY);
     mRaySceneQuery->setRay(mouseRay);
     mRaySceneQuery->setSortByDistance(true);
     RaySceneQueryResult &result = mRaySceneQuery->execute();
 
     Ogre::MovableObject *closestObject = NULL;
     Real closestDistance = 100000;
 
     Ogre::RaySceneQueryResult::iterator rayIterator;
 
     for(rayIterator = result.begin(); rayIterator != result.end(); rayIterator++ ) 
     {
         if ((*rayIterator).movable !=NULL && closestDistance>(*rayIterator).distance && (*rayIterator).movable->getMovableType() != "TerrainMipMap")
         {
             closestObject = ( *rayIterator ).movable;
             closestDistance = ( *rayIterator ).distance;
             oldpos = mouseRay.getPoint((*rayIterator).distance);
             originalPos = oldpos;
         }
     }
 
     mRaySceneQuery->clearResults();
      return closestObject;
 }

Selecting an object



The next code will be used to select an object. I tried to create a function that adapts the size of the arrow, based on the size of the object selected.

void MySystem::selectObjectForEdit(char* idObject, char* type)
 {
     try
     {
         SceneNode* widget = mSceneMgr->getSceneNode(type);
         SceneNode* node = mSceneMgr->getSceneNode(idObject);
                  
         Ogre::Vector3 scale = node->getScale();
         if (node->getParent()->getScale()!=Ogre::Vector3::UNIT_SCALE)
         {
             scale *= node->getParent()->getScale();
             cout << "\033[34m WARNING: ParentNode scaled! \033[0m" << endl; 
         }
 
         //size of the editable object
         Ogre::AxisAlignedBox ax = node->getAttachedObject(idObject)->getBoundingBox();
         Ogre::Vector3 min = ax.getMinimum()*scale;
         Ogre::Vector3 max = ax.getMaximum()*scale;
         Ogre::Vector3 size(fabs(max.x-min.x),fabs(max.y-min.y),fabs(max.z-min.z));
         Ogre::Vector3 center = Vector3((max.x+min.x)/2.0f,(max.y+min.y)/2.0f,(max.z+min.z)/2.0f);
                         
         float big = (size.x>size.y)?size.x:size.y;
         big = (size.z>big)?size.z:big;
         if (big < 1)
             big = 1;
 
         //size of the widgets 60
         float size_w = big/60.0f;
 
         widget->setScale(2*size_w, 2*size_w, 2*size_w);
 
         mSceneMgr->getRootSceneNode()->addChild(widget);
         widget->setPosition(node->getWorldPosition()+center);
         widget->setVisible(true);
     }
     catch(...)
     {
         cout << "\033[31m ERROR! selectObjectForEdit \033[0m" << endl;
     }
 }

Well, when we press the mouse over an object, we will select this object and reposition the "move_widget" to the same position, using the mouse listener:

Adding dummy planes

void MyListener::mousePressed(Ogre::MouseEvent* e)
 {
     //IF the left mouse button is pressed
     if(e->getButtonID() == Ogre::MouseEvent::BUTTON0_MASK)
     {
         Ogre::MovableObject* nodeM = getNode(e->getX(),e->getY()); 
 
         if(nodeM != NULL)
         {
             name = nodeM->getParentSceneNode()->getName();
 
             if (name=="move_widget_x")
             {
                 nodeM->getParentSceneNode()->attachObject(mySystem->getSceneManager()->getEntity("dummy_plane_x"));
                 nodeM->getParentSceneNode()->attachObject(mySystem->getSceneManager()->getEntity("dummy_plane_z"));
                 selectedGizmo=1;                
             }
             else
             if (name=="move_widget_y")
             {
                 nodeM->getParentSceneNode()->attachObject(mySystem->getSceneManager()->getEntity("dummy_plane_x"));
                 nodeM->getParentSceneNode()->attachObject(mySystem->getSceneManager()->getEntity("dummy_plane_z"));
                 selectedGizmo=2;            
             }
             else
             if (name=="move_widget_z")
             {
                 nodeM->getParentSceneNode()->attachObject(mySystem->getSceneManager()->getEntity("dummy_plane_x"));
                 nodeM->getParentSceneNode()->attachObject(mySystem->getSceneManager()->getEntity("dummy_plane_z"));
                 selectedGizmo=3;                    
             }
             else
                 mySystem->selectObjectForEdit(name,"move_widget");
         }
     }
 }

Removing dummy planes


void MyListener::mouseReleased(Ogre::MouseEvent* e)
 {
     try
     {
         mySystem->getSceneManager()->getSceneNode("move_widget_x")->detachObject("dummy_plane_x");
     }catch(...){}
     try
     {
         mySystem->getSceneManager()->getSceneNode("move_widget_x")->detachObject("dummy_plane_z");
     }catch(...){}
     try
     {
         mySystem->getSceneManager()->getSceneNode("move_widget_y")->detachObject("dummy_plane_x");
     }catch(...){}
     try
     {
         mySystem->getSceneManager()->getSceneNode("move_widget_y")->detachObject("dummy_plane_z");
     }catch(...){}
     try
     {
         mySystem->getSceneManager()->getSceneNode("move_widget_z")->detachObject("dummy_plane_x");
     }catch(...){}
     try
     {
         mySystem->getSceneManager()->getSceneNode("move_widget_z")->detachObject("dummy_plane_z");
     }catch(...){}
 }

Moving the objects



And now, we only need to drag the arrows and the object as well. This is done at the mouseDragged() function:

objectselected.png

void MyListener::mouseDragged (Ogre::MouseEvent *e) 
 {
     Ray mouseRay = ogreSystem->getCamera()->getCameraToViewportRay(e->getX(),e->getY());
     mRaySceneQuery->setRay(mouseRay);
     mRaySceneQuery->setSortByDistance(true);
     RaySceneQueryResult &result = mRaySceneQuery->execute();
 
     Ogre::MovableObject *closestObject = NULL;
     Real closestDistance = 100000;
 
     std::list< Ogre::RaySceneQueryResultEntry >::iterator rayIterator;
 
     for(rayIterator = result.begin(); rayIterator != result.end(); rayIterator++ ) 
     {
         if ((*rayIterator).movable !=NULL && closestDistance>(*rayIterator).distance && (*rayIterator).movable->getMovableType() != "TerrainMipMap")
     {
     
     closestObject = ( *rayIterator ).movable;
     Ogre::String name = closestObject->getName();
 
     switch (selectedGizmo)
     {
         case 1:
         case 2:
         case 3:
             if (name!="move_widget_x" && name!="move_widget_y" && name!="move_widget_z" && name!="dummy_plane_x" && name!="dummy_plane_z")
                 return;
         break;
     }
 
     if (selectedGizmo==1)
     {
         SceneNode* widget = closestObject->getParentSceneNode()->getParentSceneNode();
         SceneNode* entity = mySystem->getSceneManager()->getSceneNode(selectedObject);
 
         Ogre::Vector3 newpos = mouseRay.getPoint((*rayIterator).distance);
 
         widget->translate(newpos.x-oldpos.x,0,0);
         entity->translate(newpos.x-oldpos.x,0,0);
         oldpos=newpos;
     }
     else if (selectedGizmo==2)
     {
         SceneNode* widget = closestObject->getParentSceneNode()->getParentSceneNode();
         SceneNode* entity = mySystem->getSceneManager()->getSceneNode(selectedObject);
 
         Ogre::Vector3 newpos = mouseRay.getPoint((*rayIterator).distance);
 
         widget->translate(0,newpos.y-oldpos.y,0);
         entity->translate(0,newpos.y-oldpos.y,0);
         oldpos=newpos;
     }
     else if (selectedGizmo==3)
     {
         SceneNode* widget = closestObject->getParentSceneNode()->getParentSceneNode();
         SceneNode* entity = mySystem->getSceneManager()->getSceneNode(selectedObject);
 
         Ogre::Vector3 newpos = mouseRay.getPoint((*rayIterator).distance);
         
         widget->translate(0,0,newpos.z-oldpos.z);
         entity->translate(0,0,newpos.z-oldpos.z);
         oldpos=newpos;
     }
 
     mRaySceneQuery->clearResults();
     e->consume();
 }

It is kind of difficult to explain this last snippet, just try to understand it by reading carefully. The most important line may be this one:

Ogre::Vector3 newpos = mouseRay.getPoint((*rayIterator).distance);

You can attach the widget node to your object's node. This way you just have to move the widget, and the object would be dragged too, but because of the rotation method, I needed to make it this way.

Final Notes

  • I already have code for scale and rotate widgets, but just because I'm working on something else, and I believe those widgets do not work too well, I decided to post them some other time (shortly).

  • As I mentioned earlier, this may not work as pasted above, but may need some corrections.


Alias: Pick_Drag_Drop