OGRE中级教程三 Mouse Picking(3D Object Selection) and SceneQuery Masks

英语水平有限,欢迎大家批评指正微笑

本文并没有将原文全部翻译,只是将其中的一些知识点翻译总结了一下,想要查看详细讲解的话,可以到原文处看一下,附上英文原文地址:http://www.ogre3d.org/tikiwiki/tiki-index.php?page=Intermediate+Tutorial+3&structure=Tutorials

Showing Which Object is Selected

   本教程中我们将学习选中和移动物体,我们得有一种方法让用户指定当前被操作的对象是哪个。在游戏中,我们可能会高亮显示对象,但对我们的教程(或你发布之前的程序)来说,你可以使用showBoundingBox方法在对象周围创建一个盒子。我们的想法是当我们选中了新的对象时,使前一个被选中对象的绑定盒不可用,让当前对象的绑定盒可用。为此添加如下代码到mousePressed函数开始:

   //show that the current object has been deselected by removing the bounding box visual

if(mCurrentObject)

{

       mCurrentObject->showBoundingBox(false);

}

   然后添加如下代码到mousePressed函数最末尾:

   //now we show the bounding box so the user can see that this object is selected

if(mCurrentObject)

{

        mCurrentObject->showBoundingBox(true);

}

   现在屏幕上mCurrentObject总是高亮的。

Adding Ninjas

   我们现在想修改代码,让他不只是支持机器人,还可以摆放和移动ninjas(忍者)。我们会有"Robot Mode""Ninja Mode"来决定我们要在屏幕上放那个对象,空格键用来进行Mode间的转换。

   首先我们设置IntermediateTutorialRobot Mode为开始。我们需要添加一个变量来保存对象的状态(即我们是放的机器人还是忍者)。在IntermediateTutorialprotected部分添加变量:

   bool bRobotMode;        // The current state

   添加代码到IntermediateTutorial3的构造函数:

   // Set the default state

   bRobotMode = true;

   它启动了Robot Mode,现在我们需要根据bRobotMode变量来创建一个机器人或忍者。添加如下代码到mousePressed函数:

   char name[16];

   sprintf(name, "Robot%d", mCount++);

   Entity *ent = mSceneMgr->createEntity(name, "robot.mesh");

   The replacement should be straight forward 。根据bRobotMode的状态,我们创建一个机器人或忍者并命名。

Entity *ent;

char name[16];

if (bRobotMode)

{

        sprintf(name, "Robot%d", mCount++);

        ent = mSceneMgr->createEntity(name, "robot.mesh");

} // if

else

{

        sprintf(name, "Ninja%d", mCount++);

        ent = mSceneMgr->createEntity(name, "ninja.mesh");

} // else

   剩下要做的就是绑定空格键来改变状态。在头文件IntermediateTutorial3.h中添加keyPressed函数的重载:

   virtual bool keyPressed(const OIS::KeyEvent& arg);

   现在在IntermediateTutorial3.cpp中完成该函数,我们需要为空格键添加功能,但我们还想保留BaseApplication的功能。那怎么做呢?如下:

bool IntermediateTutorial3::keyPressed(const OIS::KeyEvent& arg)

{

       //check and see if the spacebar was hit, and this will switch which mesh is spawned

       if(arg.key == OIS::KC_SPACE)

       {

              bRobotMode = !bRobotMode;

       }

       //then we return the base app keyPressed function so that we get all of the functionality

       //and the return value in one line

       return BaseApplication::keyPressed(arg);

}

编译运行程序,你现在可以通过使用空格键来选择放哪个对象了。

