第十章 DirectX 绘制简单场景,地形,天空盒和跟随摄像机(下)

上个文章里面,我们分别控制胶囊体和摄像机确实比较麻烦,接下来,我将创建一个可以自由移动的模型对象,然后让摄像机跟随其移动和旋转。我们使用VS2019来新建一个项目“D3D_10_Follow”。首先,我们还是引入之前封装好的“InputClass.h”和“InputClass.cpp”两个文件。接下来,就是我们的自由移动摄像机类“CameraClass.h”和“CameraClass.cpp”文件。但是,这个文件需要做一点调整。首先是“CameraClass.h”代码如下:

#pragma once
#include "main.h"

// 跟随摄像机
class CameraClass {

public:

	D3DXVECTOR3 camera_pos;		// 摄像机位置
	D3DXVECTOR3 camera_target;	// 摄像机观察点

	D3DXVECTOR3 camera_up;		// 向上方向
	D3DXVECTOR3 camera_right;	// 向右方向
	D3DXVECTOR3 camera_look;	// 观察点方向

public:

	// 构造方法
	CameraClass();

	// 有参构造函数
	CameraClass(D3DXVECTOR3 target, float distance);

	// 析构方法
	~CameraClass(){};

	// 前后移动
	void frontBackMove(float step, D3DXVECTOR3 front);

	// 左右移动
	void leftRightMove(float step);

	// 上下移动
	void upDownMove(float step);

	// 围绕 _up 方向旋转,也就是左右旋转(摇头)
	void leftRightRotate(float angle);

	// 围绕 _right 方向旋转,也就是上下旋转(点头)
	void upDownRotate(float angle);

	// 围绕 _look 方向旋转,相当于侧头
	void sideRotate(float angle);

	// 计算取景变换矩阵
	void getViewMatrix(D3DXMATRIX* V);

};

变化有三点。第一,增加了摄像机观察点。第二,构造函数的参数是观察点和间距。第三,就是前后移动方法的参数增加了front向量,也就是说,摄像机前后移动不是根据观察点的方向,而是跟随目标的前方向。接下来就是实现“CameraClass.cpp”的内容:

#include "CameraClass.h"

// 跟随摄像机: 默认构造方法
CameraClass::CameraClass() {

	camera_pos = { 0.0f, 0.0f, -10.0f };
	camera_target = { 0.0f, 0.0f, 0.0f };

	camera_up = { 0.0f, 1.0f, 0.0f };
	camera_right = { 1.0f, 0.0f, 0.0f };
	camera_look = { 0.0f, 0.0f, 1.0f };
}

// 跟随摄像机:有参构造函数
CameraClass::CameraClass(D3DXVECTOR3 target, float distance) {

	// 设置摄像机目标点
	camera_target.x = target.x;
	camera_target.y = target.y;
	camera_target.z = target.z;

	// 设置摄像机位置
	camera_pos.x = target.x;
	camera_pos.y = target.y + distance;
	camera_pos.z = target.z - distance;

	// 初始化向量
	camera_up = { 0.0f, 1.0f, 0.0f };
	camera_right = { 1.0f, 0.0f, 0.0f };

	// 观察点位置减摄像机位置,得到观察方向向量
	camera_look = camera_target - camera_pos;

	// 规范化 camera_look 向量
	D3DXVec3Normalize(&camera_look, &camera_look);
};

// 跟随摄像机: 前后移动
void CameraClass::frontBackMove(float step, D3DXVECTOR3 front) {

	// 注意:摄像机按照跟随模型的 front 方向移动
	camera_pos += front * step;
	camera_target += front * step;

	// 摄像机不跟随的话,就按照自己的 camera_look 方向移动
	//camera_pos += camera_look * step;
	//camera_target += camera_look * step;
}

// 跟随摄像机: 左右移动
void CameraClass::leftRightMove(float step) {

	camera_pos += camera_right * step;
	camera_target += camera_right * step;
}

