D3DX9学习笔记之设计一个灵活的摄像机类

学习目标:
了解如何实现一个可用于飞行模拟器和第一人称视角游戏的灵活摄像机类

我们已经学会了如何用 D3DXMatrixLookAtLH 函数计算出观察矩阵(即取景变换矩阵),当在某一固定地点固定摄像机方向时,该函数十分有用,但其用户接口对于一个能够根据用户输入做出响应的移动摄像机来说,就显得力不从心,这就促使我们开发自己的解决方案,接下来介绍如何实现一个 Camera 类,以使我们能够设计出一个比 D3DXMatrixLookAtLH 函数更好用于控制摄像机的类,该Camera类特别适合用于飞行模拟器以及第一人称视角游戏

实现的大致原理就是使用世界变换将模型变换至射线机坐标系,这里使用左手坐标系,现在假设摄像机在世界坐标系的(0,0,-10),摄像机观察的点是(0,0,0),世界的上方向是(0,1,0)(这里可以忽略),在世界坐标系的原点位置(也就是0,0,0)有一个模型,我们现在想移动摄像机靠近模型,使模型看的更清楚,那也就是改变观察坐标系中摄像机的位置,使摄像机更靠近这个模型得以实现,另一种方法就是使用世界变换,使世界坐标系中的所有模型位移,摄像机不动,这样也实现了摄像机的灵活变动,这一思路在DXSDK文档中建议使用,SDK文档中建议使用单位矩阵的观察坐标系,将所需要的观察坐标系和世界变换合并成一个变换矩阵,这有利于提高应用程序的性能

理解了思路,那我们现在开始吧

我们用4个摄像机向量,右向量(right),上向量(up),观察方向向量(look),以及摄像机位置向量(position)来定义摄像机相对于世界坐标系的位置和朝向,这些向量实质上为相对世界坐标系描述的摄像机定义了一个局部坐标系,由于右向量,上向量和观察向量定义了摄像机在世界坐标系中的朝向,有时我们也将这三个向量统称为方向向量,方向向量必须是标准正交的,如果一个向量集合中的向量都彼此正交,且模均为1,则称该向量是标准正交的,引入这些约束的原因是在后面我们要将这些向量插入到一个矩阵的某些行中,以使该矩阵称为标准正交矩阵,标准正交矩阵的一个重要性质是其逆矩阵与其转置矩阵相等

用上诉4个向量来描述摄像机,我们就可以对摄像机实施如下6种变换:

  • 绕向量 right 的旋转(俯仰)
  • 绕向量 up 的旋转(偏航)
  • 绕向量 look 的旋转(滚动)
  • 沿向量 right 方向的扫视
  • 沿向量 up 方向的升降
  • 沿向量 look 的平动
    通过上诉6总运算,摄像机可沿3个轴平动以及绕3个轴转动,即摄像机具有6个自由度,下面的 Camera 类的定义反应了上诉描述变量和期望的方法
class Camera
{
public:
	enum CameraType {LANDOBJECT, AIRCRAFT};//摄像机模式,用于限制摄像机可以执行哪些操作

	Camera();//构造函数
	Camera(CameraType cameraType);
	~Camera() {};//析构函数

	void strafe(float units);	//左/右
	void fly(float units);		//上/下
	void walk(float units);		//前进/后退

	void pitch(float angle);	//在右矢量上旋转
	void yaw(float angle);		//向上旋转矢量
	void roll(float angle);		//按外观矢量旋转

	void getViewMatrix(D3DXMATRIX* V);			
	void setCameraType(CameraType cameraType);	
	void getPosition(D3DXVECTOR3* pos);			
	void setPosition(D3DXVECTOR3* pos);			
	void getRight(D3DXVECTOR3* right);			
	void getUp(D3DXVECTOR3* look);				

private:
	CameraType _cameraType;		//摄像机的类型
	D3DXVECTOR3 _right;			//右向量
	D3DXVECTOR3 _up;			//上向量
	D3DXVECTOR3 _look;			//观察方向向量(左手坐标系中的正Z方向)
	D3DXVECTOR3 _pos;			//摄像机的位置向量
};