Selecting Objects

   我们现在要使用RaySceneQueries来选择屏幕上的对象。开始改代码之前我首先要详细解释一下RaySceneQueryResultEntryRaySceneQueryResult返回一个RaySceneQueryResultEntry结构体的迭代器。该结构体有三个变量,distance变量告诉你对象距射线有多远,另外两个变量之一为non-null,如果射线与一个可移动对象相交movable变量将包含一个MovableObject(可移动对象)。worldFragment如果碰到一个world fragment(比如地面)就会包含一个WorldFragment对象。

   MovableObjects(可移动对象)可以使你绑定到场景节点上的任何对象(如实体、光源等)。大部分RaySceneQueries程序都包括选择和操作你点击的MovableObject(可移动对象)或他们所绑定的场景节点。getName方法可以获取MovableObject(可移动对象)的名字。getParentSceneNode(或getParentNode)方法可以获取对象所绑定的场景节点。如果结果不是一个MovableObject(可移动对象),RaySceneQueryResultEntry中的movable变量就等于NULL

   当一个RaySceneQueryResultWorldFragment成员被设置,这意味着结果是被场景管理器创建的世界几何体(world geometry)的一部分。返回的world fragment的类型是根据场景管理器的得到的。所使用的方法是WorldFragment结构体包含的fragmentType变量,该变量指出了它所包含的world fragment类型。根据fragmentType变量,另外一个变量将被设置(singleIntersection, planes, geometryrenderOp)。总的来说,RaySceneQueries只返回WFT_SINGLE_INTERSECTION  WorldFragmentssingleIntersection变量只是一个包含相交位置的Vector3(三维向量)。其他类型的world fragment超出了本教程的范围。

   现在来看一个例子,假如一次射线场景查询(RaySceneQuery)后我们想打印出一个结果列表。下面的代码会实现这个功能:

   // Do not add this code to the program, just read along:

RaySceneQueryResult &result = mRayScnQuery->execute();

RaySceneQueryResult::iterator itr;

// loop through the results

for ( itr = result.begin( ); itr != result.end(); itr++ )

{

        // Is this result a WorldFragment?

        if ( itr->worldFragment )

        {

        Vector3 location = itr->worldFragment->singleIntersection;

        fout << "WorldFragment: (" << location.x << ", " << location.y << ", " << location.z << ")" << endl;

        } //  if

   

        // Is this result a MovableObject?

       else if ( itr->movable )

        {

        fout << "MovableObject: " << itr->movable->getName() << endl;

        } // else if

} // for

   它将打印出所有与射线相交的可移动对象(MovableObjects)的名字,并打印出射线与世界几何体(world geometry)相交的位置(如果相交)。注意它有时可能会运行的比较奇怪,例如,如果你使用TerrainSceneManager,你发射的射线的原点必须高过(must be over)地面否则相交查询不会把他作为碰撞。不同的场景管理器使用不同方法完成射线场景查询(RaySceneQueries)。当你使用一个新的场景管理器时注意测试一下。

   现在如果我们回过头来看我们的射线场景查询(RaySceneQuery)代码,我们没有循环所有的结果。实际上我们只看第一个结果,就是world geometry(世界几何体)。这是不好的,因为我们不能确定TerrainSceneManager总是第一个返回world geometry(世界几何体)。我们需要循环所有结果,确保找到我们要找的结果。我们要做的另一件事是选择并拖拽已经被放置的对象。现在如果你点击一个已经放置的对象,程序会忽略并在它之后放一个机器人。现在我们来进行修改。

   我们首先想要确保当我们点击时,我们获取的是离射线最近的东西。为此我们需要设置RaySceneQuery按深度优先搜索(sort by depth)。在mousePressed函数中找到如下代码:

// Setup the ray scene query, use CEGUI's mouse position

CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();

Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));

mRayScnQuery->setRay(mouseRay);

 

// Execute query

RaySceneQueryResult &result = mRayScnQuery->execute();

RaySceneQueryResult::iterator itr = result.begin();

然后改成如下代码:

// Setup the ray scene query

CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();

Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));

mRayScnQuery->setRay(mouseRay);

mRayScnQuery->setSortByDistance(true);

// Execute query

RaySceneQueryResult &result = mRayScnQuery->execute();

RaySceneQueryResult::iterator iter = result.begin();

