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

本章节,我们将要创建一个简单的创建,其中包括地形,天空盒以及跟随摄像机三种对象类。

首先我们使用3ds max创建一个地形:

第一步,在3ds max中创建一个平面,长度和宽度都是10000,长宽分段都是100。这样,我们就会创建一个拥有10000个正四边形组成的大平面(大正四边形)了。注意,每个正四边形的边长也是100。这跟我们在DirectX中使用灵活顶点构造一个超大四边形,其实是一个原理。很显然,使用3ds max制作要比DirectX简单容易多了。虽然我们在3ds max中看到整个大的平面是由10000个正四边形构成的,但是在DirectX中绘制这个平面的时候,每个四边形其实还是由两个三角形组成的。游戏引擎中最基本的图元还是三角形。需要注意的是,我们要将平面的中心点移动到世界坐标系的原点,具体操作就是选中平面,按W键(移动工具)后,将下面的x/y/z值改成0即可。

为了方便查看平面的样子,我们可以滚动鼠标中间的滚轮,来放大/缩小视图,这样可以更大范围的看到整个平面。同时也可以按下鼠标中间的滚轮拖动,来移动视图,使用更好的角度来观察整个平面。这是3ds max最基本的视图操作。

第二步,将我们提前准备好的一张草地的纹理贴图,直接拖拽到平面上。这个简便的操作实际就是给平面施加一个纹理材质,效果如下:

第三步,给草地做出高度,将平面转换成“可编辑多边形”,然后选择“面”的操作级别,这样,我们就能选中平面中的任意一个四边形了,按住Ctrl键可以帮我们加选四边形。

 

 

选中几个四边形,使用移动工具(W),将其沿Z轴向上拖动,使其选中的四边形向上移动。这里,我们选中的是世界坐标系原点附近的四边形。使用Ctrl键可以加选多个四边形面。

 

当然,我们也可以直接修改右下角的Z值,来明确向上移动的距离数值,这里我们设置为100

第四步,使用软选择工具,给草地做出平滑的高度。

我们将平面的编辑模式改成顶点,而不是之前的模式,这样,我们选中的则是平面的顶点,而不是之前的四边形。

在“顶点”选择模式的下方,由一个“软选择”工具,勾选“使用软选择”,然后设置“衰减”值,这个数值越大,影响的顶点范围就越广。此时,你点击平面上的一点,就会发下效果如下:

此时,我们同样使用移动工具(W键)来移动我们选中的顶点,就会发现附近的顶点会跟着一起移动,这样就会形成一个平滑的坡度。

其实,我们可以从顶点颜色能够推算出来,红色区域的顶点,移动距离大,橙色顶点小一些,绿色会再小一些,蓝色基本上影响不大了。在3ds max中使用颜色来区分影响程度的方式,在蒙皮的时候也会用到。当然,我们可以继续选择其他顶点,继续拖拽形成高低起伏的效果。

第五步,将我们的模型导出为X格式即可。

默认情况下,3ds max不支持导出X格式模型,我们需要借助Axe_free 插件来完成。这个插件是免费的,根据你的3ds max版本下载对应的Axe_free版本即可。解压Axe_free之后,直接放到 3ds max安装目录下的plugins下即可,非常简单。

接下来我们使用VS2019来创建新项目“D3D_10_Terrain”,首先创建公共头文件“main.h”文件,该文件中包含了我们以往用到的所有头文件,以及释放对象的宏,代码如下:

#pragma once
#include <windows.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <dinput.h> 
#include <time.h>
#include <math.h>
#include <iostream>
#include <fstream>
#include <string>

// 引入依赖的库文件
#pragma comment(lib,"d3d9.lib")
#pragma comment(lib,"d3dx9.lib")
#pragma comment(lib,"dinput8.lib") 
#pragma comment(lib,"dxguid.lib")
#pragma comment(lib,"winmm.lib")

#define WINDOW_LEFT		200				// 窗口位置
#define WINDOW_TOP		100				// 窗口位置
#define WINDOW_WIDTH	800				// 窗口宽度
#define WINDOW_HEIGHT	600				// 窗口高度
#define WINDOW_TITLE	L"D3D游戏开发"	// 窗口标题
#define CLASS_NAME		L"D3D游戏开发"	// 窗口类名

// 自定义一个SAFE_RELEASE()宏, 释放指针对象
#ifndef SAFE_DELETE
#define SAFE_DELETE(p){ if (p) { delete (p); (p)=NULL; } }
#endif