// 跟随摄像机: 上下移动
void CameraClass::upDownMove(float step) {

	camera_pos.y += step;
	camera_target.y += step;
}

// 跟随摄像机: 围绕 _up 方向旋转,也就是左右旋转(摇头)
void CameraClass::leftRightRotate(float angle) {

	// 创建一个绕 Y 轴向上方向旋转 angle 角度的 R 矩阵
	D3DXMATRIX rotationY;
	D3DXVECTOR3 Up = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
	D3DXMatrixRotationAxis(&rotationY, &Up, angle);

	// 让 camera_right 向量和 camera_look 向量绕 Up 方向旋转 angle 个角度
	D3DXVec3TransformCoord(&camera_right, &camera_right, &rotationY);
	D3DXVec3TransformCoord(&camera_look, &camera_look, &rotationY);

	// 更新一下观察点,摄像机跟随模型旋转的话,观察点是不变的,变的是摄像机的位置
	//camera_target = camera_look * D3DXVec3Length(&camera_pos);

	// camera_look 相反向量
	D3DXVECTOR3 look = camera_pos - camera_target;
	// 让 camera_look 相反向量围绕 Up 方向旋转 angle 个角度
	D3DXVECTOR3 temp;
	D3DXVec3TransformCoord(&temp, &look, &rotationY);
	// 更新摄像机位置
	camera_pos = camera_target + temp;
}

// 跟随摄像机: 围绕 _right 方向旋转,也就是上下旋转(点头)
void CameraClass::upDownRotate(float angle) {

	D3DXMATRIX M;
	D3DXMatrixRotationAxis(&M, &camera_right, angle);
	D3DXVec3TransformCoord(&camera_up, &camera_up, &M);
	D3DXVec3TransformCoord(&camera_look, &camera_look, &M);

	// 这里应该同步修改摄像机观察点,这样会让观察点偏离跟随模型,最好不能这么做
	// 因此,我们还是要调整摄像机的位置
	//camera_target = camera_look * D3DXVec3Length(&camera_pos);

	// camera_look 相反向量
	D3DXVECTOR3 look = camera_pos - camera_target;
	// 让 camera_look 相反向量围绕 Up 方向旋转 angle 个角度
	D3DXVECTOR3 temp;
	D3DXVec3TransformCoord(&temp, &look, &M);
	// 更新摄像机位置
	camera_pos = camera_target + temp;
}

// 跟随摄像机: 围绕 _look 方向旋转,相当于侧头(歪头)
void CameraClass::sideRotate(float angle) {

	D3DXMATRIX M;
	D3DXMatrixRotationAxis(&M, &camera_look, angle);
	D3DXVec3TransformCoord(&camera_right, &camera_right, &M);
	D3DXVec3TransformCoord(&camera_up, &camera_up, &M);
}

// 跟随摄像机: 计算取景变换矩阵
void CameraClass::getViewMatrix(D3DXMATRIX* V) {

	// 三个方向向量正交单位化
	D3DXVec3Normalize(&camera_look, &camera_look);
	D3DXVec3Cross(&camera_up, &camera_look, &camera_right);
	D3DXVec3Normalize(&camera_up, &camera_up);
	D3DXVec3Cross(&camera_right, &camera_up, &camera_look);
	D3DXVec3Normalize(&camera_right, &camera_right);

	// 取景变换
	float x = -D3DXVec3Dot(&camera_right, &camera_pos);
	float y = -D3DXVec3Dot(&camera_up, &camera_pos);
	float z = -D3DXVec3Dot(&camera_look, &camera_pos);

	// 取景变换矩阵
	(*V)(0, 0) = camera_right.x; (*V)(0, 1) = camera_up.x; (*V)(0, 2) = camera_look.x; (*V)(0, 3) = 0.0f;
	(*V)(1, 0) = camera_right.y; (*V)(1, 1) = camera_up.y; (*V)(1, 2) = camera_look.y; (*V)(1, 3) = 0.0f;
	(*V)(2, 0) = camera_right.z; (*V)(2, 1) = camera_up.z; (*V)(2, 2) = camera_look.z; (*V)(2, 3) = 0.0f;
	(*V)(3, 0) = x;	             (*V)(3, 1) = y;	       (*V)(3, 2) = z;	                
    (*V)(3, 3) = 1.0f;
}