实现细节

观察矩阵的计算(即取景变换矩阵)
给定摄像机向量时,如何计算取景变换矩阵,令向量 p,r,u,d分别表示position,right,up,look这4个向量。
取景变换所解决的问题其实就是世界坐标系中的物体在以摄像机为中心的坐标系中如何进行描述,等价于将世界坐标系中的物体随摄像机一起进行变换,以使摄像机坐标系与世界坐标系完全重合。

关于矩阵的推导就不细述了,下面列出代码的实现,从代码的实现中推导出矩阵的实现

void Camera::getViewMatrix(D3DXMATRIX* V)
{
	//保持摄像机轴相互垂直
	D3DXVec3Normalize(&_look, &_look);	//标准化观察向量,将向量除以它的大小即获得单位向量

	D3DXVec3Cross(&_up, &_look, &_right);//确定两个三维向量的叉积,叉乘得到的向量垂直于原来的两个向量
	D3DXVec3Normalize(&_up, &_up);		//标准化 上 向量

	D3DXVec3Cross(&_right, &_up, &_look);	//重置右向量
	D3DXVec3Normalize(&_right, &_right);	//标准化向量

	//构建视图矩阵
	float x = -D3DXVec3Dot(&_right, &_pos);	//确定两个三维向量的点乘(也称为内积),向量点乘就是对应分量乘积的和,其结果是一个标量
	float y = -D3DXVec3Dot(&_up, &_pos);	//确定两个三维矢量的点积。
	float z = -D3DXVec3Dot(&_look, &_pos);	//确定两个三维矢量的点积。

	(*V)(0, 0) = _right.x;
	(*V)(0, 1) = _up.x;
	(*V)(0, 2) = _look.x;
	(*V)(0, 3) = 0.0f;

	(*V)(1, 0) = _right.y;
	(*V)(1, 1) = _up.y;
	(*V)(1, 2) = _look.y;
	(*V)(1, 3) = 0.0f;

	(*V)(2, 0) = _right.z;
	(*V)(2, 1) = _up.z;
	(*V)(2, 2) = _look.z;
	(*V)(2, 3) = 0.0f;

	(*V)(3, 0) = x;
	(*V)(3, 1) = y;
	(*V)(3, 2) = z;
	(*V)(3, 3) = 1.0f;
}

前面的几行代码主要用于保证3个方向向量的正交,因为在几次旋转变换后,由于浮点数运算的误差,摄像机的各向量不可能再是标准正交的,所以每次调用该函数时,必须重新根据摄像机观察方向重新计算上方向和右方向以保证三者的相互正交,新的上方向正交向量可由前方向向量乘以右方向向量得到,即 up = look * right;新的右方向向量也是如此。

绕任意轴的旋转

实现摄像机的旋转方法时,我们应使得能够绕任意轴进行旋转,D3DX库提供了如下函数:

 D3DXMATRIX * D3DXMatrixRotationAxis(
  __inout  D3DXMATRIX *pOut,			//指向作为操作结果的D3DXMATRIX结构的指针
  __in     const D3DXVECTOR3 *pV,		//指向任意轴的指针
  __in     FLOAT Angle					//以弧度表示的旋转角度。当沿着旋转轴向原点看时,角度是顺时针方向测量的。
);

//例如,我们想绕由向量(0.707f, 0.707f, 0.0f)所确定的轴旋转1.57弧度(即90度),可以这样做:
D3DXMATRIX R;
D3DXVECTOR3 axis(0.707f, 0.707f, 0.0f);
D3DXMatrixRotationAxis(&R, &axis, 1.75f);

摄像机的动作

前进/后退:

