|
|
我看到Ogre3d.cn上有人正在翻译这篇,但是好几天没有翻译完。我正好练习到这篇,所以顺手翻了出来,大家交流。一直到Expert Question 1都已解决,但是Expert Question 2碰撞检测,还没有试验成功。有方法的朋友,请不吝赐教。可以在这里跟帖,或者email我,aaron.yp.young@gmail.com。
翻译:Aaron
中级教程1
动画、两点间行走及四元数初探
在练习这个教程时,无论遇到什么问题,都可以到[http://www.ogre3d.org/phpBB2/viewforum.php?f=2 Help Forum]发帖讨论。
==介绍=
在本教程中,我们会讲到如何使用Entity,让它活动起来,在预定义的点之间走动。也会初步涉及四元数旋转,如何使Entity保持面向其移动的方向。随着演示的进行,你应该可以慢慢地向自己的项目中增添代码,然后编译看看结果。
==前期准备==
这个教程会假设你已经了解如何创建Ogre项目并且可以成功编译。本教程还会实用STL deque数据结构。虽然如何使用deque不是必备的知识,但至少要知道模板是什么。如果你还不熟悉STL,我建议你最好还是先学习一下STL。那样会节省你很多时间。
==现在开始==
首先,你需要创建一个新项目。向项目中,增加一个文件,取名“MoveDemo.cpp”,添加如下代码:
#include "ExampleApplication.h"
#include <deque>
using namespace std;
class MoveDemoListener : public ExampleFrameListener
{
public:
MoveDemoListener(RenderWindow* win, Camera* cam, SceneNode *sn,
Entity *ent, deque<Vector3> &walk)
: ExampleFrameListener(win, cam, false, false), mNode(sn), mEntity(ent), mWalkList(walk)
{
} // MoveDemoListener
/* This function is called to start the object moving to the next position
in mWalkList.
*/
bool nextLocation()
{
return true;
} // nextLocation()
bool frameStarted(const FrameEvent &evt)
{
return ExampleFrameListener::frameStarted(evt);
}
protected:
Real mDistance; // The distance the object has left to travel
Vector3 mDirection; // The direction the object is moving
Vector3 mDestination; // The destination the object is moving towards
AnimationState *mAnimationState; // The current animation state of the object
Entity *mEntity; // The Entity we are animating
SceneNode *mNode; // The SceneNode that the Entity is attached to
std::deque<Vector3> mWalkList; // The list of points we are walking to
Real mWalkSpeed; // The speed at which the object is moving
};
class MoveDemoApplication : public ExampleApplication
{
protected:
public:
MoveDemoApplication()
{
}
~MoveDemoApplication()
{
}
protected:
Entity *mEntity; // The entity of the object we are animating
SceneNode *mNode; // The SceneNode of the object we are moving
std::deque<Vector3> mWalkList; // A deque containing the waypoints
void createScene(void)
{
}
void createFrameListener(void)
{
mFrameListener= new MoveDemoListener(mWindow, mCamera, mNode, mEntity, mWalkList);
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
MoveDemoApplication app;
try {
app.go();
} catch(Exception& e) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
MessageBox(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;
}
在继续下面的内容之前,确保你可以编译这段代码。
==创建场景==
在我们开始之前,我们已经在MoveDemoApplication中定义了3个变量。mEntity会保存我们创建的实体,mNode保存创建的场景节点,mWalkList包含所有我们想让目标走到的点。
在MoveDemoApplication::createScene函数中,增加如下代码。首先我们要设置环境光,以便我们可以看见放在屏幕上的物体。
// Set the default lighting.
mSceneMgr->setAmbientLight(ColourValue(1.0f, 1.0f, 1.0f));
接下来,我们会创建一个机器人。要创建一个机器人,我们将为机器人创建一个实体,然后在为它创建一个场景节点。
// Create the entity
mEntity = mSceneMgr->createEntity("Robot", "robot.mesh");
// Create the scene node
mNode = mSceneMgr->getRootSceneNode()->
createChildSceneNode("RobotNode", Vector3(0.0f, 0.0f, 25.0f));
mNode->attachObject(mEntity);
至此都是很基本的,所以就不细说了。在下面一段代码中,我们要告诉机器人它要到的地方。如果你对STL一点不了解,那么我在此做一点说明。deque对象是一个双向队列。我们将只使用它的部分方法。push_front和push_back方法将项相应地放置在队列首和队列尾。front和back方法相应地返回队列首和队列尾的值。pop_front和pop_back方法相应地删除队列首项和队列尾项。最后,empty方法返回队列是否空。下面代码向队列中增加两个向量,即我们稍候让机器人到的点坐标。
// Create the walking list
mWalkList.push_back(Vector3(550.0f, 0.0f, 50.0f ));
mWalkList.push_back(Vector3(-100.0f, 0.0f, -200.0f));
下面,我们想在屏幕上放一些标记物来显示机器人要到达的位置。这样可以让我们看到机器人相对于屏幕上其他物体的移动。注意它们的Y轴负方向位置。这些物体就放在机器人要去的位置下,看起来好像机器人就站在那一点上。
// Create objects so we can see movement
Entity *ent;
SceneNode *node;
ent = mSceneMgr->createEntity("Knot1", "knot.mesh");
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("Knot1Node",
Vector3(0.0f, -10.0f, 25.0f));
node->attachObject(ent);
node->setScale(0.1f, 0.1f, 0.1f);
ent = mSceneMgr->createEntity("Knot2", "knot.mesh");
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("Knot2Node",
Vector3(550.0f, -10.0f, 50.0f));
node->attachObject(ent);
node->setScale(0.1f, 0.1f, 0.1f);
ent = mSceneMgr->createEntity("Knot3", "knot.mesh");
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("Knot3Node",
Vector3(-100.0f, -10.0f,-200.0f));
node->attachObject(ent);
最后,我们要将镜头摆在一个合适的位置,好让我们看得更清楚。我们将镜头移动到一个更好的位置。
// Set the camera to look at our handiwork
mCamera->setPosition(90.0f, 280.0f, 535.0f);
mCamera->pitch(Degree(-30.0f));
mCamera->yaw(Degree(-15.0f));
现在,编译运行代码,你应该看到如下这样的画面:
[[Image:Intermediate_Tutorial_01.png]]
在继续下面内容前,注意一下MoveDemoListener的构造函数。我们随标准的BaseFrameListener参数一起,传递了SceneNode,Entity,和deque三个参数。
==动画==
我们现在要建立一些基本动画。在Ogre中,动画是非常简单的。要动起来,你需要从Entity对象处取得AnimationState,设置它的选项,启用它。这样就激活了动画,但是为了运行动画你还需要在每帧后给动画增加一个时间。我们将一次执行一步。首先,在MoveDemoListener的构造函数中增加如下代码:
// Set idle animation
mAnimationState = ent->getAnimationState("Idle");
mAnimationState->setLoop(true);
mAnimationState->setEnabled(true);
第二行就是从Entity取得AnimationState。第三行我们调用setLoop(ture),让我们的动画循环往复。对于某些动画(如死亡动画),我们应该将它设置为false。第四行实际上是启用动画。但是,等等……我们从哪儿得到“Idle”?这个神奇的常量怎么跑到这儿来的?每个mesh有它们自己定义好的动画集。要看到正在使用的某个mesh的所有动画,你需要下载OgreMeshViewer,从那里查看mesh。
现在,如果我们编译运行这个演示程序,我们看到……没什么改变。这是因为我们需要每帧每隔一段时间更新动画状态。找到MoveDemoListener::frameStarted方法,在函数的开始处增加这行代码:
mAnimationState->addTime(evt.timeSinceLastFrame);
现在,编译运行吧。你应该看到一个机器人静止地站在起点上。
==移动机器人==
现在我们要让机器人从一个点走到另一个点。在我们开始之前,我想介绍一下我们要存储在MoveDemoListener类中的几个变量。我们要用4个变量完成移动机器人的任务。首先,我们要在mDirection中存储机器人要移动的方向。在mDestination中存储机器人要移动到的当前目的地。在mDistance中存储机器人剩余要走的距离。最后,在mWalkSpeed中存储机器人移动的速度。
我们需要做的第一件事,就是清空MoveDemoListener构造函数。我们将用稍微有点儿不同的代码取代原来的代码。我们首先需要做的是先建立类的变量。我们将行走的速度设置为每秒35单位。有件重要的事需要注意,我们将mDirection显式地设置为ZERO向量,因为稍后我们要使用它决定机器人是否移动。
// Set default values for variables
mWalkSpeed = 35.0f;
mDirection = Vector3::ZERO;
既然做完了这些,那么我们就要让机器人动起来。要让机器人移动,我们只需告诉它改变动画就行了。但是,我们只想在有下一个移动点时才让它移动。所以,我们调用nextLocation函数。在MoveDemoListener::frameStarted方法的开始处,增加如下代码:
if (mDirection == Vector3::ZERO)
{
if (nextLocation())
{
// Set walking animation
mAnimationState = mEntity->getAnimationState("Walk");
mAnimationState->setLoop(true);
mAnimationState->setEnabled(true);
}
}
如果你立即编译运行这段代码,机器人会原地踏步。这是因为机器人初始方向是ZERO,我们的MoveDemoListener::nextLocation函数总是返回true。在接下来的几步中,我们会给MoveDemoListener::nextLocation函数多增加一点智能。
现在,我们要真的移动机器人了。我们需要让它每一帧移动一点点。进入MoveDemoListener::frameStarted方法。我们要在刚才我们添加的if语句段后面增加如下代码。这段代码会控制机器人的实际移动;mDirection!=Vector3::ZERO。
else
{
Real move = mWalkSpeed * evt.timeSinceLastFrame;
mDistance -= move;
现在,我们需要检查看看如果机器人过了目标点怎么办。也就是说,如果mDistance比0小,我们需要“跳”到目标点上,然后再向下一个点移动。注意,我们现在将mDirection设置为ZERO向量。如果nextLocation方法不改变mDirection(例如,没有别的点可以去了),那么机器人就不再移动了。
if (mDistance <= 0.0f)
{
mNode->setPosition(mDestination);
mDirection = Vector3::ZERO;
既然我们已经移动到第一个点了,那么我们就需要设置到下一个点的动画了。一旦我们知道是否需要移动到下一点,我们就能设置好合适的动画;如果有要去的下一点,就设置行走“Walk”;如果没有更多的目的地点了,就设置空闲“Idle”。设置空闲动画是一件非常简单的事。
// Set animation based on if the robot has another point to walk to.
if (! nextLocation())
{
// Set Idle animation
mAnimationState = mEntity->getAnimationState("Idle");
mAnimationState->setLoop(true);
mAnimationState->setEnabled(true);
}
else
{
// Rotation Code will go here later
}
}
注意,如果队列中有多个要到达的点,我们不必再次设置行走动画。因为机器人已经在走了,没有必要再告诉它一次。但是,如果机器人需要去另一个点,那么我们需要旋转它,让它面对那个点。目前,我们将else语句段留空;记得这一点,因为我们稍后会回来的。
上面这些保证了我们非常接近目标点时的情况。现在,我们需要控制正常情况了,即当我们正在移动中,但还没有到达目标点时的情况。要做到这一点,我们将沿着移动的方向,以move变量定义的距离量,平移机器人。增加如下代码,完成这一点:
else
{
mNode->translate(mDirection * move);
} // else
} // if
我们快完工了。我们的代码现在除了移动必须的几个变量外,已经基本完成了。如果我们合理设置移动变量,那么我们的机器人会移动得比较自然。找到MoveDemoListener::nextLocation函数。当我们走完所有目的地时,这个函数会返回false。将这一代码放在这个函数的开始处。(注意,你应该在函数末尾返回ture。)
if (mWalkList.empty())
return false;
现在我们需要设置变量了。首先,我们将从deque中取出目的地向量。我们将用目的地向量减去场景节点当前距离目的地的位置设置方向向量。尽管这有一些问题。记得我们在frameStarted中是如何用move移动量乘以mDirection的吗?如果我们这么做,我们需要将方向向量变成单位向量(即,向量的模为1)。normalise函数可以为我们做到这一点,并且返回原来的向量长度。太好了不是吗,因为我们也需要设置到目的地的距离。
mDestination = mWalkList.front(); // this gets the front of the deque
mWalkList.pop_front(); // this removes the front of the deque
mDirection = mDestination - mNode->getPosition();
mDistance = mDirection.normalise();
现在编译运行这些代码。机器人现在走过所有的点,但是它总是面向Vector3::UNIT_X(它默认的方向)。我们根据要移动到的点改变它面朝的方向。
我们要怎么做获得机器人面朝的方向,再用rotate函数旋转目标物体到正确的方向上呢?在上一步,机器人离开当前位置的地方插入如下代码。第一行得到机器人面朝的方向。第二行创建一个表示从当前方向旋转到目的方向的四元数。第三行实际旋转机器人。
Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;
Ogre: uaternion quat = src.getRotationTo(mDirection);
mNode->rotate(quat);
我们在基础教程4中简单提到过四元数,但是这次是我们第一次真正使用它。简单来说,四元数是三维空间中旋转的表示方法。它们被用于跟踪空间中的物体的位置,并且在Ogre中可以被用来旋转物体。在第一行里,我们调用了getOrientation方法,它返回一个表示机器人面朝方向的四元数。因为Ogre不知道哪面是机器人的正面,所以我们必须再乘以UNIT_X向量(机器人“原本”面朝的方向)。我们将这个方向保存到变量src中。在第二行中,getRotationTo方法给了我们一个表示从机器人面朝的方向转到我们想要它面朝的方向的四元数。第三行,我们旋转场景节点让机器人面朝新的方向。
我们创建的代码只有一个问题。那就是,这里有一个特殊情况,即SceneNode::rotate可能会失败。如果我们要尝试将机器人旋转180度,旋转代码会蹦出一个被0除的错误。为了修补它,我们会先做一个测试,看看我们是否要做180度旋转。如果要做,我们会简单地用yaw函数让机器人向后一转,而不是进行rotate旋转。将上面所说的三行代码,做如下修改:
Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;
if ((1.0f + src.dotProduct(mDirection)) < 0.0001f)
{
mNode->yaw(Degree(180));
}
else
{
Ogre::Quaternion quat = src.getRotationTo(mDirection);
mNode->rotate(quat);
} // else
所有这些应该是不难理解的,除了if语句那一行。如果两个单位向量方向彼此相反(即,二者之间的夹角是180度),那么它们的点积会是-1。所以,如果我们dotProduct两个向量,而结果又是-1.0f,那么我们需要反转180度,否则我们就要用rotate代替。为什么要加上1.0f,还要检查是否小于0.0001f呢?别忘了浮点数的舍入错误。你应该永远都不直接地比较两个浮点数。最后,注意在这个例子中,这两个向量的点积会落入-1到1的闭区间范围内。假如对此不太清楚,要做图形编程,你至少需要了解线性代数!最最起码你需要复习一下四元数和旋转入门,并找一本介绍向量和矩阵基本操作的书作为参考。
至此,我们的代码就全部完成了!编译运行之,看看机器人行走在给定的点之间吧。
==进阶练习==
===简单问题===
:1. 给机器人的路上增加更多的点。保证在这些新加的点下也增加更多的结,使得我们可以跟踪机器人的位置。
:2. 机器人走完全程就不应该再继续存在!当机器人已经完成行走过程,让它执行死亡动画而不是空闲动画。死亡动画是“Die”。(行走动画是“Walk”,空闲动画是“Idle”)
===中级问题===
:1. mWalkSpeed有点问题,你注意到了吗?我们只是一次性设置了一个值,然后就再也没变过。就好像是一个类的不变的静态变量。试着改变一下这个变量。(提示:可以定义键盘的+和-分别表示加速和减速)
:2. 代码中有些地方非常取巧,例如跟踪机器人是否正在走,用了mDirection向量跟Vector3::ZERO比较。如果我们换用一个bool型变量mWalking来跟踪机器人是否在移动也许会更好。实现这个改变。
===困难问题===
:1. 这个类的一个局限是你不能在创建对象后再给机器人行走的路线增加新的目的地点。修补这个问题,实现一个带有一个Vector3参数的新方法,并且将它插入mWalkList队列。(提示:如果机器人还未完成行走过程,你就只需要将目的地点插入队列尾即可。如果机器人已经走完全程,你将需要让它再次开始行走,然后调用nextLocation开始再次行走。)
===专家问题===
:1. 这个类的另一个主要局限是它只跟踪一个物体。重新实现这个类,使之可以彼此独立地移动任意数量的物体。(提示:你可以再创建一个类,这个类包含移动一个物体所需要知道的全部东西。把它存储在一个STL对象中,以便以后可以通过key获取数据。)如果可以不注册附加的framelistener,你会得到加分。
:2. 做完上面的改变,你也许注意到了机器人可能会彼此发生碰撞。修复它,或者创建一个聪明的寻路函数,或者当机器人碰撞时检测,阻止它们彼此穿透而过。
|
|