在这个文件中,代码还是比较复杂的。在构造函数中,我们确定了目标点,也就是观察点,也是跟随的目标。然后让摄像机位于目标点的后上方。有了观察点和位置点,就能计算观察方向向量了。其他两个向量也给了默认值。移动操作基本就是让位置和观察点一起变动。旋转的有些复杂了。例如左右旋转,我们是让摄像机沿着世界坐标系Y轴进行旋转,同时调整摄像机的位置坐标。大家可以实景想想一下,当我们角色左右旋转的时候,摄像机应该如何变动才能让我们的观察的范围同步。其余的方法不在介绍。取景变换方法没有变动。

完成了跟随摄像机的封装,我们开始对玩家进行封装,首先玩家是一个模型,因此我们需要将之前封装好的“MeshClass.h”和“MeshClass.cpp”两个文件复制过来。完成这个基础类的封装后,我们就可以继承MeshClass来完成自由移动的玩家类了,首先我们创建“PlayerClass.h”和“PlayerClass.cpp”两个文件,首先是“PlayerClass.h”文件内容:

#pragma once
#include "MeshClass.h"
#include "CameraClass.h"

// 玩家类,继承 MeshClass 类
class PlayerClass : public MeshClass {

public:

	D3DXVECTOR3 _pos;		// 玩家位置
	D3DXVECTOR3 _up;		// 向上的方向
	D3DXVECTOR3 _right;		// 向右的方向
	D3DXVECTOR3 _front;		// 向前的方向

	CameraClass* _camera;	// 跟随的摄像机

public:

	// 构造方法
	PlayerClass();
	PlayerClass(LPDIRECT3DDEVICE9 device, const wchar_t* dir, const wchar_t* file, D3DXVECTOR3 pos);

	// 析构方法
	~PlayerClass() {}

	// 设置摄像机类
	void setCamera(CameraClass* camera);

	// 计算世界变换矩阵
	void getWorldMatrix(D3DXMATRIX* V);

	/******************************************************/

	// 左右移动(_right轴方向)
	void move(float step);

	// 前进后退(_front轴方向)
	void forward(float step);

	// 上下飞行(_up轴方向)
	void fly(float step);

	/******************************************************/

	// 摇头,左右旋转(_up轴旋转)
	void shake(float angle);

	// 点头,上下旋转(_right轴旋转)
	void nod(float angle);

	// 歪头,侧歪旋转(_front轴旋转)
	void roll(float angle);

};

其实这个类的实现,和自由移动的摄像机非常相似。我们声明三个方向向量以及位置信息。然后围绕三个方向做移动和旋转的操作。只不过,最后我们要生成的是世界变换矩阵,而不是摄像机的取景变换矩阵,两者是不一样的。接下来就是实现“PlayerClass.cpp”的内容:

#include "PlayerClass.h"

// 玩家类:构造方法
PlayerClass::PlayerClass() {};

// 玩家类:有参构造方法 
PlayerClass::PlayerClass(LPDIRECT3DDEVICE9 device, const wchar_t* dir, const wchar_t* file, D3DXVECTOR3 pos) : MeshClass(device, dir, file) {

	_pos = pos;
	_up = { 0.0f, 1.0f, 0.0f };
	_right = { 1.0f, 0.0f, 0.0f };
	_front = { 0.0f, 0.0f, 1.0f };
};

// 玩家类:设置摄像机类
void PlayerClass::setCamera(CameraClass* camera) {

	_camera = camera;
};