void Camera::walk(float units)
{
	//仅在地面对象的xz平面上移动
	if (_cameraType == LANDOBJECT)
		_pos += D3DXVECTOR3(_look.x, 0.0f, _look.z) * units;

	if (_cameraType == AIRCRAFT)
		_pos += _look * units;
}

左/右:

void Camera::strafe(float units)
{
	//仅在地面对象的xz平面上移动
	if (_cameraType == LANDOBJECT)
		_pos += D3DXVECTOR3(_right.x, 0.0f, _right.z) * units;

	if (_cameraType == AIRCRAFT)
		_pos += _right * units;
}

上/下:

void Camera::fly(float units)
{
	//仅在Y轴上移动陆地物体
	if (_cameraType == LANDOBJECT)
		_pos.y += units;

	if (_cameraType == AIRCRAFT)
		_pos += _up * units;
}

以摄像机Y轴转动:

void Camera::yaw(float angle)
{
	D3DXMATRIX T;
	//始终为陆地对象绕世界y(0,1,0)旋转
	if (_cameraType == AIRCRAFT)
		D3DXMatrixRotationY(&T, angle);//建立一个围绕y轴旋转的矩阵。

	//飞机自转矢量
	if (_cameraType == AIRCRAFT)
		D3DXMatrixRotationAxis(&T, &_up, angle);//建立一个围绕任意轴旋转的矩阵。

	//左右旋转,向上旋转Y轴或Y轴
	D3DXVec3TransformCoord(&_right, &_right, &T);
	D3DXVec3TransformCoord(&_look,&_look,&T);
}

以摄像机的X轴转动:

void Camera::pitch(float angle)	//在右矢量上旋转
{
	D3DXMATRIX T;
	D3DXMatrixRotationAxis(&T, &_right, angle);	//建立一个围绕任意轴旋转的矩阵。

	//向上旋转并环视右向量
	D3DXVec3TransformCoord(&_up, &_up, &T);//通过给定的矩阵变换三维向量,将结果投影回w=1。
	D3DXVec3TransformCoord(&_look, &_look, &T);//通过给定的矩阵变换三维向量,将结果投影回w=1。
}

代码中实现:

	if (GetAsyncKeyState('W') & 0x8000f)
		TheCamera.walk(0.0005);
	if (GetAsyncKeyState('S') & 0x8000f)
		TheCamera.walk(-0.0005);
	if (GetAsyncKeyState('A') & 0x8000f)
		TheCamera.strafe(0.0005);
	if (GetAsyncKeyState('D') & 0x8000f)
		TheCamera.strafe(-0.0005);
	if (GetAsyncKeyState('F') & 0x8000f)
		TheCamera.fly(0.0005);
	if (GetAsyncKeyState('R') & 0x8000f)
		TheCamera.fly(-0.0005);
	if (GetAsyncKeyState(VK_UP) & 0x8000f)
		TheCamera.pitch(0.00005);
	if (GetAsyncKeyState(VK_DOWN) & 0x8000f)
		TheCamera.pitch(-0.00005);
	if (GetAsyncKeyState(VK_LEFT) & 0x8000f)
		TheCamera.yaw(0.00005);
	if (GetAsyncKeyState(VK_RIGHT) & 0x8000f)
		TheCamera.yaw(-0.00005);
	if (GetAsyncKeyState('N') & 0x8000f)
		TheCamera.roll(0.0005);
	if (GetAsyncKeyState('M') & 0x8000f)
		TheCamera.roll(-0.0005);
	D3DXMATRIX V;
	TheCamera.getViewMatrix(&V);
	device->SetTransform(D3DTS_VIEW, &V);

上诉的内容叙述的不够详细,有关取景变换矩阵的推导没有说明,理解会比较困难,主要是要明白实现的原理,代码中最后使用的是世界变换来实现摄像机的操作,而不是设置设备的取景变换矩阵,如果要明白代码的细节,需要细嚼慢咽,当然知道如何使用也是可以的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值