在上一章节中,我们介绍了渲染流水线,它的内容主要由两点,一个是世界变换,我们经常使用它来完成角色的移动和旋转。另一个就是摄像机,也就是取景变换和投影变换。接下来,我们来做一个可以简单移动的透视摄像机,其实本质就是可以根据键盘的输入来调整摄像机的位置和观察点。我们说简单移动,其实就是在世界坐标系的X,Y,Z轴的方向来调整摄像机的位置和观察点。其实,一个合理的摄像机应该可以自由的移动和旋转。我们创建新项目“D3D_04_ProjectionCamera”,同时复制之前的代码。在本案例中,我们就不绘制一个顶点立方体了,我们直接使用DirectX的API来绘制一个茶壶模型。我们在main.cpp文件中,只介绍重点代码,首先是对象的声明:
// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;
// 鼠标位置
int mx = 0, my = 0;
// 茶壶网格对象
LPD3DXMESH Teapot = NULL;
// 摄像机位置坐标
float eyeX = 0.0f, eyeY = 0.0f, eyeZ = -10.0f;
// 摄像机观察点坐标
float lookAtX = 0.0f, lookAtY = 0.0f, lookAtZ = 0.0f;
为了能够控制摄像的位置和观察点,我们需要将位置和观察点坐标声明为全局变量。然后修改initScene函数的内容,代码如下:
// 创建一个茶壶网格对象
D3DXCreateTeapot(D3DDevice, &Teapot, 0);
// 设置多边形填充模式,D3DFILL_POINT代表点模式,D3DFILL_WIREFRAME代表线模式,D3DFILL_SOLID代表面模式
D3DDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
// 初始化投影变换
initProjection();
然后,我们需要重构initProjection函数,其实就是调整取景变换矩阵,另外的投影变换矩阵和视口变换不变。代码如下:
// 设置取景变换矩阵
// 1.镜头X轴平移:改变摄像机(位置和观察点)的X轴即可
// 2.镜头Y轴拉高拉低:改变摄像机(位置和观察点)的Y轴即可
// 3.镜头Z轴拉近拉远缩放物体:改变摄像机的Z轴即可
// 4.镜头X轴旋转:改变观察点X轴即可
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(eyeX, eyeY, eyeZ); // 摄像机的位置
D3DXVECTOR3 viewLookAt(lookAtX, lookAtY, lookAtZ); // 观察点的位置
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f); // 向上的向量
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
上面简单介绍了投影变换矩阵的变换,也就是摄像机的变换。如果我们向要让场景沿X轴向右平移的话,我们只需要将摄像机的位置和观察点沿X轴向右移动即可。上下平移(拉高和拉低)也是同样的道理,只是在Y轴上修改。其他的改变也都是同理的。需要大家注意的是,我们的所有改变都是相对于世界坐标系的X/Y/Z轴而言的。一个自由移动的摄像机应该不能限制在世界坐标系的X/Y/Z轴。接下来我们介绍renderScene函数,这里我们主要是绘制茶壶,这个非常简单,一句代码就可以了,如下:
// 绘制茶壶
Teapot->DrawSubset(0);
需要注意的是,我们并没有使用世界变换矩阵。因为一般情况下,模型都是默认绘制在世界坐标系的原点。此时,如果我们运行代码,就能看到窗体中央位置展示一个线框构成的茶壶。本案例主要说明,使用键盘输入来控制取景变换。因此,我们需要完善我们的update函数了。假设我们使用“AD”来控制摄像机左右的平移,使用“WS”来放大和缩小场景,使用“QE”来设置摄像机的左右旋转。同时,我们只接收键盘按起事件,其他事件不处理。通过按下事件来修改摄像机位置和观察点坐标的数值,然后重新设置取景变换矩阵即可。代码如下:
// 我们只处理键盘按起事件
if (type != 2) return;
// 默认用户没有正确输入按键
bool input = false;
switch (wParam) {
case 'W':
// 向前移动(放大场景)
eyeZ += 1.0f;
input = true;
break;
case 'A':
// 向左移动
eyeX += 1.0f;
lookAtX += 1.0f;
input = true;
break;
case 'S':
// 向后移动(缩小场景)
eyeZ -= 1.0f;
input = true;
break;
case 'D':
// 向右移动
eyeX -= 1.0f;
lookAtX -= 1.0f;
input = true;
break;
case 'Q':
// 向左旋转
lookAtX -= 1.0f;
input = true;
break;
case 'E':
// 向右旋转
lookAtX += 1.0f;
input = true;
break;
}
// 调整取景变换矩阵
if (input) {
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(eyeX, eyeY, eyeZ);
D3DXVECTOR3 viewLookAt(lookAtX, lookAtY, lookAtZ);
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
}
运行代码,我们就可以使用“WASDQE”来控制摄像机的取景变换了。
最后我们来介绍平行投影,也就是正交摄像机。正交摄像机的取景范围不是一个正四棱锥,而是一个立方体。也正是这个原因,正交摄像机不会产生近大远小的效果。正交摄像机一般用于2D游戏开发。在2D游戏世界坐标系中,主要是x/y两个坐标轴,也对应了屏幕坐标系的x/y的两个维度轴。那么z轴对于2D游戏有什么意义吗?答案是,有的。Z轴可以决定2D图像的前后遮挡关系。也就是说,如果一个2D图像的Z轴值越小,它就越靠近正交摄像机,它就会遮挡住后面的2D图像。那么正交摄像机的投影立方体,其实就是对X/Y横截面(矩形)进行投影。在DirectX中使用D3DXMatrixOrthoLH来创建一个正交投影。该函数的主要参数就非常的简单,就是投影横截面(矩形)的宽和高。我们使用VS2019来创建一个新项目“D3D_04_OrthoCamera”,我们可以复制上面透视投影摄像机的代码来做调整。一般情况下,2D游戏都是由四边形构成的。因此,本案例将创建一个四边形。首先是全局变量的声明,代码如下:
// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;
// 鼠标位置
int mx = 0, my = 0;
// 定义FVF灵活顶点格式结构体
struct D3D_DATA_VERTEX { FLOAT x, y, z; DWORD color; };
// 世界坐标,窗体中央为坐标系原点,需要投影(正交/透视)
#define D3D_FVF_VERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE)
// 顶点缓冲区对象
LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer = NULL;
// 摄像机位置坐标
float eyeX = 0.0f, eyeY = 0.0f, eyeZ = -10.0f;
// 摄像机观察点坐标
float lookAtX = 0.0f, lookAtY = 0.0f, lookAtZ = 0.0f;
// 正交投影横截面矩形宽度和高度
float viewWidth = 0.0f, viewHeight = 0.0f;
接下来,我们来完成initScene函数内容,主要就是构建四边形顶点数据,然后调用投影变换函数,这个四边形的Z轴数值都是0,也就是做,我们在世界坐标系原点的XY平面上绘制一个四边形。2D游戏开发基本上都是这样,它只需要一个平面,不需要体积。代码如下:
// 四边形顶点数组
D3D_DATA_VERTEX vertexArray[] =
{
// 红色三角形V0V1V2,左下角顺时针
{ -10.0f, -10.0f, 0.0f, D3DCOLOR_XRGB(255, 0, 0) },
{ -10.0f, 10.0f, 0.0f, D3DCOLOR_XRGB(255, 0, 0) },
{ 10.0f, 10.0f, 0.0f, D3DCOLOR_XRGB(255, 0, 0) },
// 蓝色三角形V0V2V3,左下角顺时针
{ -10.0f, -10.0f, 0.0f, D3DCOLOR_XRGB(0, 0, 255) },
{ 10.0f, 10.0f, 0.0f, D3DCOLOR_XRGB(0, 0, 255) },
{ 10.0f, -10.0f, 0.0f, D3DCOLOR_XRGB(0, 0, 255) },
};
// 创建顶点缓冲区对象
D3DDevice->CreateVertexBuffer(sizeof(vertexArray), 0, D3D_FVF_VERTEX, D3DPOOL_DEFAULT, &D3DVertexBuffer, NULL);
// 填充顶点缓冲区对象
void* ptr;
D3DVertexBuffer->Lock(0, sizeof(vertexArray), (void**)&ptr, 0);
memcpy(ptr, vertexArray, sizeof(vertexArray));
D3DVertexBuffer->Unlock();
// 设置多边形填充模式,D3DFILL_POINT代表点模式,D3DFILL_WIREFRAME代表线模式,D3DFILL_SOLID代表面模式
D3DDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
// 初始化投影变换
initProjection();
接下来,就是完成initProjection函数,代码如下:
// 设置取景变换矩阵
// 1.镜头左右平移:改变摄像机位置和观察点X轴数值
// 2.镜头上下平移:改变摄像机位置和观察点Y轴数值
// 2D游戏中一般不需要旋转
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(eyeX, eyeY, eyeZ); // 摄像机的位置
D3DXVECTOR3 viewLookAt(lookAtX, lookAtY, lookAtZ); // 观察点的位置
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f); // 向上的向量
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
// 关闭光照,顶点颜色才会起作用
D3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
// 正交投影变换
// 镜头拉近放大物体对象, 可以通过缩小正交投影传入的viewWidth和viewHeight来实现放大;
// 增加viewWidth和viewHeight的数值来实现远离物体,使其变小的效果
D3DXMATRIX matProject;
viewWidth = WINDOW_WIDTH / 10;
viewHeight = WINDOW_HEIGHT / 10;
D3DXMatrixOrthoLH(
&matProject, // 输出的正交投影矩阵
viewWidth, // 投影横截面的宽度
viewHeight, // 投影横截面的高度
0, // Z深度缓存最小值
1000); // Z深度缓存最大值
D3DDevice->SetTransform(D3DTS_PROJECTION, &matProject);
// 设置视口变换
D3DVIEWPORT9 viewport = {
0, // 视口相对于窗口的X坐标,默认0即可
0, // 视口相对于窗口的Y坐标,默认0即可
WINDOW_WIDTH, // 视口的宽度
WINDOW_HEIGHT, // 视口的高度
0, // 视口在深度缓存中的最小深度值,默认0即可
1 // 视口在深度缓存中的最大深度值,默认1即可
};
D3DDevice->SetViewport(&viewport);
然后就是我们的renderScene函数,就是绘制一个四边形而已,同样,我们没有设置世界变换矩阵,让其默认在世界坐标系的原点,也就是窗体的中央位置。
// 绘制四边形
D3DDevice->SetStreamSource(0, D3DVertexBuffer, 0, sizeof(D3D_DATA_VERTEX));
D3DDevice->SetFVF(D3D_FVF_VERTEX);
D3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
在2D游戏中,我们一般只在X/Y轴方向上进行平移操作,也就是改变摄像机位置和观察点的X/Y方向上的值。另外,我们还可以修改正交投影横截面的矩形尺寸来缩放整个场景。接下来就是我们的update函数,代码如下:
// 我们只处理键盘按起事件
if (type != 2) return;
// 默认用户没有正确输入按键
bool input = false;
bool input2 = false;
switch (wParam) {
case 'W':
// 向上移动
eyeY -= 1.0f;
lookAtY -= 1.0f;
input = true;
break;
case 'A':
// 向左移动
eyeX += 1.0f;
lookAtX += 1.0f;
input = true;
break;
case 'S':
// 向下移动
eyeY += 1.0f;
lookAtY += 1.0f;
input = true;
break;
case 'D':
// 向右移动
eyeX -= 1.0f;
lookAtX -= 1.0f;
input = true;
break;
case 'Q':
// 放大场景
viewWidth -= 1.0f;
viewHeight -= 1.0f;
input2 = true;
break;
case 'E':
// 缩小场景
viewWidth += 1.0f;
viewHeight += 1.0f;
input2 = true;
break;
}
// 取景变换,定义一个视图矩阵
if (input) {
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(eyeX, eyeY, eyeZ);
D3DXVECTOR3 viewAt(lookAtX, lookAtY, lookAtZ);
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
}
// 正交投影
if (input2) {
D3DXMATRIX matProject;
D3DXMatrixOrthoLH(&matProject, viewWidth, viewHeight, 0, 1000);
D3DDevice->SetTransform(D3DTS_PROJECTION, &matProject);
}
上述代码就是通过“WASDQR”来完成上下左右的平移摄像机,以及整个场景的缩放。运行代码,按下“WSADQE”按键就能发现我们绘制的2D场景就能移动了。在2D游戏开发中,这种正交摄像机的移动其实已经够用了。如果需要正交摄像机跟随角色一起移动的话,就是在角色进行移动后,让正交摄像机也按照相同的方向移动相同的距离。
本课程的所有代码案例下载地址:
备注:这是我们游戏开发系列教程的第二个课程,这个课程主要使用C++语言和DirectX来讲解游戏开发中的一些基础理论知识。学习目标主要依理解理论知识为主,附带的C++代码能够看懂且运行成功即可,不要求我们使用DirectX来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!