// 玩家类:计算世界变换矩阵
void PlayerClass::getWorldMatrix(D3DXMATRIX* V) {

	// _front, _up, _right正交单位化
	D3DXVec3Normalize(&_front, &_front);
	D3DXVec3Cross(&_up, &_front, &_right);
	D3DXVec3Normalize(&_up, &_up);
	D3DXVec3Cross(&_right, &_up, &_front);
	D3DXVec3Normalize(&_right, &_right);

	// 世界矩阵
	(*V)(0, 0) = _right.x;	(*V)(0, 1) = _right.y;	(*V)(0, 2) = _right.z;	(*V)(0, 3) = 0.0f;
	(*V)(1, 0) = _up.x;	    (*V)(1, 1) = _up.y;	    (*V)(1, 2) = _up.z;	    (*V)(1, 3) = 0.0f;
	(*V)(2, 0) = _front.x;	(*V)(2, 1) = _front.y;	(*V)(2, 2) = _front.z;	(*V)(2, 3) = 0.0f;
	(*V)(3, 0) = _pos.x;	(*V)(3, 1) = _pos.y;	(*V)(3, 2) = _pos.z;	(*V)(3, 3) = 1.0f;
};

// 玩家类:左右移动(_right轴方向)
void PlayerClass::move(float step) {

	_pos += _right * step;
	if (_camera != NULL) _camera->leftRightMove(step);
};

// 玩家类:前进后退(_front轴方向)
void PlayerClass::forward(float step) {

	_pos += _front * step;
	if (_camera != NULL) _camera->frontBackMove(step, _front);
};

// 玩家类:上下飞行(_up轴方向)
void PlayerClass::fly(float step) {

	_pos.y += step;
	if (_camera != NULL) _camera->upDownMove(step);
};

// 玩家类:摇头,左右旋转(_up轴旋转)
void PlayerClass::shake(float angle) {

	D3DXMATRIX T;
	D3DXMatrixRotationAxis(&T, &_up, angle);
	D3DXVec3TransformCoord(&_right, &_right, &T);
	D3DXVec3TransformCoord(&_front, &_front, &T);
	if (_camera != NULL) _camera->leftRightRotate(angle);
};

// 玩家类:点头,上下旋转(_right轴旋转),不建议使用此方法
void PlayerClass::nod(float angle) {

	D3DXMATRIX T;
	D3DXMatrixRotationAxis(&T, &_right, angle);
	D3DXVec3TransformCoord(&_up, &_up, &T);
	D3DXVec3TransformCoord(&_front, &_front, &T);
	if (_camera != NULL) _camera->upDownRotate(angle);
};

// 玩家类:歪头,侧歪旋转(_front轴旋转),不建议使用此方法
void PlayerClass::roll(float angle) {

	D3DXMATRIX T;
	D3DXMatrixRotationAxis(&T, &_front, angle);
	D3DXVec3TransformCoord(&_right, &_right, &T);
	D3DXVec3TransformCoord(&_up, &_up, &T);
	if (_camera != NULL) _camera->sideRotate(angle);
};

由于完成过自由摄像机的封装,因此这个玩家类就比较容易理解。复杂的在于玩家的移动和旋转后,要同步跟随摄像机的移动和旋转。关于如何计算世界变换矩阵,也不需要大家理解。接下来,就是我们的主文件“main.cpp”的内容,首先是全局变量的声明:

// 引入头文件
#include "main.h"
#include "MeshClass.h"
#include "InputClass.h"
#include "CameraClass.h"
#include "PlayerClass.h"

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;

// 输入类
InputClass* input;

// 摄像机类
CameraClass* camera;

// 玩家类(立方体)
PlayerClass* player;

// 草地模型 
MeshClass* land;

然后就是我们的initScene函数,代码如下:

// 初始化输入
input = new InputClass(D3DDevice, hwnd, hInstance);

