书<<Beginning.XNA.3.0.Game.Programming.From.Novice.to.Professional>>第10章,图片未贴出,原译文在附近中
光线,摄影机,变换
一个3d场景可能包含很多摄影机,光线和物体.场景一些类来代表这些物体可以使你的游戏更容易管理.
本章,你将创建一个基础框架来管理摄影机,光线和物体变换.11,12,13章创建一个完整的游戏时你将发现你的游戏结构很受益于这些类.
3d游戏的一个最核心的组件是摄影机.因此,本章首先来说明该如何管理摄影机系统.
摄影机
根据创建的游戏的类型,你可以需要使用不同类型的摄影机,如固定位置的摄影机,fps摄影机,tps摄影机,rts摄影机等,有这么类型的摄影机最好创建一个基础的摄影机其他特殊摄影机来继承它.
基础摄影机类
本节你将创建一个通用的摄影机基类,命名为BaseCamera.此类将处理摄影机视野和投影矩阵,用可视锥体来定义可见的视野,只有处于可视锥体内的物体才在摄影机视野内.
摄影机的可视锥体是通过摄影机的视野和投影矩阵来定义的.当你的显卡将3d场景转换为2d图形时这些矩阵是必须的.可视锥体也常被用来检测物体是否可见,这可以帮你决定哪些物体应该被绘制.
摄影机透视矩阵
本节创建的投影矩阵定义摄影机可视锥体的边界,这是摄影机视野的可见范围.摄影机的可视锥体由摄影机的视野角度和近,远面共同决定.你将在SetPerspectiveFov方法中创建投影矩阵(给出这个名字是因为摄影机的视角定义了视野的一部分).也需要定义Projection属性用以程序来获取投影矩阵.
BaseCamera类只支持透视投影,这是游戏中最常用的类型.你可以使用下面的代码来创建和更新摄影机透视投影矩阵:
// 透视投影参数
float fovy;
float aspectRatio;
float nearPlane;
float farPlane;
// 矩阵和更新标识
protected bool needUpdateProjection;
protected bool needUpdateFrustum;
protected Matrix projectionMatrix;
//获取摄影机投影矩阵
public Matrix Projection
{
get
{
if (needUpdateProjection) UpdateProjection();
return projectionMatrix;
}
}
// 设置摄影机透视投影
public void SetPerspectiveFov(float fovy, float aspectRatio, float nearPlane,float farPlane)
{
this.fovy = fovy;
this.aspectRatio = aspectRatio;
this.nearPlane = nearPlane;
this.farPlane = farPlane;
needUpdateProjection = true;
}
//更新摄影机透视投影矩阵
protected virtual void UpdateProjection()
{
// 创建透视视角矩阵
projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(fovy), aspectRatio, nearPlane, farPlane);
needUpdateProjection = false;
needUpdateFrustum = true;
}
SetPerspectiveFov方法储存了新的透视投影参数,但没有生成新的投影矩阵.只是将needUpdateProjection变量设为true,指明投影矩阵在使用之前需要更新.当通过Projection属性来获取投影矩阵时,会根据需要来更新投影矩阵.最后,在UpdateProjection方法内,你使用Matrix的CreatePerspectiveFieldOfView方法来生成新的透视投影矩阵.
注意当投影矩阵更新时摄影机的可视锥体就需要更新,因为它依赖投影矩阵的4个参数.
摄影机视野(位置和方位)
摄影机并不是由可视锥体来唯一定义.你还需要确定摄影机在3d世界中的位置,以及它的方位.世界中摄影机的位置和方位由本节中创建的可视view矩阵来定义.你将创建SetLookAt方法来设定摄影机的可视矩阵,并用View属性来获取它.通过获取view和projection矩阵使xna完成物体3d到2d的转换.
使用Matrix.CreateLookAt方法来创建view矩阵.你需要知道3个摄影机向量(或方向):向前,向右,向上.在3d由这3个向量来唯一定义一个方位.SetLookAt方法来计算这3个向量,从摄影机的位置开始,它的目标,及它的向上的向量.
你可以像计算任意向量一样通过2点来定义向前的向量:从结束的位置减去开始的位置.为了找到向右的向量,你需要计算向前和向上向量的垂直向量,使用Vector3.Cross来获取正确值.
下节将看到计算的细节.
使用下面的代码来修改和更新摄影机的视野矩阵:
// 位置和目标
Vector3 position;
Vector3 target;
// 方位向量
Vector3 headingVec; //前
Vector3 strafeVec; //右
Vector3 upVec; //上
// 矩阵 和标识
protected bool needUpdateView;
protected bool needUpdateFrustum;
protected Matrix viewMatrix;
// 获取摄影机的视野矩阵
public Matrix View
{
get
{
if (needUpdateView) UpdateView();
return viewMatrix;
}
}
// 设置摄影机的视野
public void SetLookAt(Vector3 cameraPos, Vector3 cameraTarget, Vector3 cameraUp)
{
this.position = cameraPos;
this.target = cameraTarget;
this.upVec = cameraUp;
// 计算摄影机的所有轴 (向前heading, 向上upVector, 和 向右 strafeVector)
headingVec = cameraTarget - cameraPos;
headingVec.Normalize();
upVec = cameraUp;
strafeVec = Vector3.Cross(headingVec, upVec);
needUpdateView = true;
}
// 更新摄影机的视野矩阵
protected virtual void UpdateView()
{
viewMatrix = Matrix.CreateLookAt(position, target, upVec);
needUpdateView = false;
needUpdateFrustum = true;
}
如SetPerspectiveFov方法,SetLookAt方法储存新的视野参数但并没生成新的视野矩阵.只是激活了needUpdateView变量.当程序通过View属性来获取视野矩阵时将根据需要更新视野矩阵.
最后,在UpdateView方法中,使用xna的Matrix.CreateLookAt方法来生成新的视野矩阵.注意当视野矩阵更新时摄影机的可视锥体需要更新.
摄影机坐标系统
每次当通过SetLookAt方法来改变摄影机的配置时,你需要计算3个摄影机坐标系向量:向前(z),向右(x),向上(y).图10-1 显示了放置在世界坐标系统中的摄影机坐标系统.注意因为这些向量组合起了摄影机坐标系统,所有向量必须是单位向量(长度必须为1)并互相垂直.你还可以使用单位向量来表示方向.因为方向不需要大小.坐标系更多的信息参考第8章.
图 10-1,世界坐标系中的摄影机坐标系.摄影机的x,y,z轴分别表示向右,向上,向前的向量.
你可以按以下步骤来计算摄影机向量:
向前:向前的向量是从摄影机的位置到目标位置的方向.它用来描述摄影机的朝向.可以用摄影机的目标位置减去摄影机的位置来求得.
向上:向上向量确定了摄影机的向上的方向及被用于摄影机的方位.如你可以使用向量(0,1,0)来将世界的y轴作为摄影机的向上向量.
向右:向右向量是一个垂直于向上和向前的向量.可以通过使用向量叉乘来计算.xna的Vector3.Cross来执行叉乘计算.注意此向量必须为单位向量(你必须将计算结果归一化).并且传递到Cross方法向量的顺序不同将导致不同的结果.(顺序为cross(right,up))
当你需要根据轴来转变摄影机时就会用到摄影机坐标系中的3根轴;比如,向前移动摄影机.
我们提到3根轴必须垂直而之前的代码并没有对此作出保障.假设摄影机面向正前并轻微有点向上.如果你调用SetLookAt方法并使用正常的向上向量(0,1,0)来做为第3个参数,将会出现一些麻烦,因为第3轴并不垂直于其余的2轴.(向右的轴垂直于向前的轴,因为它是通过叉乘来获取的).如果你想确保向上的向量一定垂直于向前的向量,计算完向右的向量后,你必须再次计算向前和向右向量的叉乘.如下:
upVec = Vector3.Cross(strafeVec,headingVec);
这样第3个向量就会与前面2个互助垂直.
摄影机视锥体
将使用xna的BoundingFrustum类来表示视锥体.xna有一些类专门用来表示体积,并且每个都有碰撞检测方法.可用它们来快速检测2物体是否碰撞.视锥体特殊的地方是可以用来检查物体是否在摄影机视野范围内.
Xna中边界体积类是BoundingBox(轴对称边界盒),BoundingSphere,及BoundingFrustum.为使碰撞检测尽量的准确,你应该是边界类尽可能的贴近实际的3d物体.摄影机的视锥体使用BoundingFrustum类.使用BoundingBox来表示完整的人体.只有需要检查手部及头部时才用到BoundingSphere.使用BoundingFrustum来检查物体在视锥体内还是外.
创建UpdateFrustum方法来生成摄影机的视锥体,用Frustum属性来获取它.你需要使用摄影机的view和projection矩阵来构造一个新的BoundingFrustum类以生成摄影机的视锥体.
摄影机由视野矩阵(位置和方位)和投影矩阵(视锥体的形状)组成,这也是创建BoundingFrustum需要这些矩阵的原因.可以使用下面的代码来创建摄影机的视锥体:
public BoundingFrustum Frustum
{
get
{
if (needUpdateProjection)
UpdateProjection();
if (needUpdateView)
UpdateView();
if (needUpdateFrustum)
UpdateFrustum();
return frustum;
}
}
protected virtual void UpdateFrustum()
{
frustum = new BoundingFrustum(viewMatrix * projectionMatrix);
needUpdateFrustum = false;
}
最后,BaseCamera类必须有一个抽象的Update方法,用来定义摄影机应该如何被更新.Update是一个抽象的方法,后面的每一个基础BaseCamera的摄影机都必须实行此方法.Update方法签名如下:
public abstract void Update(GameTime time);
一个第三人称摄影机
本节中,你将继承BaseCamera类来创建一个更特殊的摄影机:第3人称摄影机.此摄影机继承BaseCamera命名为ThirdPersonCamera.第3人称摄影机的目标是当物体移动时跟随它并保持一定的距离.否则当物体移动到摄影机边界后,将造成摄影机剧烈移动.
让摄影机跟随一个物体-如,玩家控制角色-你需要定义一些如下的参数:
1 跟随位置,摄影机跟随的目标物体的位置.
2 跟随方向,摄影机沿哪个方向移动来跟随物体.
3 跟随速度
4 跟随距离,摄影机与物体的距离
这里,用图来说明跟随距离,物体与摄影机间有3个变量:minimum,desired,maximum.图10-2说明了一些必须配置的参数.
图10-2 方块是摄影机跟随的位置,点是摄影机的最大,期望,最小的距离.
设置跟随参数
在ThirdPersonCamera类中,创建SetChaseParameters方法来设置摄影机的非每一帧更新的跟随参数:跟随距离和速度.你可以通过存取器来配置频繁更新的跟随位置和方向参数:
// 跟随的参数
float desiredChaseDistance;
float minChaseDistance;
float maxChaseDistance;
float chaseSpeed;
Vector3 chasePosition;
public Vector3 ChasePosition
{
get { return chasePosition; }
set { chasePosition = value; }
}
Vector3 chaseDirection;
public Vector3 ChaseDirection
{
get { return chaseDirection; }
set { chaseDirection = value; }
}
public void SetChaseParameters(float chaseSpeed,float desiredChaseDistance, float minChaseDistance, float maxChaseDistance){
this.chaseSpeed = chaseSpeed;
this.desiredChaseDistance = desiredChaseDistance;
this.minChaseDistance = minChaseDistance;
this.maxChaseDistance = maxChaseDistance;
}
更新摄影机位置
每次摄影机被更新,位置需要重新计算.理想情况下,摄影机新位置等于摄影机的跟随位置减去跟随方向,乘以跟随距离,如图10-2.如果摄影机以一个固定距离来跟随,那么摄影机期望的新位置将是摄影机的最终位置.然而为了让摄影机平滑移动,摄影机和跟随位置间的距离在最小与最大范围间(由属性minChaseDistance和maxChaseDistance定义).
Vector3 targetPosition = chasePosition;
Vector3 desiredCameraPosition = chasePosition –
chaseDirection * desiredChaseDistance;
float interpolatedSpeed = MathHelper.Clamp(chaseSpeed *
elapsedTimeSeconds, 0.0f, 1.0f);
desiredCameraPosition = Vector3.Lerp(position, desiredCameraPosition,
interpolatedSpeed);
这样,摄影机新的位置通过在当前位置与期望位置间进行线性插值来计算.线性插值是根据2个值和一个比重来进行插值,比重通常是一个浮点数值域是[0,1].如,在10,20,比重为0.50进行线性插值就是说"在10,20间给出50%的数".结果是15.比重为0,0.25,1结果为10,12.5,20.像10,20间的0%,25%,100%.3d向量的线性插值在各个分量(x,y,z)中进行.
摄影机位置插值所使用的比重是根据上次更新所逝去时间和摄影机速度来计算的.然而,因为插值比重必须在0,1间,所以必须将其限制在0,1间.使用xna的Vector3的Lerp方法来在向量间进行插值.chaseSpeed值越小,摄影机反应的越慢,摄影机开始跟着物体移动所需要的时间越长.chaseSpeed值越高,摄影机反应的越快,启动的时间越短.(摄影机的反应时间通常称其为延迟)
创建UpdateFollowPosition方法来更新摄影机的位置.以下是UpdateFollowPosition的代码.为了保持整洁,你将所有的方法都设定为1.可以调用Normalize方法或除以它们的长度,结果储存在targetVector,代码如下:
private void UpdateFollowPosition(float elapsedTimeSeconds,
bool interpolate)
{
Vector3 targetPosition = chasePosition;
Vector3 desiredCameraPosition = chasePosition- chaseDirection *
desiredChaseDistance;
if (interpolate)
{
float interpolatedSpeed = MathHelper.Clamp(
chaseSpeed * elapsedTimeSeconds, 0.0f, 1.0f);
desiredCameraPosition = Vector3.Lerp(position,
desiredCameraPosition, interpolatedSpeed);
// 限制最小和最大的跟随距离
Vector3 targetVector = desiredCameraPosition - targetPosition;
float targetLength = targetVector.Length();
targetVector /= targetLength;
if (targetLength < minChaseDistance)
{
desiredCameraPosition = targetPosition +
targetVector * minChaseDistance;
}
else if (targetLength > maxChaseDistance)
{
desiredCameraPosition = targetPosition +
targetVector * maxChaseDistance;
}
}
// 需要重新计算向前,向右,向上
SetLookAt(desiredCameraPosition, targetPosition, upVec);
}
UpdateFollowPosition方法有interpolate参数,用来定义摄影机是否需要直接被放置在期望的位置(interpolate为false),还是通过平滑的插值到期望位置.当摄影机第一次跟随物体时需要设置interpolate值为false,并将摄影机初始化到开始的位置.因为这时还没跟随的目标.使用interpolate来检测是否需要将摄影机移动到开始的位置.
当摄影机最后的位置通过在当前位置和目标位置进行插值计算后,你需要检查摄影机到跟随位置的距离是否在最小和最大距离间,见图10-2.如果距离小于最小的距离则设为最小的距离,如果超过最大则设为最大.这些测试很重要,可以确保摄影机可以跟随物体,而物体的速度比摄影机快.
摄影机绕目标旋转
需要加入的另外一个特性是摄影机可以绕目标旋转.为了达成此目的,你需要定义摄影机最大旋转速度和当前摄影机的旋转.另外需要摄影机旋转开始和结束都平滑,你需要保持跟踪旋转速度.在ThirdPersonCamera类中加入3个属性:
//最大允许的旋转
public static float MAX_ROTATE = 30.0f;
// 摄影机轴上的,当前的旋转角度(向前,向上,向右)
Vector3 eyeRotate;
// 摄影机轴上的旋转速度
Vector3 eyeRotateVelocity;
public Vector3 EyeRotateVelocity
{
get { return eyeRotateVelocity; }
set { eyeRotateVelocity = value; }
}
允许的旋转角度在-MAX_ROTATE和MAX_ROTATE间.如果摄影机的旋转出界,你需要将其限制在此范围内.eyeRotate向量将储存当前摄影机的旋转,分量x,y,z分别储存摄影机的向右,向上,向前周的旋转角度.最后eyeRotateVelocity储存摄影机旋转角度更新的速度.
为了将摄影机view矩阵的计算加入摄影机的旋转,需要重新BaseCamera的UpdateView方法.记住UpdateView方法是通过View属性来获取view矩阵时才被调用并将needUpdateView设为true.下面是ThirdPersonCamera的UpdateView方法:
protected override void UpdateView()
{
Vector3 newPosition = Position - Target;
// 计算摄影机新的位置,绕轴旋转
newPosition = Vector3.Transform(newPosition,
Matrix.CreateFromAxisAngle(UpVector,
MathHelper.ToRadians(eyeRotate.Y)) *
Matrix.CreateFromAxisAngle(StrafeVector,
MathHelper.ToRadians(eyeRotate.X)) *
Matrix.CreateFromAxisAngle(HeadingVector,
MathHelper.ToRadians(eyeRotate.Z))
);
viewMatrix = Matrix.CreateLookAt(newPosition + Target,
Target, UpVector);
needUpdateView = false;
needUpdateFrustum = true;
}
在重新的UpdateView方法中,你需要考虑旋转的情况下来计算摄影机的位置.摄影机与轴有关的旋转储存在eyeRotation属性.为了让摄影机绕自身轴旋转你首先需要创建一个旋转矩阵来绕任意轴旋转.可以使用Matrix的CreateFromAxisAngle方法来创建.通过按顺序组合摄影机的y,x,z轴旋转矩阵,可以计算出最后的摄影机旋转矩阵.使用组合矩阵来转换世界坐标系中的3d位置到摄影机坐标系.(见图10-1).
更新摄影机
最后ThirdPersonCamera必须实行Update方法:它是由父类BaseCamera所定义的抽象方法.每次摄影机需要更新时都会调用.在Update方法中,你需要更新摄影机的属性,及调用必要的方法来更新摄影机.UpdateView和UpdateProjection用来更新摄影机的view和projection矩阵.这2个方法只有当view和projection矩阵通过属性来访问时才会被调用并更新.下面是ThirdPersonCamera类的Update方法:
public override void Update(GameTime time)
{
float elapsedTimeSeconds =
(float)time.ElapsedGameTime.TotalSeconds;
// 更新跟随的位置
UpdateFollowPosition(elapsedTimeSeconds, !isFirstTimeChase);
if (isFirstTimeChase)
{
eyeRotate = Vector3.Zero;
isFirstTimeChase = false;
}
// 根据旋转速度来计算新的旋转
if (eyeRotateVelocity != Vector3.Zero)
{
eyeRotate += eyeRotateVelocity * elapsedTimeSeconds;
eyeRotate.X = MathHelper.Clamp(eyeRotate.X,
-MAX_ROTATE, MAX_ROTATE);
eyeRotate.Y = MathHelper.Clamp(eyeRotate.Y,
-MAX_ROTATE, MAX_ROTATE);
eyeRotate.Z = MathHelper.Clamp(eyeRotate.Z,
-MAX_ROTATE, MAX_ROTATE);
needUpdateView = true;
}
}
在Update方法中,你首先使用UpdateFollowPosition方法来更新摄影机的位置.然后假定摄影机的旋转速度不是0,根据摄影机的旋转速度和逝去的时间计算摄影当前的旋转.这样可以确保不同的pc不同的处理速度都有一致的效果.
光线
伴随摄影机系统,你的游戏还需要包含光线.光线对于一个真实感的游戏非常重要,特别是3d游戏.一个游戏场景可以拥有变化的光影分散的环绕着,如根据角色的位置动态的激活.在一个场景里放置很多光影最不利的方式是光源越多就需要更多的处理.游戏中的一些光线是直射光(如,阳光),聚光等,点光(向全方向发散的光).
本节,你将创建一些光线助手类.这样来处理光线将保持良好的结构,允许你的光线系统很容易管理及整合进游戏引擎.13章会示范.
继承光线
本节,将创建一个光线的基类,命名为BaseLight.含有构造器及一些方法.因为不同的关系类型没有多少相同的地方,在此基类中储存一个光源颜色.每个特殊的光线继承此类并会加入特殊属性:
// 光线 漫反射或高光颜色
Vector3 color;
public Vector3 Color
{
get { return color; }
set { color = value; }
}
BaseLight类的color属性被用于光线的漫反射或高光的颜色.漫反射颜色的强度依赖入射光的角度和物体的表面法线,只有光线被物体反射进摄影机时高光才可见.漫反射与高光颜色只同时存在一个.注意颜色向量的(x,y,z)分量表示的是颜色的rgb格式.
还需要注意,光线没有环境分量.环境分量是场景任意一部分的光线颜色的总数,并不依赖与摄影机或光线的位置.在场景中对于任意光线环境光颜色都一样.
点光/全方向光
本节中,将继承BaseLight的类来创建一个更特殊的光线类型:点光源(全方向光源).将此类命名为PointLight并继承BaseLight.
点光源很容易定义,你只需在PointLight类中储存光线的位置:
public class PointLight : BaseLight
{
// 全方向光源的位置
Vector3 position;
public Vector3 Position
{
get { return position; }
set { position = value; }
}
}
与位置一样,你也可以储存点光源的范围,可以通过它来计算光线的衰减.但是,为了简化光线积分,本例只储存了光线的位置.
摄影机和光线管理器
为了简化摄影机和光线的管理,需要创建2个不同的管理器:一个负责摄影机一个负责光线.
摄影机管理器
本节将要创建一个摄影机管理器,命名为CameraManager.摄影机管理器允许在场景中放置多个摄影机,并在扫描时间管理激活的摄影机.激活的摄影机被用来观察场景.下面是CameraManager类的完整代码:
public class CameraManager
{
// 激活摄影机的索引和引用
int activeCameraIndex;
BaseCamera activeCamera;
// Sorted list 包含所有的摄影机
SortedList<string, BaseCamera> cameras;
#region Properties
public int ActiveCameraIndex
{
get { return activeCameraIndex; }
}
public BaseCamera ActiveCamera
{
get { return activeCamera; }
}
public BaseCamera this[int index]
{
get { return cameras.Values[index]; }
}
public BaseCamera this[string id]
{
get { return cameras[id]; }
}
public int Count
{
get { return cameras.Count; }
}
#endregion
public CameraManager()
{
cameras = new SortedList<string, BaseCamera>(4);
activeCameraIndex = -1;
}
public void SetActiveCamera(int cameraIndex)
{
activeCameraIndex = cameraIndex;
activeCamera = cameras[cameras.Keys[cameraIndex]];
}
public void SetActiveCamera(string id)
{
activeCameraIndex = cameras.IndexOfKey(id);
activeCamera = cameras[id];
}
public void Clear()
{
cameras.Clear();
activeCamera = null;
activeCameraIndex = -1;
}
public void Add(string id, BaseCamera camera)
{
cameras.Add(id, camera);
if (activeCamera == null)
{
activeCamera = camera;
activeCameraIndex = -1;
}
}
public void Remove(string id)
{
cameras.Remove(id);
}
}
在CameraManager类中,摄影机被储存在SortedList中,以string为key,BaseCamera为value的形式存放.这样摄影机可以通过id或字符串来访问.注意使用的索引并不是摄影机添加到管理器的顺序,它们是按名字来排序的.CameraManager类提供了添加和删除及激活摄影机的方法.
光线管理器
本节将创建一个光线管理器,命名为LightManager.与摄影机类型,关系管理器允许你在场景内加入不同的光线.不同的是,所有加入的光线都是激活的;因此不需要定义激活的光线,此后与场景有关系的环境光都被放置和储存在这里.下面是LightManager类完整的代码:
public class LightManager
{
// 场景全局环境分量
Vector3 ambientLightColor;
// Sorted list 包含所有的光线
SortedList<string, BaseLight> lights;
#region Properties
public Vector3 AmbientLightColor
{
get { return ambientLightColor; }
set { ambientLightColor = value; }
}
public BaseLight this[int index]
{
get { return lights.Values[index]; }
}
public BaseLight this[string id]
{
get { return lights[id]; }
}
public int Count
{
get { return lights.Count; }
}
#endregion
public LightManager()
{
lights = new SortedList<string, BaseLight>();
}
public void Clear()
{
lights.Clear();
}
public void Add(string id, BaseLight light)
{
lights.Add(id, light);
}
public void Remove(string id)
{
lights.Remove(id);
}
}
在LightManager中,光线储存在SortedList中,与CameraManager类似.这样可以通过id或name来访问光线.LightManager类提供了添加删除光线的方法.
对象变换
3d游戏中变换是非常重要.任何变换都是平移,旋转,缩放的组合.变换本身被用来储存3d世界中任意物体的位置或方位.
变换被储存在一个矩阵中(4x4浮点矩阵).储存3d世界中物体的位置和方位的矩阵称为World矩阵,正如同它的定义表示物体在世界中的位置和方位.除了世界矩阵外,你还需要摄影机的View和Projection矩阵来将3d位置转换到2d屏幕坐标去.
为了简化物体变换的处理,需要创建一个类命名为Transformation.此类储存对象的平移,旋转和缩放并创建一个矩阵来持有所有变换的组合,下面是代码:
// 平移
Vector3 translate;
// 绕世界(X, Y, Z)坐标轴旋转
Vector3 rotate;
// 绕 X, Y, Z 轴旋转
Vector3 scale;
bool needUpdate;
// 储存变换的组合
Matrix matrix;
public Vector3 Translate
{
get { return translate; }
set { translate = value; needUpdate = true; }
}
public Vector3 Rotate
{
get { return rotate; }
set { rotate = value; needUpdate = true; }
}
public Vector3 Scale
{
get { return scale; }
set { scale = value; needUpdate = true; }
}
public Matrix Matrix
{
get
{
if (needUpdate)
{
// 计算最后的矩阵 (Scale * Rotate * Translate)
matrix = Matrix.CreateScale(scale) *
Matrix.CreateRotationY(MathHelper.ToRadians(rotate.Y)) *
Matrix.CreateRotationX(MathHelper.ToRadians(rotate.X)) *
Matrix.CreateRotationZ(MathHelper.ToRadians(rotate.Z)) *
Matrix.CreateTranslation(translate);
needUpdate = false;
}
return matrix;
}
}
在Transformation类中,平移,旋转和缩放用Vector3来储存在translate,rotate,scale属性中.可以通过属性来存取.平移,Vector3储存平移/位置的x,y,z分量.旋转另一个Vector3储存分别绕3
轴的旋转量.matrix属性用Matrix对象储存平移,旋转和缩放的组合.三种变换的组合成为世界变换.最为物体在3d世界位置中的唯一定义.可以通过属性Matrix来存取matrix属性.并且平移,旋转,缩放更新后需要重新计算.
你可以使用xna的Matrix类CreateTranslate,CreateRotation,CreateScale方法来创建用于平移,旋转,缩放的矩阵.
注意物体世界变换矩阵是通过以缩放,旋转,平移的顺序相乘来计算的.因为矩阵乘法不满足交换律,乘法的顺序就变的很重要.
总结
本章,你场景了一个基础的框架来处理摄影机,光线和变换,这些都是游戏中很常见的对象.你学习了如何构建对象层次,将通用属性和方法放入父类,子类继承此父类来创建特殊的基类.使用此观念继承基础摄影机类创建第三人称摄影机及继承基础光线创建一个点光线,最后创建了一些管理器来管理场景中的摄影机和光线.