// 自定义一个SAFE_RELEASE()宏, 释放COM对象
#ifndef SAFE_DELETE_ARRAY
#define SAFE_DELETE_ARRAY(p){ if (p) { delete[] (p); (p)=NULL; } }
#endif

// 自定义一个SAFE_DELETE_ARRAY()宏, 释放数组对象
#ifndef SAFE_RELEASE
#define SAFE_RELEASE(p){ if (p) { (p)->Release(); (p)=NULL; } }
#endif

// 记录日志
void log(std::string txt);

// wchar_t* 转 char*
char* convert(const wchar_t* wstr);

接下来,我们将对Input的输入做一个封装,这个类之前我们封装过,可以直接拿过来使用,也就是“InputClass.h”和“InputClass.cpp”两个文件。接下来,我们要创建一个可以自由移动的透视摄像机类。我们之前做的摄像机只能沿着世界坐标系X/Y/Z轴移动,并不能自由移动。这样的摄像机对于2D游戏已经够用了,但是3D游戏还是不行的。在3D游戏中,我们可以随意的移动和旋转摄像机来查看场景中的任何物体。这个自由移动的摄像机的实现,主要是一个局部坐标系的概念。我们为摄像机声明三个方向向量,向上的方向,向右的方向,以及向观察点的方向。摄像机移动或旋转的时候,就可以按照这三个方向来调整。由于这个三个方向向量是局部坐标系的概念,因此它是跟随摄像机的变动而变动的。也就是说,我们向前移动的时候,实际就是向着观察点的方向移动。当我们旋转之后,观察点也随之变动,那么我们继续向前移动的话,还是向着观察点的方向移动。也就是说,摄像机向前移动,永远都是相对于自己的前面而言的。我们创建“CameraClass.h”和“CameraClass.cpp”两个文件,首先是“CameraClass.h”文件内容:

#pragma once
#include "main.h"

// 自由移动摄像机
class CameraClass {

public:

	D3DXVECTOR3 _right;	// 右方向
	D3DXVECTOR3 _up;	// 上方向
	D3DXVECTOR3 _look;	// 观察方向
	D3DXVECTOR3 _pos;	// 位置

public:

	// 构造方法
	CameraClass();

	// 有参构造函数
	CameraClass(float x, float y, float z);

	// 析构方法
	~CameraClass();

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

	// 左右移动
	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);

};

我们声明的三个方向向量和摄像机为位置信息,其中分别针对这三个方向定义了移动和旋转的函数。最后一个就是根据方向和位置来计算一个取景变换矩阵。“CameraClass.cpp”内容:

#include "CameraClass.h"

// 自由移动摄像机: 构造方法
CameraClass::CameraClass() {

	_pos = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
	_right = D3DXVECTOR3(1.0f, 0.0f, 0.0f);
	_up = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
	_look = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
}


// 自由移动摄像机: 有参构造方法
CameraClass::CameraClass(float x, float y, float z) {

	_pos = D3DXVECTOR3(x, y, z);
	_right = D3DXVECTOR3(1.0f, 0.0f, 0.0f);
	_up = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
	_look = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
}

// 自由移动摄像机: 析构方法
CameraClass::~CameraClass() {}

// 自由移动摄像机: 前后移动
void CameraClass::frontBackMove(float step) {

	_pos += D3DXVECTOR3(_look.x, 0.0f, _look.z) * step;
}

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

	_pos += D3DXVECTOR3(_right.x, 0.0f, _right.z) * step;
}

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

	_pos.y += step;
}

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

	D3DXMATRIX M;
	D3DXMatrixRotationY(&M, angle);
	D3DXVec3TransformCoord(&_right, &_right, &M);
	D3DXVec3TransformCoord(&_look, &_look, &M);
}

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

	D3DXMATRIX M;
	D3DXMatrixRotationAxis(&M, &_right, angle);
	D3DXVec3TransformCoord(&_up, &_up, &M);
	D3DXVec3TransformCoord(&_look, &_look, &M);
}

// 自由移动摄像机: 围绕 _look 方向旋转,相当于侧头
void CameraClass::sideRotate(float angle) {

	D3DXMATRIX M;
	D3DXMatrixRotationAxis(&M, &_look, angle);
	D3DXVec3TransformCoord(&_right, &_right, &M);
	D3DXVec3TransformCoord(&_up, &_up, &M);
}