// 初始化草地模型
land = new MeshClass(D3DDevice, L"asset/", L"bg.X");
land->init();

// 初始化玩家模型
D3DXVECTOR3 pos(0.0f, 0.0f, 0.0f);
player = new PlayerClass(D3DDevice, L"asset/", L"cube.X", pos);
player->init();

// 初始化摄像机
camera = new CameraClass(pos, 1.0f);

// 摄像机给到玩家
player->setCamera(camera);

// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

// 初始化投影变换
initProjection();

// 初始化光照
initLight();

需要注意的是,这里的草地模型,并不是我们之前创建的地形类,它只是一个没有高度的平面草地而已。initProjection函数和initLight函数还是按照上个项目中代码不变。接下来,就是renderScene函数,代码如下:

// 绘制草地
D3DXMATRIX matrix;
D3DXMatrixTranslation(&matrix, 0.0f, 0.0f, 0.0f);
D3DDevice->SetTransform(D3DTS_WORLD, &matrix);
land->render();

// 绘制玩家
D3DXMATRIX worldMatrix;
player->getWorldMatrix(&worldMatrix);
D3DDevice->SetTransform(D3DTS_WORLD, &worldMatrix);
player->render();

// 调整取景变换矩阵
D3DXMATRIX viewMatrix;
camera->getViewMatrix(&viewMatrix);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);

运行代码效果如下:

接下来就是我们的update方法,我们使用“WASD”来左右前后平移立方体模型,使用“QE”来左右旋转,使用“CV”来上下平移模型。代码如下:

// 获取键盘信息
input->update();
char key = input->key[0];

// 按键判断
switch (key) {

	case 'A':
		// 向左移动
		player->move(-0.02f);
		break;

	case 'D':
		// 向右移动
		player->move(0.02f);
		break;

	case 'W':
		// 向前移动
		player->forward(0.02f);
		break;

	case 'S':
		// 向后移动
		player->forward(-0.02f);
		break;

	case 'Q':
		// 向左旋转
		player->shake(-0.02f);
		break;

	case 'E':
		// 向右旋转
		player->shake(0.02f);
		break;

	case 'C':
		// 向上移动
		player->fly(0.02f);
		break;

	case 'V':
		// 向下移动
		player->fly(-0.02f);
		break;
}

运行后效果如下:

最后,我们要讲一个天空盒。它的实现非常简单,它就是一个缺少底部的立方体而已。说白了,就是使用它来模拟周围的天空。我们使用VS2019新建项目“D3D_10_Skybox”。首先我们创建“SkyboxClass.h”和“SkyboxClass.cpp”两个文件,其中“SkyboxClass.h”文件内容如下:

#pragma once
#include "main.h"

// 为天空盒类定义一个FVF灵活顶点格式
struct D3D_SKYBOX_DATA_VERTEX { float x, y, z, u, v; };

// 定义带纹理的顶点类型
#define D3D_SKYBOX_FVF_VERTEX D3DFVF_XYZ | D3DFVF_TEX1

// 天空盒类
class SkyBoxClass {

private:

	LPDIRECT3DDEVICE9 D3DDevice;			// D3D设备对象
	LPDIRECT3DVERTEXBUFFER9	vertexBuffer;	// 顶点缓存对象
	LPDIRECT3DTEXTURE9 textureArray[5];		// 5个纹理贴图
	float length;							// 天空盒边长

public:

	// 构造函数
	SkyBoxClass(LPDIRECT3DDEVICE9 device, float len);

	// 析构函数
	~SkyBoxClass();

	// 初始化天空盒
	void init();

	// 加载天空盒纹理贴图
	void load(const wchar_t* front, const wchar_t* back, const wchar_t* left, const wchar_t* right, const wchar_t* top);

	// 渲染天空盒
	void render();

};

