From OGRE 3D 中文
介绍
在这一课里,我们将涉及如何进行体积选取。意思就是,当你在屏幕上点击并且拖拽鼠标时,一个白色矩形会追踪你正在选择的区域。当鼠标移动时,所有在 选择区域里的物体都会被高亮。为了实现它,我们将学习两种对象:ManualObject(创建矩形)和 PlaneBoundedVolumeListSceneQuery。注意,当我们涉及ManualObject的基本用法时,只是对它的简单介绍,而不 是教你如何完全用它创建3D物体。我们只会涉及我们所需要的。
你能在这里找到本课的代码。当你学习本课时,你应该逐个地往你的工程里添加代码,编 译后观察相应的结果。
先决条件
用你喜欢的IDE创建一个cpp,并添加以下代码:
#include <CEGUI/CEGUI.h>
#include <OgreCEGUIRenderer.h>
#include "ExampleApplication.h"
class SelectionRectangle : public ManualObject
{
public:
SelectionRectangle(const String &name)
: ManualObject(name)
{
}
/**
* Sets the corners of the SelectionRectangle. Every parameter should be in the
* range [0, 1] representing a percentage of the screen the SelectionRectangle
* should take up.
*/
void setCorners(float left, float top, float right, float bottom)
{
}
void setCorners(const Vector2 &topLeft, const Vector2 &bottomRight)
{
setCorners(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
}
};
class DemoListener : public ExampleFrameListener, public OIS::MouseListener
{
public:
DemoListener(RenderWindow* win, Camera* cam, SceneManager *sceneManager)
: ExampleFrameListener(win, cam, false, true), mSceneMgr(sceneManager), mSelecting(false)
{
mMouse->setEventCallback(this);
} // DemoListener
~DemoListener()
{
}
/* MouseListener callbacks. */
bool mouseMoved(const OIS::MouseEvent &arg)
{
CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);
return true;
}
bool mousePressed(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
{
return true;
}
bool mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
{
return true;
}
void performSelection(const Vector2 &first, const Vector2 &second)
{
}
void deselectObjects()
{
std::list<MovableObject*>::iterator itr;
for (itr = mSelected.begin(); itr != mSelected.end(); ++itr)
(*itr)->getParentSceneNode()->showBoundingBox(false);
}
void selectObject(MovableObject *obj)
{
obj->getParentSceneNode()->showBoundingBox(true);
mSelected.push_back(obj);
}
private:
Vector2 mStart, mStop;
SceneManager *mSceneMgr;
PlaneBoundedVolumeListSceneQuery *mVolQuery;
std::list<MovableObject*> mSelected;
SelectionRectangle *mRect;
bool mSelecting;
static void swap(float &x, float &y)
{
float tmp = x;
x = y;
y = tmp;
}
};
class DemoApplication : public ExampleApplication
{
public:
DemoApplication()
: mRenderer(0), mSystem(0)
{
}
~DemoApplication()
{
if (mSystem)
delete mSystem;
if (mRenderer)
delete mRenderer;
}
protected:
CEGUI::OgreCEGUIRenderer *mRenderer;
CEGUI::System *mSystem;
void createScene(void)
{
mRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, mSceneMgr);
mSystem = new CEGUI::System(mRenderer);
CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme");
CEGUI::MouseCursor::getSingleton().setImage((CEGUI::utf8*)"TaharezLook", (CEGUI::utf8*)"MouseArrow");
mCamera->setPosition(-60, 100, -60);
mCamera->lookAt(60, 0, 60);
mSceneMgr->setAmbientLight(ColourValue::White);
for (int i = 0; i < 10; ++i)
for (int j = 0; j < 10; ++j)
{
Entity *ent = mSceneMgr->createEntity("Robot" + StringConverter::toString(i + j * 10), "robot.mesh");
SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode(Vector3(i * 15, 0, j * 15));
node->attachObject(ent);
node->setScale(0.1, 0.1, 0.1);
}
}
void createFrameListener(void)
{
mFrameListener = new DemoListener(mWindow, mCamera, mSceneMgr);
mFrameListener->showDebugOverlay(true);
mRoot->addFrameListener(mFrameListener);
}
};
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT)
#else
int main(int argc, char **argv)
#endif
{
// Create application object
DemoApplication app;
try {
app.go();
} catch(Exception& e) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
MessageBoxA(NULL, e.getFullDescription().c_str(), "An exception has occurred!",
MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
fprintf(stderr, "An exception has occurred: %s/n",
e.getFullDescription().c_str());
#endif
}
return 0;
}
继续之前,请确保这段代码能够编译。当你运行它时,你应该能够移动鼠标指针,但程序目前不能做其它的。按ESC退出。
ManualObject对象
3D对象的快速入门
在我们开始进入制作网格(mesh)之前,有必要讲一下mesh是什么,以及它的构成。尽管非常简化,mesh大致由两部分组成:顶点缓存 (Vertex buffer)和索引缓存(Index buffer)。
顶点缓存在3D空间里定义点集。顶点缓存里的每一个元素由若干你能设置的属性来定义。唯一一个你必须设置的属性就是顶点的坐标。除了这个,你还有设 置其它可选的属性,比如顶点颜色、纹理坐标等。你实际需要的属性取决于mesh的用途。
索引缓存通过从顶点缓存选取顶点,以“把点连起来”。索引缓存里每三个顶点定义了一个由GPU绘制的三角形。你在索引缓存里选取顶点的顺序,告诉了 显卡这个三角形的朝向。逆时针绘制的三角形是朝向你的,顺时针绘制的三角形是背向你的。一般情况下只有三角形的正面才被绘制,所以确保你的三角形被正确载 入是很重要的。
虽然所有的mesh都有顶点缓存,但不一定都有索引缓存。比如,我们只要创建一个空的三角形(而不是实心的),我们创建的mesh就不需要索引缓 存。最后要注意,顶点缓存和索引缓存通常保存在显卡自己的内存里,所以你的软件只要发送一些离散的命令,告诉它使用预定义的缓存来一口气渲染整个3D网 格。
介绍
在Ogre里有两种方法来创建你自己的网格。第一种是继承SimpleRenderable,并直接提供给它顶点和索引缓存。这是最直接的创建方 式,但也是最不直观的。为了使事情更简单,Ogre提供一个更棒的接口叫做ManualObject,它能让你用一些简单的函数来定义一个网格,而不用往 缓存里写原始数据。你仅仅调用"position"和"colour"函数,而不用往缓存里丢位置、颜色等数据。
在本课里,当我们拖动鼠标去选择物体时,我们要创建并显示一个白色矩形。在Ogre里并没有真正的用来显示2D矩形的类。我们必须自己找一个解决办 法。我们可以使用一个Overlay并缩放它,以显示一个矩形选择框,但这样做带来的问题是,选择框的图像可能会随着拉升而难看变形。取而代之,我们将生 成一个非常简单的2D网格,来作为我们的选择矩形。
代码
当我们创建选择矩形的时候,我们想让它以2D的形式呈现。我们还想保证当在屏幕里发生重叠时,它显示在所有其它物体之上。实现这个非常简单。找到 SelectionRectangle的构造器,并添加如下代码:
setRenderQueueGroup(RENDER_QUEUE_OVERLAY);
setUseIdentityProjection(true);
setUseIdentityView(true);
第一个函数把这个物体的渲染队列设置成重叠队列(Overlay queue)。接下来的两个函数把投影矩阵(projection matrix)和视图矩阵(view matrix)设置成identity。投影矩阵和视图矩阵被很多渲染系统所使用(比如OpenGL和DirectX),以定义物体在世界中的坐标。既然 Ogre为我们做了抽象,我们不必深究这些矩阵是什么样的或他们干了些什么。然而,你需要知道如果你把投影矩阵和视图矩阵设置成identity,就像刚 才那样,我们基本上就是在绘制2D物体。这样定义之后,坐标系统发生了一些改变。我们不再需要Z轴(若你被要求提供Z轴,设置成-1)。取而代之,我们有 一个新的坐标系统,X和Y的范围分别都是-1到1。最后,我们将把这个物体的查询标记设置成0,如下:
setQueryFlags(0);
现在,对象设置好了,我们来实际构建这个矩形。我们开始之前还有一个小小阻碍,我们将使用鼠标坐标来调用这个函数。也就是,传给我们一个0到1之间 的数字为每个坐标轴,然而我们需要把这个数字转换成范围[-1,1]的。还有更复杂的,y坐标要反向。在CEGUI里,鼠标指针在屏幕顶部时,值为+1, 在底部时,值为-1。感谢上帝,用一个快速转换就能解决这个问题。找到setCorners函数并添加如下代码:
left = left * 2 - 1;
right = right * 2 - 1;
top = 1 - top * 2;
bottom = 1 - bottom * 2;
现在转换成新坐标系统了。下面,我们来真正创建这个对象。为此,我们首先调用begin方法。它需要两个参数,物体的这一部分所使用的材质,以及它 所使用的渲染操作。因为我们不使用纹理,把这个材质置空。第二个参数是渲染操作(RenderOperation)。我们可以使用点、线、三角形来渲染这 个网格。如果我们要渲染一个实心的网格,可以用三角形。但我们只需要一个空的矩形,所以我们使用线条(line strip)。从你定义的前一个顶点到现在的顶点,线条绘制一条直线。所以为了创建我们的矩形,需要定义5个点(第一个和最后一个是相同的,这样才能连接 成整个矩形):
clear();
begin("", RenderOperation::OT_LINE_STRIP);
position(left, top, -1);
position(right, top, -1);
position(right, bottom, -1);
position(left, bottom, -1);
position(left, top, -1);
end();
注意,因为我们将在后面多次调用它,我们在最前面加入clear函数,在重新绘制矩形之前移除上次的矩形。当定义一个手动物体时,你可能要多次调用 begin/end来创建多个子网格(它们可能有不同的材质/渲染操作)。注意,我们把Z参数设成-1,因为我们只定义一个2D对象而不必使用Z轴。把它 设置为-1,可以保证当渲染时我们不处在摄像机之上或之后。
最后我们还要为这个物体设置包围盒。许多场景管理器会把远离屏幕的物体剔除掉。尽管我们创建的差不多是一个2D物体,但Ogre仍是一个3D引擎, 它把2D物体当作在3D空间里对待。这意味着,如果我们创建这个物体,并把它绑在场景节点上(正如我们下面要做的那样),当我们远一点观看时会消失。为了 修正这个问题,我们将把这个物体的包围盒设置成无限大,这样摄像机就永远在它里面:
AxisAlignedBox box;
box.setInfinite();
setBoundingBox(box);
请注意,我们在调用clear()之后添加这段代码的。当每你调用ManualObject::clear,包围盒都会被重置,所以当你创建经常清 空的ManualObject时要格外小心,每当你重新创建它的时候,也要重新设置包围盒。
好了,我们要为SelectionRectangle类所做的全部就是这些。继续下去之前请保证能编译你的代码,但目前还没有为程序添加功能。
体积选取
设置
在我们进入选取操作的代码之前,先来设置一些东西。首先,我们要创建一个SelectionRectangle类的实例,然后让 SceneManager来为我们创建一个体积查询:
mRect = new SelectionRectangle("Selection SelectionRectangle");
mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(mRect);
mVolQuery = mSceneMgr->createPlaneBoundedVolumeQuery(PlaneBoundedVolumeList());
再来,我们要保证结束时帧监听器做一些清理。把下面的代码加到~DemoListener:
mSceneMgr->destroyQuery(mVolQuery);
delete mRect;
注意,我们让SceneManager为我们进行清理,而不是直接删除。
鼠标处理
我们要展示的特性是体积选取。这意味着当用户点击鼠标并拖拽时,屏幕里将绘制一个矩形。随着鼠标的移动,所有在矩形内的物体将被选取。首先,我们要 处理鼠标的点击事件。我们要保存鼠标的起始位置,并且把SelectionRectangle设置成可见的。找到mousePressed函数并添加如下 代码:
if (id == OIS::MB_Left)
{
CEGUI::MouseCursor *mouse = CEGUI::MouseCursor::getSingletonPtr();
mStart.x = mouse->getPosition().d_x / (float)arg.state.width;
mStart.y = mouse->getPosition().d_y / (float)arg.state.height;
mStop = mStart;
mSelecting = true;
mRect->clear();
mRect->setVisible(true);
}
注意,我们使用的是CEGUI::MouseCursor的x和y坐标,而不是OIS的鼠标坐标。这是因为有时OIS反映的坐标与CEGUI实际显 示的不一样。为了保证我们与用户所看到的相一致,我们使用CEGUI的鼠标坐标。
接下来我们要做的是,当用户释放鼠标按钮时,停止显示选择框,并执行这个选取查询。在mouseReleased里加入以下代码:
if (id == OIS::MB_Left)
{
performSelection(mStart, mStop);
mSelecting = false;
mRect->setVisible(false);
}
最后,每当鼠标移动时,我们需要更新矩形的坐标:
if (mSelecting)
{
CEGUI::MouseCursor *mouse = CEGUI::MouseCursor::getSingletonPtr();
mStop.x = mouse->getPosition().d_x / (float)arg.state.width;
mStop.y = mouse->getPosition().d_y / (float)arg.state.height;
mRect->setCorners(mStart, mStop);
}
每当鼠标移动时,我们都调整mStop向量,这样我们就能轻松地使用setCorners成员函数了。编译并运行你的程序,现在你能用鼠标绘制一个 矩形了。
PlaneBoundedVolumeListSceneQuery
现在,我们可以让SelectionRectangle正确地渲染了,我们还想执行一个体积选择。找到performSelection函数,并添 加如下代码:
float left = first.x, right = second.x,
top = first.y, bottom = second.y;
if (left > right)
swap(left, right);
if (top > bottom)
swap(top, bottom);
在这段代码里,我们分别为left、right、top、botton变量赋予向量参数。if语句保证了我们实际的left和top值最小。(如果 这个矩形是“反向”画出来的,意味着从右下角到左上角,我们就要进行这种交换。)
接下来,我们要检查并了解矩形区域的实际小大。如果这个矩形太小了,我们的创建平面包围体积的方法就会失败,并且导致选取太多或太少的物体。如果这 个矩形小于屏幕的某个百分比,我们只将它返回而不执行这个选取。我随意地选择0.0001作为取消查询的临界点,但在你的程序里你应该自己决定它的值。还 有,在真实的应用里,你应该找到这个矩形的中心,并执行一个标准查询,而不是什么都不做:
if ((right - left) * (bottom - top) < 0.0001)
return;
现在,我们进入了这个函数的核心,我们要执行这个查询本身。PlaneBoundedVolumeQueries使用平面来包围一个区域,所以所有 在区域里的物体都被选取。我们将创建一个被五个平面包围的区域,它是朝向里面的。为了创建这些平面,我们建立了4条射线,每一条都是矩形的一个角产生的。 一旦我们有四条射线,
For this example we will build an area enclosed by five planes which face inward. To create these planes out of our rectangle, we will create 4 rays, one for each corner of the rectangle. Once we have these four rays, we will grab points along the rays to create the planes:
Ray topLeft = mCamera->getCameraToViewportRay(left, top);
Ray topRight = mCamera->getCameraToViewportRay(right, top);
Ray bottomLeft = mCamera->getCameraToViewportRay(left, bottom);
Ray bottomRight = mCamera->getCameraToViewportRay(right, bottom);
现在我们来创建平面。注意,我们沿着射线走100个单位抓取一个点。这是随便选择的,我们也可以选择2而不是100。在这里唯一重要的是前平面,它 在摄像机前面3个单位的位置。
PlaneBoundedVolume vol;
vol.planes.push_back(Plane(topLeft.getPoint(3), topRight.getPoint(3), bottomRight.getPoint(3))); // 前平面
vol.planes.push_back(Plane(topLeft.getOrigin(), topLeft.getPoint(100), topRight.getPoint(100))); // 顶平面
vol.planes.push_back(Plane(topLeft.getOrigin(), bottomLeft.getPoint(100), topLeft.getPoint(100))); // 左平面
vol.planes.push_back(Plane(topLeft.getOrigin(), bottomRight.getPoint(100), bottomLeft.getPoint(100))); // 底平面
vol.planes.push_back(Plane(topLeft.getOrigin(), topRight.getPoint(100), bottomRight.getPoint(100))); // 右平面
这些平面定义了一个在摄像机前无限伸展的“开放盒子”。你可以把我们用鼠标绘制的矩形,想像成在镜头跟前,这个盒子的终点。好了,我们已经创建了平 台,我们还需要执行这个查询:
PlaneBoundedVolumeList volList;
volList.push_back(vol);
mVolQuery->setVolumes(volList);
SceneQueryResult result = mVolQuery->execute();
最后我们需要处理查询返回的结果。首先我们要取消所有先前选取的物体,然后选取所有查询得到的物体。deselectObjects和 selectObject函数已经为你写好了,因为在前面的教程里我们就已经介绍了这些函数:
deselectObjects();
SceneQueryResultMovableList::iterator itr;
for (itr = result.movables.begin(); itr != result.movables.end(); ++itr)
selectObject(*itr);
这就是全部我们要为查询所做的。注意,我们在体积查询里也使用查询标记,虽然本课我们还没有这么做。想了解更多关于查询标记,请参考上一课。
翻译并运行程序。你现在可以在场景里选取物体了!
最后关于包围盒的注意事项
也许你可能注意到了,在这一课里以及前面两课中,Ogre的选取依赖于物体的包围盒而不是网格本身。这意味着RaySceneQuery和 PlaneBoundedVolumeQuery总是承认这个查询实际上接触的东西。存在一些方法,可以进行基于像素的完美射线选取(比如在FPS游戏 里,需要判断一个射击是否命中目标,你就需要这么做)。而出于速度考虑,使用体积选取也能为你提供非常精确的结果。不幸的是,这超出了本课的范围。更多关 于如何在纯Ogre里实现,请参考多面体级别的射线构建。
如果你为Ogre整合了物理引擎,比如OgreNewt,它们也会为你提供一些方法。但你仍然不会白学了射线查询和体积查询。做一个基于网格的选取 是非常费时的,而且如果你尝试检测所有在场景里的东西,会大大影响你的帧率。事实上,进行一个鼠标选取最常用的方法是,首先执行一个Ogre查询(比如射 线场景查询),然后再用物理引擎逐个检测查询返回的结果,检查网格的几何形状来看看是否真的击中了,还是仅仅非常的接近。
--Xiao7cn 19:21 2008年3月1日 (CST)Xiao7cn(FriedChicken)翻译