// 自由移动摄像机: 计算取景变换矩阵
void CameraClass::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;
}

上面的函数中,D3DXMatrixRotationAxis是生成一个围绕指定方向旋转指定角度的旋转矩阵。D3DXVec3TransformCoord就是将一个向量做旋转操作。最后就是计算取景变换矩阵,这个过程不需要大家掌握。接下来就是我们的地形类,首先地形是一个网格对象,因此我们需要先对网格对象进行封装,创建“MeshClass.h”和“MeshClass.cpp”两个文件,其实我们之前就已经对网格进行了封装,可以拿过来直接使用。有了基本模型的封装,那么我们的地形类就可以继承MeshClass,并在此基础上做一个调整了。我们上文中创建了一个地形模型,也就是一个大的四边形纹理,直接使用MeshClass加载进来不就得嘛。为什么还要创建一个地形类呢,答案就是地形高度的计算,也就是根据X/Z值,计算Y值。我们的地形四边形中,每个顶点记录了X/Y/Z的数值。但是,小四边形的边长是100,那么小四边形内部的X/Y/Z值怎么计算呢?XZ是根据角色的移动计算出来的,那么我们就需要根据X/Z的值来计算Y值。计算Y值得的原理就是三角形的线性高度变化。

在上面的图例中,点P位于AB中间位置,如果AA´的距离是10,也就是说A的高度是10。那么点P的高度就应该是5,也就是P´的高度。这就是线性变化的原理。因为PAB的一半,因此P点的高度就应该是A高度的一半。明白原理后,我们创建“TerrainClass.h”和“TerrainClass.cpp”两个文件,其中“TerrainClass.h”文件如下:

#pragma once
#include "MeshClass.h"

// 地形类,主要是地形高度的计算
class TerrainClass : public MeshClass {

public:

	// 模型顶点数组,里面包含x,y,z值
	D3DXVECTOR3* meshVertexes;

	// 模型中四边形的长度
	float interval = 0.0f;

public:

	// 构造方法
	TerrainClass(LPDIRECT3DDEVICE9 _device, const wchar_t* _dir, const wchar_t* _file, float _interval);

	// 析构方法
	~TerrainClass();

	// 获取草地模型顶点数组
	void getMeshVertexes();

	// 获取草地模型顶点Y值
	float getMeshVertexY(float x, float z);

	// 获取地形高度值(Y值)
	float getTerrainHeight(float x, float z);

};

我们提供了三个函数,getMeshVertexes函数用于获取模型中所有顶点的坐标信息。getMeshVertexY函数用于根据给定的X/Z值来获取所在小四边形的四个顶点的Y值。也就是说,我们需要确定给定的X/Z值的点,来确定该点在那个小四边形中,并获取这个小四边形的四个顶点坐标,还有进一步确认该点在这个小四边形中那个三角形中。我们知道这个小四边形是由两个三角形组成的。这个信息确认后,我们就可以借助上文的图例算法来计算了。接下来就是“TerrainClass.cpp”文件的内容:

#include "TerrainClass.h"

// 地形类: 构造方法
TerrainClass::TerrainClass(LPDIRECT3DDEVICE9 _device, const wchar_t* _dir, const wchar_t* _file, float _interval) : MeshClass(_device, _dir, _file), interval(_interval) {

	// 获取草地模型顶点数组
	init();
	getMeshVertexes();
};

// 地形类: 析构方法
TerrainClass::~TerrainClass() {

	delete[] meshVertexes;
};

// 地形类: 获取草地模型顶点数组
void TerrainClass::getMeshVertexes() {

	// 顶点数量
	const int size = meshObj->GetNumVertices();
	meshVertexes = new D3DXVECTOR3[size];

	// 先克隆一份顶点数据
	LPD3DXMESH cloneMesh = 0;
	meshObj->CloneMeshFVF(meshObj->GetOptions(), D3DFVF_XYZ, D3DDevice, &cloneMesh);

	// 获取顶点缓冲数据
	LPDIRECT3DVERTEXBUFFER9 cloneMeshVertexBuffer;
	cloneMesh->GetVertexBuffer(&cloneMeshVertexBuffer);

	// 读取顶点缓存
	D3DXVECTOR3* pVertices;
	cloneMeshVertexBuffer->Lock(0, sizeof(meshVertexes), (void**)&pVertices, 0);
	for (DWORD i = 0; i < size; i++)
	{
		meshVertexes[i].x = round(pVertices[i].x);
		meshVertexes[i].y = round(pVertices[i].y);
		meshVertexes[i].z = round(pVertices[i].z);
	}
	cloneMeshVertexBuffer->Unlock();

	// 是否克隆网格对象
	cloneMesh->Release();
	cloneMesh = NULL;
};