天空盒的实现原理还是四边形纹理,我们需要构建一个前后左右,有上无下的5个四边形纹理。“SkyboxClass.cpp”如下:

#include "SkyBoxClass.h"

// 天空盒类: 构造函数
SkyBoxClass::SkyBoxClass(LPDIRECT3DDEVICE9 device, float len) {

	length = len;
	D3DDevice = device;
	vertexBuffer = NULL;
	for (int i = 0; i < 5; i++) textureArray[i] = NULL;
}

// 天空盒类: 初始化天空盒
void SkyBoxClass::init() {

	// 创建顶点缓存对象,20个顶点数据大小
	D3DDevice->CreateVertexBuffer(20 * sizeof(D3D_SKYBOX_DATA_VERTEX), 0, D3D_SKYBOX_FVF_VERTEX, D3DPOOL_DEFAULT, &vertexBuffer, 0);

	// 顶点数组数据
	D3D_SKYBOX_DATA_VERTEX vertices[] = {

		// 前面的四个顶点
		{ -length / 2, 0.0f,		 length / 2, 0.0f, 1.0f, },
		{ -length / 2, length / 2,   length / 2, 0.0f, 0.0f, },
		{  length / 2, 0.0f,		 length / 2, 1.0f, 1.0f, },
		{  length / 2, length / 2,   length / 2, 1.0f, 0.0f, },

		// 背面的四个顶点
		{  length / 2, 0.0f,		-length / 2, 0.0f, 1.0f, },
		{  length / 2, length / 2,  -length / 2, 0.0f, 0.0f, },
		{ -length / 2, 0.0f,		-length / 2, 1.0f, 1.0f, },
		{ -length / 2, length / 2,  -length / 2, 1.0f, 0.0f, },

		// 左面的四个顶点
		{ -length / 2, 0.0f,		-length / 2, 0.0f, 1.0f, },
		{ -length / 2, length / 2,  -length / 2, 0.0f, 0.0f, },
		{ -length / 2, 0.0f,		 length / 2, 1.0f, 1.0f, },
		{ -length / 2, length / 2,   length / 2, 1.0f, 0.0f, },

		// 右面的四个顶点
		{ length / 2, 0.0f,			 length / 2, 0.0f, 1.0f, },
		{ length / 2, length / 2,	 length / 2, 0.0f, 0.0f, },
		{ length / 2, 0.0f,			-length / 2, 1.0f, 1.0f, },
		{ length / 2, length / 2,	-length / 2, 1.0f, 0.0f, },

		// 上面的四个顶点
		{  length / 2, length / 2,	-length / 2, 1.0f, 0.0f, },
		{  length / 2, length / 2,   length / 2, 1.0f, 1.0f, },
		{ -length / 2, length / 2,	-length / 2, 0.0f, 0.0f, },
		{ -length / 2, length / 2,	 length / 2, 0.0f, 1.0f, },
	};

	// 填充顶点数据
	void* ptr;
	vertexBuffer->Lock(0, 0, (void**)&ptr, 0);
	memcpy(ptr, vertices, sizeof(vertices));
	vertexBuffer->Unlock();
}

// 天空盒类: 加载天空盒纹理贴图
void SkyBoxClass::load(const wchar_t* front, const wchar_t* back, const wchar_t* left, const wchar_t* right, const wchar_t* top) {

	D3DXCreateTextureFromFile(D3DDevice, front, &textureArray[0]); // 前面
	D3DXCreateTextureFromFile(D3DDevice, back, &textureArray[1]);  // 后面
	D3DXCreateTextureFromFile(D3DDevice, left, &textureArray[2]);  // 左面
	D3DXCreateTextureFromFile(D3DDevice, right, &textureArray[3]); // 右面
	D3DXCreateTextureFromFile(D3DDevice, top, &textureArray[4]);   // 上面	
}