现在我们要按顺序返回结果,需要更新查询结果代码。我们要重写这一部分,所有移除如下代码:

// Get results, create a node/entity on the position

if (iter != result.end() && iter->worldFragment)

{

        Entity *ent;

        char name[16];

 

        if (bRobotMode)

       {

                sprintf(name, "Robot%d", mCount++);

                ent = mSceneMgr->createEntity(name, "robot.mesh");

        } // if

        else

        {

                sprintf(name, "Ninja%d", mCount++);

                ent = mSceneMgr->createEntity(name, "ninja.mesh");

        } // else

 

        mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(String(name) + "Node", iter->worldFragment->singleIntersection);

        mCurrentObject->attachObject(ent);

        mCurrentObject->setScale(0.1f, 0.1f, 0.1f);

} // if

我们想要选择已经放在屏幕上的对象,我们的策略是分两部分。第一,如果用户点击一个对象,然后让mCurrentObject等于它的父场景节点。如果用户没有点击对象(如果他点击地面),在该位置放一个机器人。第一个改变就是我们要用一个for循环而不是if语句:

// Get results, create a node/entity on the position

for ( iter; iter != result.end(); iter++ )

{

首先检查第一个相交的对象是否为可移动对象(MovableObject),如果是我们就把它的父场景节点给mCurrentObjectTerrainSceneManager为地面自己创建可移动对象(MovableObjects),所以我们可能实际上与其中之一相交。未来修正,检查对象的名字以确保它与一个地图块的名字不相似。最后最有break语句,我们只处理以一个对象,所以当我们找到一个有效结果时就要跳出for循环。

if (iter->movable && iter->movable->getName().substr(0, 5) != "tile[")

{

        mCurrentObject = iter->movable->getParentSceneNode();

        break;

} // if

然后查看相交是否返回的是WorldFragment

else if (iter->worldFragment)

{

        Entity *ent;

        char name[16];

现在我们根据mRobotState要么创建一个机器人实体要么创建一个忍者实体,之后我们将创建场景节点并跳出for循环:

if (bRobotMode)

        {

                sprintf(name, "Robot%d", mCount++);

                ent = mSceneMgr->createEntity(name, "robot.mesh");

        } // if

        else

        {

                sprintf(name, "Ninja%d", mCount++);

                ent = mSceneMgr->createEntity(name, "ninja.mesh");

        } // else

                mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(String(name) + "Node", iter->worldFragment->singleIntersection);

                mCurrentObject->attachObject(ent);

                mCurrentObject->setScale(0.1f, 0.1f, 0.1f);

                break;

        } // else if

} // for

现在我们已经完成编码,编译运行代码。现在当我们点击地面时,可以创建正确类型的对象了。当我们点击对象时,能看到包围盒。有一个问题,由于我们只要第一个相交的结果而且我们使用的是按深度优先搜索,那为什么不用if语句呢?这主要是因为we could actually have a fall through if there the first returned object is one of those pesky tiles. We have to loop until we find something other than a tile or we hit the end of the list.

我们还需要在其他地方修改RaySceneQuery代码,在frameRenderingQueued mouseMoved函数中我们只需要找到地面(Terrain)。没有必要对结果排序,因为地面会在列表的最后。我们仍然想循环所有结果,以防万一TerrainSceneManager某一天不是第一个返回地面(terrain)。首先在frameRenderingQueued函数中找到以下代码:

// Perform the scene query

Ogre::RaySceneQueryResult &result = mRayScnQuery->execute();

Ogre::RaySceneQueryResult::iterator itr = result.begin();

// Get the results, set the camera height

if (iter != result.end() && iter->worldFragment)

{

        Ogre::Real terrainHeight = iter->worldFragment->singleIntersection.y;

        if ((terrainHeight + 10.0f) > camPos.y)

        {

                mCamera->setPosition(camPos.x, terrainHeight + 10.0f, camPos.z);

        }

}

然后替换为如下代码:

// Perform the scene query

mRayScnQuery->setSortByDistance(false);

Ogre::RaySceneQueryResult &result = mRayScnQuery->execute();

Ogre::RaySceneQueryResult::iterator iter = result.begin();

// Get the results, set the camera height

for (iter; iter != result.end(); iter++)

{

        if (iter->worldFragment)

        {

                Ogre::Real terrainHeight = iter->worldFragment->singleIntersection.y;

                if ((terrainHeight + 10.0f) > camPos.y)

                {

                        mCamera->setPosition(camPos.x, terrainHeight + 10.0f, camPos.z);

                }

                break;

        } // if

} // for

我们添加一行代码来关闭排序,然后把if语句变成一个for循环,找到我们寻找的位置后就跳出循环。找到mouseMoved函数中的如下部分代码:

mRayScnQuery->setRay(mouseRay);

            Ogre::RaySceneQueryResult &result = mRayScnQuery->execute();

            Ogre::RaySceneQueryResult::iterator iter = result.begin();

            if (iter != result.end() && iter->worldFragment)

            {

                mCurrentObject->setPosition(iter->worldFragment->singleIntersection);

            }

   替换为如下代码:

   mRayScnQuery->setRay(mouseRay);

mRayScnQuery->setSortByDistance(false);

Ogre::RaySceneQueryResult &result = mRayScnQuery->execute();

Ogre::RaySceneQueryResult::iterator iter = result.begin();

for (iter; iter != result.end(); iter++)

{

        if (iter->worldFragment)

        {

                mCurrentObject->setPosition(iter->worldFragment->singleIntersection);

                break;

        } // if

}

Query Masks

注意无论我们在哪个mode我们都可以选择对象,RaySceneQuery将返回一个机器人或忍者。所有的可移动对象(MovableObjects)允许你给他设一个标记值,RaySceneQueries允许你根据这个标记过滤你的结果。

首先我们要创建标记值,在IntermediateTutorial3类的最开始在public语句后添加如下代码:

enum QueryFlags

{

        NINJA_MASK = 1<<0,

        ROBOT_MASK = 1<<1

};

它创建了一个有两个值的枚举变量,两个值按二进制就是00010010。每当我们创建一个机器人实体,我们就调用他的setMask函数来设置他的查询标记为ROBOT_MASK,相对的创建忍者实体时,查询标记为NINJA_MASK。当我们在Robot mode时,我们只考虑ROBOT_MASK

找到mousePressed的这部分代码:

if (bRobotMode)

{

        sprintf(name, "Robot%d", mCount++);

        ent = mSceneMgr->createEntity(name, "robot.mesh");

} // if

else

{

        sprintf(name, "Ninja%d", mCount++);

        ent = mSceneMgr->createEntity(name, "ninja.mesh");

} // else

我们将添加两行代码来设置他们两个的标记:

if (bRobotMode)

{

        sprintf(name, "Robot%d", mCount++);

        ent = mSceneMgr->createEntity(name, "robot.mesh");

        ent->setQueryFlags(ROBOT_MASK);

} // if

else

{

        sprintf(name, "Ninja%d", mCount++);

        ent = mSceneMgr->createEntity(name, "ninja.mesh");

       ent->setQueryFlags(NINJA_MASK);

} // else

我们还要使得当我们在某一mode时,我们只能点击和拖动该类型的对象。我们需要设置查询标识,以使得只有正确类型的对象才能被选中。我们通过在Robot mode时设查询标记为ROBOT_MASK,Ninja mode时设查询标记为NINJA_MASK来实现。找到onLeftPressed函数中的如下代码:

mRayScnQuery->setSortByDistance(true);

在改代码下一行添加如下代码:

mRayScnQuery->setQueryMask(bRobotMode ? ROBOT_MASK : NINJA_MASK);

Query Type Masks