// 地形类: 获取草地模型顶点Y值
float TerrainClass::getMeshVertexY(float x, float z) {

	int size = meshObj->GetNumVertices();
	for (int i = 0; i < size; i++) {

		D3DXVECTOR3 temp = meshVertexes[i];
		if (temp.x == x && temp.z == z) {
			return temp.y;
		}
	}
	return 0;
};

// 地形类: 获取地形高度值(Y值)
float TerrainClass::getTerrainHeight(float x, float z) {

	// 根据坐标获取所在四边形的四个顶点
	float vx_min = 0, vx_max = 0, vz_min = 0, vz_max;
	if (x >= 0) {
		vx_min = (int)(x / 100) * 100;
		vx_max = vx_min + interval;
	}
	else {
		vx_max = (int)(x / 100) * 100;
		vx_min = vx_max - interval;
	}
	if (z >= 0) {
		vz_min = (int)(z / 100) * 100;
		vz_max = vz_min + interval;
	}
	else {
		vz_max = (int)(z / 100) * 100;
		vz_min = vz_max - interval;
	}


	// 四边形四条边平行于X/Z轴
	// 两个顶点的间距为 100
	// 下图可以理解为俯视角度
	// 
	//  V1   V2
	//  *---*
	//  | / |
	//  *---*  
	//  V0   V3
	// 
	// 获取四个顶点的Y值
	float V0 = getMeshVertexY(vx_min, vz_min);
	float V1 = getMeshVertexY(vx_min, vz_max);
	float V2 = getMeshVertexY(vx_max, vz_max);
	float V3 = getMeshVertexY(vx_max, vz_min);

	// 如果四个顶点的高度是一样的,就直接返回任意一个高度即可
	//if (V0 == V1 && V1 == V2 && V2 == V3) {
		//return V0;
	//}

	// 判断(x,z)坐标在四边形中的位置
	// 四边形是一个边长为100的正方形
	// 如果 x == z 则该点在对角线V0V2上
	// 如果 x < z 则该点在三角形V0V1V2中
	// 如果 x > z 则该点在三角形V0V2V3中

	// 地形高度值(Y值)
	float height = 0.0f;

	// 在三角形V0V1V2中
	if (x <= z)
	{
		// V2和V1之间的高度差(可正可负)
		float uy = V2 - V1;
		// 点(x,z)与V1的水平差(x轴方向)
		float ux = x - vx_min;
		// 点(x,z)在V1V2的比率
		float ux_rate = ux / interval;

		// V0和V1之间的高度差(可正可负)
		float vy = V0 - V1;
		// 点(x,z)与V1的水平差(z轴方向)
		float vz = vz_max - z;
		// 点(x,z)在V1V0的比率
		float vz_rate = vz / interval;

		// 点(x,z)的高度为:V1高度 + x轴高度差 + z轴高度差
		// x/z轴的高度差计算公式为:V1点到V2点在x轴上的高度是线性变化的,如果V1到V2高度下降2米,则一半位置的时候,下降1米
		// 我们根据x轴的距离比率来计算高度差的
		height = V1 + uy * ux_rate + vy * vz_rate;
	}
	else
	{
		float uy = V0 - V3;
		float ux = vx_max - x;
		float ux_rate = ux / interval;

		float vy = V2 - V3;
		float vz = z - vz_min;
		float vz_rate = vz / interval;

		height = V3 + uy * ux_rate + vy * vz_rate;
	}

	// 返回地形高度
	return height;
};

这个实现类可能有些复杂,需要认真的去思考,里面涉及的一些细节比较多。比如如何确定指定点在那个三角形中,以及正负数的判断等等。所有类都封装完毕之后,我们就开始“main.cpp”文件的内容,首先是全局变量的声明:

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

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

// 输入类
InputClass* input;


// 摄像机
CameraClass* camera;

// 草地模型 
TerrainClass* land;

// 胶囊体模型
float x, y, z;
MeshClass* capsule;

// 摄像机移动和胶囊体移动速度
float distance = 5.0f;