// 天空盒类: 渲染天空盒
void SkyBoxClass::render() {

	// 将纹理颜色混合的第一个参数的颜色值用于输出
	D3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
	// 纹理颜色混合的第一个参数的值就取纹理颜色值
	D3DDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);

	// 绘制5个四边形
	D3DDevice->SetStreamSource(0, vertexBuffer, 0, sizeof(D3D_SKYBOX_DATA_VERTEX));
	D3DDevice->SetFVF(D3D_SKYBOX_FVF_VERTEX);
	for (int i = 0; i < 5; i++) {
		D3DDevice->SetTexture(0, textureArray[i]);
		D3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, i * 4, 2);
	}
}

// 天空盒类: 析构函数
SkyBoxClass::~SkyBoxClass() {

	SAFE_RELEASE(vertexBuffer);
	for (int i = 0; i < 5; i++) SAFE_RELEASE(textureArray[i]);
}

这个四边形纹理的绘制方式与我们之前不太一样。它使用的是D3DPT_TRIANGLESTRIP方式,也就是使用连续的四个顶点来绘制一个四边形,其实本质也是两个三角形组成一个四边形,只不过三角形的顶点顺序不同而已。天空盒封装完毕之后,我们就开始创建InputClass封装类,直接复制我们之前的代码即可。然后即使跟随摄像机类CameraClass,直接复制我们之前的代码即可。这里需要注意一点,由于我们使用3D人物动画模型的原因,在CameraClass的构造函数中,初始化方向向量的时候,要反转一下。

// 初始化向量
camera_up = { 0.0f, -1.0f, 0.0f };
camera_right = { -1.0f, 0.0f, 0.0f };

这样做的原因是3D人物动画模型制作完导出后的朝向问题,大家可以实景想想一下。如果你的人物模型的正面是Z轴正方向或是负方向的话,肯定是不一样的。这样导致你跟随摄像机的方向要调整。当然,这个不是重点。接下来就是MeshClass类的引入,还是之前的旧代码。然后就是我们之前封装的地形类TerrainClass的引入。然后就是我们关于骨骼动画的CAllocateHierarchySkinMesh的引入。最后即使我们的玩家类,它需要继承SkinMesh,然后让摄像机跟随移动。这个父类的改变,其实对PlayerClass的影响不是很大。以上所有的类都构建完毕后,就是主文件“main.cpp”的内容,首先是全局变量的声明:

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;

// 天空盒
SkyBoxClass* skybox;

// 草地模型(地形)
TerrainClass* land;

// 输入类
InputClass* input;

// 摄像机类
CameraClass* camera;

// 玩家类
float step;
float height;
PlayerClass* player;

然后就是我们的initScene函数,代码如下:

// 初始化天空盒
skybox = new SkyBoxClass(D3DDevice, 10000.0f);
skybox->init();
skybox->load(L"asset/front.jpg", L"asset/back.jpg", L"asset/left.jpg", L"asset/right.jpg", L"asset/top.jpg");

// 草地模型边长为10000,分段为100,因此每两个顶点间隔就是100
float interval = 100.0f;

// 初始化草地模型
land = new TerrainClass(D3DDevice, L"asset/", L"ground.X", interval);

// 初始化输入
input = new InputClass(D3DDevice, hwnd, hInstance);

// 初始化玩家模型
step = 5.0f;
height = 250.0f;
D3DXVECTOR3 pos(3000.0f, height, 3000.0f);
player = new PlayerClass(D3DDevice, L"tiny/", L"tiny.x", pos);
player->isPlay = false;
player->startTime = (float)timeGetTime();

// 初始化摄像机
camera = new CameraClass(pos, 1000.0f);

// 摄像机给到玩家
player->setCamera(camera);

// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

// 初始化投影变换
initProjection();

// 初始化光照
initLight();


接下来就是renderScene函数,代码如下:

// 渲染天空盒
D3DXMATRIX matrix;
D3DXMatrixTranslation(&matrix, 0.0f, 0.0f, 0.0f);
D3DDevice->SetTransform(D3DTS_WORLD, &matrix);
skybox->render();

