英语水平有限,欢迎大家批评指正
本文并没有将原文全部翻译,只是将其中的一些知识点翻译总结了一下,想要查看详细讲解的话,可以到原文处看一下,附上英文原文地址:http://www.ogre3d.org/tikiwiki/tiki-index.php?page=Intermediate+Tutorial+4&structure=Tutorials
ManualObjects
A Crash Course in 3D Objects
创建网格之前我们来讨论一下什么是网格,网格是由什么组成的。一个网格粗略的由两部分组成:顶点缓存(vertex buffer)和索引缓存(index buffer)。
顶点缓存:定义3D空间的点。每一个顶点缓存中的元素都由你可以设置的几个属性定义。你必须设置的属性是顶点的位置。除此之外,你可以设置很多可选属性,如顶点颜色、纹理坐标等等。你需要使用哪些属性是根据你用网格要干什么。
索引缓存:作用是通过选择顶点缓存中的点来"连接他们"。索引缓存中指定每三个索引定义一个由GPU来绘制的三角形。你在索引缓存中选择的顶点的顺序告诉了图形卡,三角形是面向哪边的。一个逆时针绘制的三角形面对你,那么顺时针绘制的三角形就背对你。通常只有三角形的前面被渲染,所以确保你的三角形恰当的设置(setup)是很重要的。
虽然所有网格都有一个顶点缓存,但不是所有网格都有一个索引缓存。如我们要创建的网格就没有索引缓存,因为我们想要创建一个空矩形。最后要注意顶点缓存和索引缓存通常都是存储在图形卡的内存中的,这样你就可以只给图形卡传一个简单、独立的命令集来告诉它,使用那些预定义的缓存一次渲染整个3D网格。
Introduction
有两种方法使用OGRE来创建你的网格。第一种方法是吧SimpleRenderable对象划分子集并直接提供顶点缓存和索引缓存,这是最直接也是最复杂的一种创建网格的方法。要更简单的创建网格,OGRE提供了一个ManualObject的接口,它允许你使用一些简单的函数定义一个网格,而不是给缓存对象写枯燥的数据。不用把位置、颜色等写进缓存,你只需要调用position和colour函数就行了。
Selection Box
现在我们可以用本教程的程序来处理所有的选择盒功能,but that is just going to add clutter that doesn't need to be there. 将不同的对象独立成不同的类是一个很好的习惯,因为这让事情变得更加的整洁、简单。所有对于选择盒,我们要单独建一个SelectionBox.h头文件和一个SelectionBox.cpp文件,并声明该类为SelectionBox,它继承自Ogre::ManualObject。你的头文件和CPP文件如下:
SelectionBox.h
#ifndef __SelectionBox_h_
#define __SelectionBox_h_
#include "OgreManualObject.h"
class SelectionBox : public Ogre::ManualObject
{
public :
SelectionBox(const Ogre::String& name);
~SelectionBox(void);
void setCorners(float left, float top, float right, float bottom);
void setCorners(const Ogre::Vector2& topLeft, const Ogre::Vector2& topRight);
};
#endif
SelectionBox.cpp
#include "SelectionBox.h"
SelectionBox::SelectionBox(const Ogre::String& name): Ogre::ManualObject(name)
{
}
SelectionBox::~SelectionBox()
{
}
void SelectionBox::setCorners(float left, float top, float right, float bottom)
{
}
void SelectionBox::setCorners(const Ogre::Vector2& topLeft, const Ogre::Vector2& bottomRight)
{
setCorners(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
}
The Code
当我们创建选择矩形时,我们让他以2D的方式渲染。我们还要确保当OGRE的Overlays渲染时他渲染,这样可以让他在屏幕上所有对象之上显示。这很简单,添加如下代码到SelectionBox的构造函数:
setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); // when using this, ensure Depth Check is Off in the material
setUseIdentityProjection(true);
setUseIdentityView(true);
setQueryFlags(0);
第一个函数把对象的渲染队列设为Overlay队列。后面两个函数设置projection和view matrices为identity。projection和view matrices被很多渲染系统(如OpenGL和DirectX)用来定义where objects go in the world.你需要知道如果设置projection和view matrices为identity,我们基本上就是要创建一个2D对象。当定义了这个对象,坐标系统也要有一点改变。我们不再使用Z轴(如果你需要Z轴,那么设为-1)。作为代替我们有一个包含X、Y轴范围为-1到1的新的坐标系。最后设对象查询标记为0,可以预防选择矩形被包含在查询结果中。
对象已经设置好,我们现在要创建矩形。开始之前我们有个小问题,我们要根据鼠标位置调用这个函数。即我们会得到一个作为x、y坐标的0到1之间的数,我们还需要把这些数转换成-1到1之间的数。更复杂的是,Y坐标还是backwards(反向的)。CEGUI中鼠标光标定义屏幕顶部为0,底部为1。在我们的新坐标系中,屏幕顶部为+1,底部为-1。幸运的是,一些快速转换会解决这个问题。添加如下代码到setCorners函数中:
left = left * 2 - 1;
right = right * 2 - 1;
top = 1 - top * 2;
bottom = 1 - bottom * 2;
现在的位置都是在新坐标系统中的位置,下面我们要创建对象。为此我们首先调用begin方法,它需要两个参数,为对象这部分使用的材质的名字,绘制对象用的渲染操作。因为我们这里不用纹理,所以我们把材质留为空白。第二个参数是RenderOperation,我们可以使用点、线、矩形来渲染网格,如果我们要渲染一整个网格我们会使用矩形,但因为我们想要一个空矩形,所以我们用线条来渲染。线条在每个顶点和前一个你定义的顶点直接画一条线。所以创建矩形,我们定义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函数来移除之前的矩形,定义一个手动(manual)对象时,你会多次调用begin/end来创建子网格(他们有不同的materials/RenderOperations)。注意我们还要设Z参数为-1,因为我们要定义不使用Z轴的2D对象,把它设为-1将确保渲染时我们没有在摄像机的上面或后面。
最后我们需要设置对象的绑定盒,许多场景管理器都会剔除(cull)掉不再屏幕中的对象。即使我们创建了一个2D对象,OGRE仍然是一个3D引擎并把我们的2D对象像在3D空间一样对待。这意味着如果我们创建对象并绑定到一个场景节点,当我们看向其他地方时它就会消失。为了修正这个问题,我们设对象的绑定盒为无穷大(infinite),so that the camera will always be inside of it:
setBoundingBox(AxisAlignedBox::BOX_INFINITE);
确保我们在clear之后加上了这行代码,每次你调用ManualObject::clear,绑定盒都重设,所以如果你要创建另一个经常被删除的(cleared)ManualObject时,一定要注意,因为每次你重建它时绑定盒都必须被设置。
Volume Selection
Setup
首先我们要设置(setup)一些事,第一包含SelectionBox.h 到IntermediateTutorial4.h中,第二在IntermediateTutorial4类的protected中声明一个指向选择盒的指针变量:
#include SelectionBox.h
SelectionBox* mSelectionBox;
现在创建一个SelectionBox类实例的设置都已经设置好了,并让场景管理器为我们创建了一个volume(体积,容量)查询。添加如下代码到createScene的末尾:
mSelectionBox = new SelectionBox("SelectionBox");
mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(mSelectionBox);
mVolQuery = mSceneMgr->createPlaneBoundedVolumeQuery(PlaneBoundedVolumeList());
然后我们需要确保侦听器在调用过它之后进行了清除(clean up),添加如下代码到~IntermediateTutorial4:
mSceneMgr->destroyQuery(mVolQuery);
if(mSelectionBox)
{
delete mSelectionBox;
}
注意让场景管理器清除(clean up)为我们创建的查询而不是直接删除(delete)。
Mouse Handlers
下面我们要完成框选(volume selection),即当用户点击并拖动鼠标,屏幕上将绘制一个矩形。当松开鼠标时,所有包含在矩形中的对象都将被选中。首先处理鼠标被按下,我们需要保存开始位置并设SelectionBox为可见。添加如下代码到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;
mSelectionBox->clear();
mSelectionBox->setVisible(true);
}
注意我们使用的是CEGUI::MouseCursor'的X、Y坐标而不是OIS的。因为OIS有时得到的位置和CEGUI实际显示的位置不同。为了同步用户看到的位置,我们使用CEGUI的鼠标坐标。下面我们要做的是停止显示选择矩形并执行选择查询,当用户松开鼠标时。添加如下代码到mouseReleased函数中:
if (id == OIS::MB_Left)
{
performSelection(mStart, mStop);
mSelecting = false;
mSelectionBox->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;
mSelectionBox->setCorners(mStart, mStop);
}
每当鼠标移动时我们就调整mStop向量,这样我们就可以让setCorners成员函数来使用它。
PlaneBoundedVolumeListSceneQuery
现在SelectionBox可以正确的渲染了,我们需要执行框选了。添加如下代码到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和bottom四个变量。If语句确保我们有最低的left和top值。
下面我们得查看矩形实际的大小,,如果矩形很小,我们创建一个平面(plane bound volumes)的方法就会失败。如果矩形小于屏幕大小一定百分率,我们只会返回并且不执行选择。我任意选择了0.0001作为取消查询的临界值。在实际程序中,你可能需要找到矩形中点并执行一个标准的RaySceneQuery,而不是什么都不做。
if ((right - left) * (bottom - top) < 0.0001)
return;
现在我们来看函数的内容,并且我们需要它自己执行查询。PlaneBoundedVolumeQueries使用平面来围绕一个区域(enclose an area),然后选择该区域的任何对象。本例中我们将一个被5个面向内部的平面围绕的区域。为了在我们的矩形外创建这些平面,我们要创建4条射线,每条对应矩形的一个角。当我们有了这4条射线,我们就使用从射线上截取的点来创建平面:
Ogre::Ray topLeft = mCamera->getCameraToViewportRay(left, top);
Ogre::Ray topRight = mCamera->getCameraToViewportRay(right, top);
Ogre::Ray bottomLeft = mCamera->getCameraToViewportRay(left, bottom);
Ogre::Ray bottomRight = mCamera->getCameraToViewportRay(right, bottom);
现在我们要创建平面,注意我们在射线上每100个单位任意截取一个点。也可以是2个单位而不是100个单位。唯一要注意的是前面的平面(front plane),它从Camera前的3个单位开始。
Ogre::PlaneBoundedVolume vol;
vol.planes.push_back(Ogre::Plane(topLeft.getPoint(3), topRight.getPoint(3), bottomRight.getPoint(3))); // front plane
vol.planes.push_back(Ogre::Plane(topLeft.getOrigin(), topLeft.getPoint(100), topRight.getPoint(100))); // top plane
vol.planes.push_back(Ogre::Plane(topLeft.getOrigin(), bottomLeft.getPoint(100), topLeft.getPoint(100))); // left plane
vol.planes.push_back(Ogre::Plane(bottomLeft.getOrigin(), bottomRight.getPoint(100), bottomLeft.getPoint(100))); // bottom plane
vol.planes.push_back(Ogre::Plane(topRight.getOrigin(), topRight.getPoint(100), bottomRight.getPoint(100))); // right plane
这些平面现在定义了一个在摄像机前扩展为无限大的"open box"。你可以把我们用鼠标画出的矩形作为在摄像机前面的"open box"的结束。现在我们创建了平面,下面要执行查询:
Ogre::PlaneBoundedVolumeList volList;
volList.push_back(vol);
mVolQuery->setVolumes(volList);
Ogre::SceneQueryResult result = mVolQuery->execute();
最后我们需要处理查询的结果,首先取消之前被选中的对象的选定,然后选择所有被查询找到的对象。deselectObjects和selectObject函数在前一个教程中已经写好:
deselectObjects();
Ogre::SceneQueryResultMovableList::iterator iter;
for (iter = result.movables.begin(); iter != result.movables.end(); ++iter)
selectObject(*iter);
这就是所有我们需要为查询所做的事情。我们还可以使用volume查询的查询标记,即使本教程中我们没有用。添加如下代码到selectObject和deselectObjects函数中:
void IntermediateTutorial4::deselectObjects()
{
std::list<Ogre::MovableObject*>::iterator iter = mSelected.begin();
for(iter; iter != mSelected.end(); iter++)
{
(*iter)->getParentSceneNode()->showBoundingBox(false);
}
}
void IntermediateTutorial4::selectObject(Ogre::MovableObject* obj)
{
obj->getParentSceneNode()->showBoundingBox(true);
mSelected.push_back(obj);
}
编译运行你的程序,你现在可以框选场景中的对象了!