在这里,我们使用一个胶囊体模型在草地上面进行移动,来测试我们的地形高度计算是否正确。摄像机可以根据“WASD”来进行左右前后移动,“QE”来进行左右旋转,“CV”来进行上下移动。而胶囊体模型的移动就是“IJKL”在世界坐标系的X轴和Z轴方向进行移动。注意,两者的移动是不一样的,摄像机是一个自由移动的,而胶囊体的移动仅能够在世界坐标系的X/Z轴方向移动。后期我们也会给这个胶囊体设置为一个自由移动的物体,并且让摄像机跟随起移动而移动。接下来就是initScene函数,代码如下:

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

// 初始化摄像机,参数是摄像机位置
camera = new CameraClass(500, 300, -300);

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

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

// 创建一个胶囊体
x = 500.0f, y = 0.0f, z = 100.0f;
capsule = new MeshClass(D3DDevice, L"asset/", L"capsule.X");
capsule->init();

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

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

// 初始化光照
initLight();

由于摄像机解决了取景变换矩阵,所以我们的initProjection函数改动如下:

// 设置取景变换矩阵
D3DXMATRIX V;
camera->getViewMatrix(&V);
D3DDevice->SetTransform(D3DTS_VIEW, &V);

// 设置透视投影变换矩阵
D3DXMATRIX projMatrix;
float angle = D3DX_PI * 0.5f;
float wh = (float)WINDOW_WIDTH / (float)WINDOW_HEIGHT;
D3DXMatrixPerspectiveFovLH(&projMatrix, angle, wh, 1.0f, 1000.0f);
D3DDevice->SetTransform(D3DTS_PROJECTION, &projMatrix);

// 设置视口变换
D3DVIEWPORT9 viewport = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, 1 };
D3DDevice->SetViewport(&viewport);

光照还是按照我们之前的旧代码,设置全局环境光以及默认材质。接下来我们renderScene函数,代码如下:

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

// 绘制胶囊体
D3DXMATRIX worldMatrix2;
D3DXMatrixTranslation(&worldMatrix2, x, y, z);
D3DDevice->SetTransform(D3DTS_WORLD, &worldMatrix2);
capsule->render();

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

由于我们需要操作摄像机的移动,因此在renderScene函数添加取景变换矩阵的调整,这样移动和旋转摄像机,场景画面就会变化了。我们先运行代码看看效果:

我们可以看到在草地上面放置了一个木板材质的胶囊体。接下来,我们就来完成update函数,让摄像机和胶囊体能够移动,代码如下:

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

// 按键判断
switch (key) {

	case 'A':
		// 摄像机向左移动
		camera->leftRightMove(-distance);
		break;

	case 'D':
		// 摄像机 向右移动
		camera->leftRightMove(distance);
		break;

	case 'W':
		// 摄像机 向前移动
		camera->frontBackMove(distance);
		break;

	case 'S':
		// 摄像机 向后移动
		camera->frontBackMove(-distance);
		break;

	case 'Q':
		// 摄像机 向左旋转
		camera->leftRightRotate(-0.02);
		break;

	case 'E':
		// 摄像机 向右旋转
		camera->leftRightRotate(0.02);
		break;

	case 'C':
		// 摄像机 向上移动
		camera->upDownMove(distance);
		break;

	case 'V':
		// 摄像机 向下移动
		camera->upDownMove(-distance);
		break;

	case 'I':
		// 模型向 z 轴正方向移动
		z += distance;
		// 根据地形高度设置模型y轴位置
		y = land->getTerrainHeight(x, z);
		break;

	case 'K':
		// 模型向 z 轴负方向移动
		z -= distance;
		// 根据地形高度设置模型y轴位置
		y = land->getTerrainHeight(x, z);
		break;

	case 'J':
		// 模型向 x 轴负方向移动
		x -= distance;
		// 根据地形高度设置模型y轴位置
		y = land->getTerrainHeight(x, z);
		break;

	case 'L':
		// 模型向 x 轴正方向移动
		x += distance;
		// 根据地形高度设置模型y轴位置
		y = land->getTerrainHeight(x, z);
		break;
}

对于胶囊体的移动,我们只需要修改器xz的数值,然后由地形类来就算y值,这样就完成了胶囊体的移动。而摄像机的自由移动则是调用起内部方法,但是,最终的取景变换还是在renderScene函数中完成的。运行效果如下:

 

通过移动胶囊体,我们发现,它的Y值计算还是正确的,但是,我们分别控制胶囊体和摄像机确实比较麻烦。 

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

workspace.zip

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

咆哮的程序猿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值