// 绘制草地模型
land->render();

// 绘制玩家
D3DXMATRIX worldMatrix;
player->getWorldMatrix(&worldMatrix);
player->render(worldMatrix);

// 调整取景变换矩阵
D3DXMATRIX viewMatrix;
camera->getViewMatrix(&viewMatrix);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);

我们先运行效果:

接下来,就是我们的update函数,我们使用键盘操控模型移动且播放动画。代码如下:

// 获取键盘信息
input->update();
char key = input->key[0];

// 按键判断
switch (key) {

	case 'W':
		// 向前移动
		player->forward(-step);
		// 根据地形高度设置模型y轴位置
		player->_pos.y = land->getTerrainHeight(player->_pos.x, player->_pos.z) + height;
		// 播放动画
		player->isPlay = true;
		break;

	case 'S':
		// 向后移动
		player->forward(step);
		// 根据地形高度设置模型y轴位置
		player->_pos.y = land->getTerrainHeight(player->_pos.x, player->_pos.z) + height;
		// 播放动画
		player->isPlay = true;
		break;

	case 'Q':
		// 向左旋转
		player->shake(-0.02f);
		break;

	case 'E':
		// 向右旋转
		player->shake(0.02f);
		break;

	case 'F':
		// 向上旋转
		player->nod(0.02f);
		break;

	case 'G':
		// 向下旋转
		player->nod(-0.02f);
		break;

	default:
		// 停止动画
		player->isPlay = false;
		break;
}

运行代码,我们使用“W”向前移动模型,使用“QE”来旋转模型,移动过程中伴随动画。

可以看到,我们的地形也起作用了。到这里,我们关于DirectX的基础课程就讲解全部完毕了。该课程主要是让大家了解游戏开发中的一些基础理论知识。如果能够看懂附带的DirectX项目源码,则能够加深对这些理论知识的掌握。

本课程的所有代码案例下载地址:

workspace.zip

备注:这是我们游戏开发系列教程的第二个课程,这个课程主要使用C++语言和DirectX来讲解游戏开发中的一些基础理论知识。学习目标主要依理解理论知识为主,附带的C++代码能够看懂且运行成功即可,不要求我们使用DirectX来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
DirectX天空盒子是一种在计算机图形中模拟真实环境的技术。天空盒子通常由一个立方体纹理组成,其六个面分别表示天空的不同方向,如上、下、前、后、左、右。在渲染过程中,天空盒子会随着摄像机的移动而跟随,给观察者呈现出一个具有深度感的天空。 为了实现天空盒子的效果,需要使用DirectX的着色器编程来对顶点和像素进行处理。首先,通过加载立方体纹理,并将其作为天空盒子的纹理贴图。然后,在渲染场景之前,将天空盒子设置为场景的背景,以便在后续的渲染过程中将其显示在屏幕上。 相机是在计算机图形中模拟实际摄像机的设备。它决定了我们从哪个角度观察场景,并控制了场景中物体的可见性。相机通常由位置、视角和方向组成。 在DirectX中,要使用相机,首先需要定义一个观察矩阵。观察矩阵是一个4x4矩阵,通过组合相机的位置、视角和方向来定义。然后,将观察矩阵传递给着色器,以便在渲染期间将场景中的顶点转换为相机坐标空间。 在渲染过程中,相机的位置可以随着用户的控制而变化,从而实现不同角度的观察。这样,用户可以通过改变相机的位置和方向来实现不同的视角,从而呈现出不同的场景效果。 综上所述,DirectX天空盒子和相机都是用于实现在计算机图形中模拟真实环境的技术。天空盒子可以通过使用立方体纹理来模拟真实的天空,而相机则用于控制观察者的视角和可见性。这两个技术的结合使得我们能够在计算机图形中创造出逼真的场景

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咆哮的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值