使用场景查询时,还有一件事情要考虑。假如你想你的场景中添加了一个广告牌集(billboardset)或一个粒子系统(a particle system),并且你想要来回移动它。你会发现查询永远不会返回你点击的广告牌集(billboardset),这是因为SceneQuery有另一个标记——QueryTypeMask,它限制了你选择该类型标记的对象。默认的当你查询时,他只返回实体类型的对象。

如果你想查询广告牌集(billboardset)或粒子系统(ParticleSystem),那么在执行查询之前你需要先做这些:

mRayScnQuery->setQueryTypeMask(SceneManager::FX_TYPE_MASK);

限制查询只返回广告牌集(billboardset)或粒子系统(ParticleSystem)作为结果。

SceneManager中定义为静态成员的QueryTypeMask由六种类型:

WORLD_GEOMETRY_TYPE_MASK //Returns world geometry.

ENTITY_TYPE_MASK         //Returns entities.

FX_TYPE_MASK             //Returns billboardsets / particle systems.

STATICGEOMETRY_TYPE_MASK //Returns static geometry.

LIGHT_TYPE_MASK          //Returns lights.

USER_TYPE_MASK_LIMIT     //User type mask limit.

QueryTypeMask的默认值为ENTITY_TYPE_MASK

More on Masks

Setting a MovableObject's Mask

每次我们创建一个新的标记时,二进制表示中必须只包含一个1。以下有效标记值:

00000001

00000010

00000100

00001000

00010000

00100000

01000000

10000000

我们可以通过右移1来很容易的创建这些值:

00000001 = 1<<0

00000010 = 1<<1

00000100 = 1<<2

00001000 = 1<<3

00010000 = 1<<4

00100000 = 1<<5

01000000 = 1<<6

10000000 = 1<<7

一直到1<<31,我们有32中不同的标记让MovableObjects来使用。

Querying for Multiple Mask

我们使用OR操作符来查询多个标记,假如在游戏中我们有三个不同的对象组:

enum QueryFlags

{

     FRIENDLY_CHARACTERS = 1<<0,

     ENEMY_CHARACTERS = 1<<1,

     STATIONARY_OBJECTS = 1<<2

};

现在如果我们想要查询friendly对象,我们可以这样:

mRayScnQuery->setQueryMask(FRIENDLY_CHARACTERS);

如果我们想要查询enemystationary对象,我们这样做:

mRayScnQuery->setQueryMask(ENEMY_CHARACTERS | STATIONARY_OBJECTS);

如果你用很多的这种查询类型,你最好把他们定义为枚举:

OBJECTS_ENEMIES = ENEMY_CHARACTERS | STATIONARY_OBJECTS

然后使用OBJECTS_ENEMIES来查询。

Querying for Everything but a Mask

还可以查询没有标记的任何事物通过使用非运算符,如:

mRayScnQuery->setQueryMask(~FRIENDLY_CHARACTERS);

它将返回friendly标记之外的所有东西,这种方法对查询多种标记的对象同样有效:

mRayScnQuery->setQueryMask(~(FRIENDLY_CHARACTERS | STATIONARY_OBJECTS));

它将返回friendlystationary标记之外的所有东西。

Selecting all Objects or No Objects

你可以用标记来做一些有趣的事情:如果你为SceneQuery设置一个查询标记QM,它将匹配所有有QM标记的可移动对象(MovableObjects),如果QM&QM包含最少一个1at least one 1)。为SceneQuery设置查询标记为0,将使得没有可移动对象(MovableObjects)返回。设置查询标记为~00xFFFFF...)将返回所有标记不为0的可移动对象(MovableObjects)。

设置查询标记为0某些情况会很有用,如当TerrainSceneManager不返回worldFragment是,它不使用QueryMasks。通过这样做:

mRayScnQuery->setQueryMask(0);

在场景管理器(SceneManager)的RaySceneQueries中,你将只得到worldFragment。如果屏幕上有很多对象,并且你只想找到与地面相交的结果(Terrain intersection)而不想浪费时间来循环所有这些对象,这将很有用。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值