第七章 DirectX 数学向量,碰撞检测和粒子系统(上)

数学不仅在游戏开发中,甚至在整个软件编程行业中,都占据了非常重要的位置。如果没有数学,那么计算机编程不会得到快速的发展。软件编程的本质就是处理数据,这种处理很大程度上取决于算法,而算法的本质就是数学。游戏开发中使用最多的就是几何数学。坐标系是游戏开发中最基础的概念,包括2D坐标系和3D坐标系,其实就是维度的不同。2D坐标系就是x/y两个维度,它与我们的电脑/手机屏幕是一致的。3D坐标系则是由x/y/z三个维度构成,z维度其实就是垂直我们的电脑/手机屏幕向里或向外的方向。3D坐标系分为左手坐标系和右手坐标系,他们的区别就是z的方向不同而已。左手坐标系Z轴向里为正值,右手坐标系Z轴是向外为正值。DirectXUnity都是左手坐标系,OpenGL则是右手坐标系。在3D游戏世界中,有局部坐标系,世界坐标系,观察者(摄像机)坐标系,屏幕坐标系。我们使用最多的是局部坐标系和世界坐标系,两者可以相互转换。

向量是带箭头的线段,拥有长度和方向两个属性,它是一个矢量。没有方向,只有大小的,我们称之为标量。向量在游戏中使用非常频繁,它可以辅助角色模型的移动,旋转,缩放等等操作。需要注意的是,向量是没有位置属性的,也就是说,同一个向量在坐标系中随意移动的话,它是不变的。在DirectX中,使用D3DXVECTOR3代表一个三维向量,里面包括x, y, z三个数值。因为向量没有位置属性,因此,我们可以把向量尾部移动到坐标系的原点,此时向量的头部点的x, y, z坐标值就对应了向量的三个数值。在三维坐标系中,我们也使用x, y, z坐标值来标记某一个点的位置,也就是说,从数据结构上来讲,向量也可以用来表示一个点。两者的区别主要是在概念上,点是相对于坐标系原点而言的,它代表的是一个位置。向量表示长度和方向,它是相对于自己尾部点而言的。向量在坐标系中移动,不会改变向量的长度和方向两个属性。只有当向量的尾部点与坐标系原点重合时候,向量顶点的位置坐标值就等于向量的三个分量值。关于向量的一些操作如下:

1. 向量的相等,如果向量的三个数值分别相等,那么两个向量就相等。如果将这两个向量的尾部移动到坐标系原点,那么两个向量将会重合。直接使用 == 或者 != 来判断两者是否相等。另外,向量是没有大小比较操作的,不存一个向量大于或小于另外一个向量。这样的操作也没有意义。

	// 向量相等
	D3DXVECTOR3 a(100, 200, 300);
	D3DXVECTOR3 b(100, 200, 300);
	D3DXVECTOR3 c(100, 200, 400);
	if (a == b) str[0] = L"a == b";
	else str[0] = L"a != b";
	if (a == c) str[1] = L"a == c";
	else str[1] = L"a != c";

2. 向量模的计算,向量的长度值就是向量的模。在DirectX中,使用D3DXVec3Length函数来计算向量的模。它的模,也就是长度是一个标量,有大小之分。

	// 向量的模(长度)
	const D3DXVECTOR3 d(300, 400, 500);
	float len = D3DXVec3Length(&d);
	str[2] = L"d模 = " + std::to_wstring(len);

3. 向量归一化,如果一个向量的模(长度)是1的话,那么这个向量就是归一化向量,也称之为单位向量。DirectX中使用 D3DXVec3Normalize来归一化向量。向量归一化之后,方向不变,只是长度变成1了。这个操作在游戏开发中有特殊的用途。

	// 向量归一化
	D3DXVECTOR3 e;
	const D3DXVECTOR3 f (300, 400, 500);
	D3DXVec3Normalize(&e, &f);
	float len1 = D3DXVec3Length(&e);
	str[3] = L"e模 = " + std::to_wstring(len1);

4. 向量的加法,两个向量相加其实就是将两个向量的各个分量相加。在Directx中使用+来计算两个向量的加法。将两个向量尾部平移到坐标原点,相加结果的几何意义就是由两个向量组成的四边形的对角线向量。我们经常使用向量的加法来代表两个力的合力。

	// 向量加法
	D3DXVECTOR3 g1(1,2,3);
	D3DXVECTOR3 g2(4,5,6);
	D3DXVECTOR3 g3 = g1 + g2;

我们还可以这样理解,g1是一个坐标点,g2是一个移动向量,那么g1+g2就是将点g1按照g2的方向和距离进行移动,也就是新的位置点g3。使用向量代表物体的移动非常合适,向量的方向代表移动的方向,向量的长度代表移动的速度。

5. 向量的减法,两个向量相减其实就是将两个向量的各个分量相减。在DirectX中使用-来计算两个向量的减法。将两个向量尾部平移到坐标原点,向减结果的几何意义就是由减数向量顶点指向被减数顶点的向量。由此可见,调换两个位置,结果向量方向相反,大小不变。

	// 向量的减法
	D3DXVECTOR3 h1(1, 2, 3);
	D3DXVECTOR3 h2(4, 5, 6);
	D3DXVECTOR3 h3 = h1 - h2;
	D3DXVECTOR3 h4 = h2 - h1;

当需要计算当前位置朝向目标位置的时候,就可以使用向量减法来表示这个方向向量,然后再根据这个方向向量进行旋转,就可以让当前位置朝向目标位置了。

6. 向量的数乘,可以将一个向量乘以一个标量数值,目的就是改变向量的长度,方向不变。除法可以让一个向量乘以一个小数。

	// 向量的数乘
	D3DXVECTOR3 i1(1, 2, 3);
	D3DXVECTOR3 i2 = i1 * 10;

如果一个向量代表物体的移动方向和速度话,那么增加几倍的速度就可以使用数乘来实现。

7. 向量的点乘,两个向量的点乘的过程是将各个分量向乘之后在累加。在DirectX中使用D3DXVec3dot来实现。如果两个向量是归一化向量,则它的结果是两个向量的余弦值。

	// 向量的点乘
	D3DXVECTOR3 j1(1, 2, 3);
	D3DXVECTOR3 j2(4, 5, 6);
	float cos = D3DXVec3Dot(&j1, &j2);

点乘用来判断位置关系,如果两个向量点乘结果值是0,则两个向量垂直。如果结果值大于0,则两个向量夹角小于90度。如果结果值小于0,则两个向量夹角大于90度。另外,我们旋转时候的角度计算,也可以通过点乘来实现(前提是两个向量先归一化)。

8. 向量的叉乘,两个向量的叉乘过程比较复杂,它的结果仍然是一个向量,这个结果向量垂直于叉乘的两个向量。也就是,叉乘就是计算同时垂直于两个向量的向量。在DirectX中使用D3DXVec3Cross函数来计算叉乘。

	// 向量的叉乘
	D3DXVECTOR3 k1;
	D3DXVECTOR3 k2(1, 2, 3);
	D3DXVECTOR3 k3(4, 5, 6);
	D3DXVec3Cross(&k1, &k2, &k3);

矩阵的数据结构本质就是一个多维数组。我们经常使用的D3DXMATRIX就是一个4X4float数组而已。它的用途主要是变换(平移变换,旋转变换,缩放变换,取景变换,投影变换等等)。一般情况下,我们不会直接去定义一个矩阵。在DirectX中,使用  D3DXMatrixTranslation 函数定义一个平移矩阵,使用D3DXMatrixRotationXD3DXMatrixRotationYD3DXMatrixRotationZ三个函数分别定义一个沿X/Y/Z轴旋转矩阵,使用D3DXMatrixScaling 函数来定义一个旋转矩阵。多个不同的矩阵可以组合成一个变换矩阵,使用D3DXMatrixMultiply 函数进行乘法运算即可完成。

	// 定义一个平移矩阵,将目标平移到坐标系原点
	D3DXMATRIX moveMatrix;
	D3DXMatrixTranslation(&moveMatrix, 0, 0, 0);
	
	// 定义一个旋转矩阵,沿Y轴旋转45度
	D3DXMATRIX rotationMatrix;
	float angle = 45 * D3DX_PI / 180.0f;
	D3DXMatrixRotationY(&rotationMatrix, angle);

	// 定义一个缩放矩阵,沿x/y/z方向都放大十倍
	D3DXMATRIX scalingMatrix;
	D3DXMatrixScaling(&scalingMatrix, 10.0f, 10.0f, 10.0f);

	// 组合变换矩阵
	D3DXMATRIX worldMatrix;
	D3DXMatrixMultiply(&worldMatrix, &scalingMatrix, &rotationMatrix);
	D3DXMatrixMultiply(&worldMatrix, &worldMatrix, &moveMatrix);

备注:以上的变换中,使用的参数值都是相对于世界坐标系的。但有的时候,我们更倾向于使用局部坐标系。例如我们经常说,让人向前走的时候,这个就是相对于局部坐标系而言的。在Unity中使用transform.Translate()函数进行移动操作,默认的就是局部坐标系,当然该函数也可以根据世界坐标系来移动。在Unity中,一切都是GameObject对象,该对象包括一个Transform组件,这个组件就是用来进行平移,旋转和缩放的。当然,GameObject对象还有其他组件,例如Mesh Renderer用来渲染模型,Box Collider碰撞盒组件等等。

四元数Quaternion),它主要用来做旋转。从数据结构上来讲,它是一个四维向量 (x, y, z, w)。它的几何意义就是使用一个3维向量表示转轴和一个角度分量表示绕此转轴的旋转角度。如果使用(a,b,c)表示旋转轴的向量,r表示绕此轴的旋转角度,那么四元数表示如下:

x = a*sin(r/2),  y = b*sin(r/2),  z = c*sin(r/2),  w = cos(r/2)

一般情况下,我们也不会直接去定义个四元数,因为它的分量数据值比较晦涩难懂。在3D数学中,还有另外的矩阵和欧拉角两种旋转方式。三种方式各有优缺点,视情况而用。在DirectXDirectXMath 库中有关于四元数的操作函数支持。在Unity中默认使用四元数。

平面(Plane),在DirectX中使用D3DXPLANE来表示,它的用途主要用于碰撞检测中位置的判断。比如说一个点和平面的关系,这个点是在平面上,还是外侧,还是内侧。

射线Ray),包含一个起始点和一个方向向量。射线的参数方程为Pt= P0 + tU; 其中P0就是射线的起始点,U是射线的方向向量,t为大于等于零的标量参数,根据给定的t,就能求出射线上任意一点P的位置。如果t是负数的话,那么点P在射线相反方向上,也就是说不在射线上。射线也是在碰撞检测中使用,比如说拾取,拾取在3D游戏中使用非常频繁。例如玩家使用鼠标选中3D场景中的物体模型。它的原理就是由鼠标点击位置(x,y)发出一条射线,射线向游戏场景中无限延伸,并与3D场景中的物体模型相交,这样我们就能获取到这些选中的物体模型了。这个过程的计算公式比较复杂,我们可以轻松获取到鼠标点击位置(x,y,然后计算获取它在投影窗口上对应的P点,根据鼠标位置和投影P点就能确定一条射线了。这个计算过程需要进行坐标系转换。射线与物体模型的碰撞检测,其实就是射线与物体模型的碰撞球(碰撞盒)的碰撞检测。如果射线与碰撞球相交的话,那么射线上存在一个点P,它到球心的距离是小于等于球半径的。在射线参数方程中,点P对应的参数t应该是一个大于等于零的数。这个算法也比较复杂,这里不在详细描述了。

碰撞(Collide),3D模型是由上千个三角形图元组成的,如果通过计算这些三角形之间是否发生碰撞,来确定两个3D模型是否发生碰撞,这将是一个非常耗时的工作。因此,我们必须使用近似值的方式来逼近这种碰撞计算。碰撞体是一个围绕3D模型的一个或一组几何图形,比如说一个立方体或者一个球体,使用它们可以简化3D模型的碰撞计算。这种碰撞的计算公式非常简单,就是计算两个几何图形是否发生重合即可。为了能够使碰撞体更加逼真的模拟3D模型的真实形状,我们可以使用多个碰撞体进行组合拼装成一个近似3D模型的形状。在2D游戏中,我们同样也使用2维的碰撞体来进行碰撞检测。

一个碰撞立方体图形,称之为碰撞盒(包围盒/边界框)。它是由x/y/z的最大值和最小值构成。检查一个点是否与碰撞盒碰撞,只需要判断该点的坐标是否在x/y/z的最大值和最小值之间就可以了。当然,我们还可以反其道而行,只要该点任何一个轴向上的坐标值不在最大值和最小值之间,则不会发生碰撞。检查两个碰撞盒是否碰撞的算法比较复杂一点,可以将一个碰撞盒的8个顶点依次判断是否在另一个碰撞盒的内部来计算得出两个碰撞盒是否发生了碰撞。当然,我们仍然可以反其道而行,在任何一个轴向上,一个碰撞盒的最大值小于另一个碰撞盒的最小值,或者一个碰撞盒的最小值大于另一个碰撞盒的最大值,则两个碰撞盒不可能发生碰撞。

一个碰撞球体图形,称之为碰撞球(包围球/边界球),它有圆心坐标和半径构成。检查一个点是否与碰撞球碰撞,只需要计算该点到圆心的距离是否小于半径即可。检查两个碰撞球是否碰撞的算法就是两个圆心坐标的距离是否小于两个圆的半径和。

碰撞的本质就是数学问题,为了演示碰撞盒和碰撞球,我们使用VS2019新建一个项目“D3D_07_Collide”,首先我们需要对碰撞盒进行封装,创建“CollideBox.h”和“CollideBox.cpp”两个文件,其中“CollideBox.h”如下:

#pragma once
#include "main.h"

// 碰撞盒(立方体)
class CollideBox {

public:

	float xMin;
	float xMax;
	float yMin;
	float yMax;
	float zMin;
	float zMax;

public:

	// 构造函数
	CollideBox(float _xmin, float _xmax, float _ymin, float _ymax, float _zmin, float _zmax);

	// 构造函数
	CollideBox(D3DXVECTOR3 min, D3DXVECTOR3 max);

	// 构造函数,参数是模型的所有顶点坐标数据列表
	CollideBox(D3DXVECTOR3* list, int len);

	// 更新碰撞盒位置
	void updateCollide(float x, float y, float z);

	// 检查某个点是否在碰撞盒内
	bool isPointInside(float x, float y, float z);

	// 检查两个碰撞盒是否碰撞
	bool isCollideBox(CollideBox* box);

};

我们提供了三个构造方法,一个是直接给出碰撞盒立方体的最大值和最小值。另一个就是根据两个向量来初始化最大值和最小值。最后一个就是根据模型的顶点列表来获取最大值和最小值。其实DirectX中已经提供了D3DXComputeBoundingBox函数用于计算碰撞盒,其实它的原理就是根据模型的顶点来计算的。由于碰撞盒是包围在模型周围的,因此,当模型移动的时候,碰撞盒也需要跟随移动,所以,我们提供了更新碰撞盒位置的函数。然后就是两个检测方法,一个是检测一个点是否在碰撞盒内部,一个就是检测两个碰撞盒是否碰撞。接下来,我们继续实现“CollideBox.cpp”函数,代码如下:

#include "CollideBox.h"

// 碰撞盒:构造函数
CollideBox::CollideBox(float _xmin, float _xmax, float _ymin, float _ymax, float _zmin, float _zmax) {

	xMin = _xmin;
	xMax = _xmax;
	yMin = _ymin;
	yMax = _ymax;
	zMin = _zmin;
	zMax = _zmax;
};

// 碰撞盒:构造函数
CollideBox::CollideBox(D3DXVECTOR3 min, D3DXVECTOR3 max) {

	xMin = min.x;
	xMax = max.x;
	yMin = min.y;
	yMax = max.y;
	zMin = min.z;
	zMax = max.z;
}

// 碰撞盒:构造函数,参数是模型的所有顶点坐标数据列表
CollideBox::CollideBox(D3DXVECTOR3* list, int len) {

	xMin = 0;
	xMax = 0;
	yMin = 0;
	yMax = 0;
	zMin = 0;
	zMax = 0;

	for (int i = 0; i < len; i++) {

		D3DXVECTOR3 temp = list[i];

		if (temp.x < xMin) xMin = temp.x;
		if (temp.y < yMin) yMin = temp.y;
		if (temp.z < zMin) zMin = temp.z;

		if (temp.x > xMax) xMax = temp.x;
		if (temp.y > yMax) yMax = temp.y;
		if (temp.z > zMax) zMax = temp.z;
	}
};

// 碰撞盒:更新碰撞盒位置
void CollideBox::updateCollide(float x, float y, float z) {

	xMin += x;
	xMax += x;
	yMin += y;
	yMax += y;
	zMin += z;
	zMax += z;
};

// 碰撞盒:检查某个点是否在碰撞盒内
bool CollideBox::isPointInside(float x, float y, float z) {

	if (xMax < x || xMin > x) return false;
	if (yMax < y || yMin > y) return false;
	if (zMax < z || zMin > z) return false;

	return true;
};

// 碰撞盒:检查两个碰撞盒是否碰撞
bool CollideBox::isCollideBox(CollideBox* box) {

	if (xMax < (*box).xMin || xMin >(*box).xMax) return false;
	if (yMax < (*box).yMin || yMin >(*box).yMax) return false;
	if (zMax < (*box).zMin || zMin >(*box).zMax) return false;

	return true;
};

碰撞盒完毕后,我们继续封装碰撞球,创建“CollideSphere.h”和“CollideSphere.cpp”两个文件,首先是“CollideSphere.h”文件,如下:

#pragma once
#include "main.h"

// 碰撞球
class CollideSphere {

public:

	float x, y, z;	// 圆心
	float radius;	// 半径

public:

	// 构造函数
	CollideSphere(float _x, float _y, float _z, float _r);

	// 构造函数,参数是模型的所有顶点坐标数据列表
	CollideSphere(D3DXVECTOR3* list, int len);

	// 更新碰撞球位置,其实就是圆心的位置
	void updateCollide(float _x, float _y, float _z);

	// 检查某个点是否在碰撞球内
	bool isPointInside(float x, float y, float z);

	// 检查两个碰撞球是否碰撞
	bool isCollideSphere(CollideSphere* sphere);

};

和我们封装碰撞盒的思路是一样的,提供两个构造函数,提供碰撞球位置更新函数,以及两个碰撞检测函数。接下来我们来实现“CollideSphere.cpp”,代码如下:

#include "CollideSphere.h"

// 碰撞球:构造函数
CollideSphere::CollideSphere(float _x, float _y, float _z, float _r) {

	x = _x;
	y = _y;
	z = _z;
	radius = _r;
};

// 碰撞球:构造函数,参数是模型的所有顶点坐标数据列表
CollideSphere::CollideSphere(D3DXVECTOR3* list, int len) {

	// 寻找圆心,最大值和最小值的中间值就是
	float xmin = 0, xmax = 0, ymin = 0, ymax = 0, zmin = 0, zmax = 0;
	for (int i = 0; i < len; i++) {

		D3DXVECTOR3 temp = list[i];

		if (temp.x < xmin) xmin = temp.x;
		if (temp.y < ymin) ymin = temp.y;
		if (temp.z < zmin) zmin = temp.z;

		if (temp.x > xmax) xmax = temp.x;
		if (temp.y > ymax) ymax = temp.y;
		if (temp.z > zmax) zmax = temp.z;
	}
	x = (xmin + xmax) / 2;
	y = (ymin + ymax) / 2;
	z = (zmin + zmax) / 2;

	// 寻找半径,每个顶点距离圆心的最大值
	float max = 0;
	for (int i = 0; i < len; i++) {

		D3DXVECTOR3 temp = list[i];
		float dx = temp.x - x;
		float dy = temp.y - y;
		float dz = temp.z - z;
		float dd = dx * dx + dy * dy + dz * dz;
		if (dd > max) max = dd;
	}
	radius = sqrt(max);
};

// 碰撞球:更新碰撞球位置,其实就是圆心的位置
void CollideSphere::updateCollide(float _x, float _y, float _z) {

	x += _x;
	y += _y;
	z += _z;
}

// 碰撞球:检查某个点是否在碰撞球内
bool CollideSphere::isPointInside(float _x, float _y, float _z) {

	float dx = _x - x;
	float dy = _y - y;
	float dz = _z - z;
	float dd = dx * dx + dy * dy + dz * dz;
	if (dd > radius * radius) return false;
	return true;
};

// 碰撞球:检查两个碰撞球是否碰撞,两个圆心的距离
bool CollideSphere::isCollideSphere(CollideSphere* sphere) {

	float dx = (*sphere).x - x;
	float dy = (*sphere).y - y;
	float dz = (*sphere).z - z;
	float dd = dx * dx + dy * dy + dz * dz;
	float dd2 = (*sphere).radius + radius;
	if (dd > dd2 * dd2) return false;
	return true;
};

两个碰撞体封装完毕后,我们就开始主源文件“main.cpp”。在本案例中,我们将创建两个茶壶,然后为两个茶壶赋予碰撞盒(或者碰撞球),然后移动其中一个茶壶用于碰撞检测。为了能够直观的看到碰撞盒的样子,我们使用一个立方体来模拟碰撞盒的样子。首先,我们声明全局变量:

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

// 鼠标位置
int mx = 0, my = 0;

// 构建两个茶壶
float x = 0, x2 = 0;
LPD3DXMESH D3DTeapt, D3DTeapt2;

// 构建两个碰撞盒
CollideBox* collideBox, * collideBox2;

// 构建两个碰撞盒模拟
LPD3DXMESH D3DCollideBox,D3DCollideBox2;

// 声明计算碰撞盒函数
void getMeshCollideBox(LPD3DXMESH _mesh, D3DXVECTOR3* _min, D3DXVECTOR3* _max);

我们使用两个茶壶作为碰撞试验对象,同时为他们创建两个碰撞盒对象,为了能够观察到碰撞盒的样子,我们使用两个立方体模型来模拟碰撞盒。碰撞盒本身就是虚拟存在的。最后就是一个用于计算碰撞盒的函数getMeshCollideBox,该函数具体代码如下:

// 定义计算碰撞盒函数
void getMeshCollideBox(LPD3DXMESH _mesh, D3DXVECTOR3* _min, D3DXVECTOR3* _max) {

	BYTE* v = 0;
	_mesh->LockVertexBuffer(0, (void**)&v);
	D3DXComputeBoundingBox((D3DXVECTOR3*)v,_mesh->GetNumVertices(),D3DXGetFVFVertexSize(_mesh->GetFVF()),_min,_max);
	_mesh->UnlockVertexBuffer();
};

该函数主要使用DirectX提供的D3DXComputeBoundingBox函数来就算碰撞盒的数值。当然,我们也可以自己读取模型的顶点列表,然后循环对比来找出最大值和最小值。接下来,就是initScene函数,代码如下:

// 初始化第一个茶壶模型和X轴位置
x = -3.0f;
D3DXCreateTeapot(D3DDevice, &D3DTeapt, NULL);

// 构建第一个茶壶的碰撞盒,并更新其位置
D3DXVECTOR3 min, max;
getMeshCollideBox(D3DTeapt, &min, &max);
collideBox = new CollideBox(min, max);
collideBox->updateCollide(x, 0.0f, 0.0f);

// 创建第一个碰撞盒模型
float w = abs(collideBox->xMax - collideBox->xMin);
float h = abs(collideBox->yMax - collideBox->yMin);
float d = abs(collideBox->zMax - collideBox->zMin);
D3DXCreateBox(D3DDevice, w, h, d, &D3DCollideBox, NULL);

// 初始化第二个茶壶模型和X轴位置
x2 = 3.0f;
D3DXCreateTeapot(D3DDevice, &D3DTeapt2, NULL);

// 构建第一个茶壶的碰撞盒,并更新其位置
D3DXVECTOR3 min2, max2;
getMeshCollideBox(D3DTeapt2, &min2, &max2);
collideBox2 = new CollideBox(min2, max2);
collideBox2->updateCollide(x2, 0.0f, 0.0f);

// 创建第二个碰撞盒模型
float w2 = abs(collideBox2->xMax - collideBox2->xMin);
float h2 = abs(collideBox2->yMax - collideBox2->yMin);
float d2 = abs(collideBox2->zMax - collideBox2->zMin);
D3DXCreateBox(D3DDevice, w2, h2, d2, &D3DCollideBox2, NULL);

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

// 初始化光照
initLight();

紧接着,就是我们的renderScene函数,该函数目前主要用于茶壶模型和碰撞盒模拟的渲染,代码如下:

// 定义第一个茶壶位置
D3DXMATRIX worldMatrix;
D3DXMatrixTranslation(&worldMatrix, x, 0.0f, 0.0f);
D3DDevice->SetTransform(D3DTS_WORLD, &worldMatrix);

// 实体表面显示模型
D3DDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);

// 设置茶壶红色材质
D3DMATERIAL9 defaultMaterial1;
::ZeroMemory(&defaultMaterial1, sizeof(defaultMaterial1));
defaultMaterial1.Ambient = D3DXCOLOR(0.8f, 0.3f, 0.3f, 0.0f);	// 部分反射环境光
defaultMaterial1.Diffuse = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 不发射漫反射光
defaultMaterial1.Specular = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 不发射高光
defaultMaterial1.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 不自发光
defaultMaterial1.Power = 0.0f;									// 没有高光区
D3DDevice->SetMaterial(&defaultMaterial1);

// 绘制第一个茶壶
D3DTeapt->DrawSubset(0);

// 线框显示模型
D3DDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);

// 设置灰色材质
D3DMATERIAL9 defaultMaterial2;
::ZeroMemory(&defaultMaterial2, sizeof(defaultMaterial2));
defaultMaterial2.Ambient = D3DXCOLOR(0.8f, 0.8f, 0.8f, 0.0f);	// 部分反射环境光
defaultMaterial2.Diffuse = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 不发射漫反射光
defaultMaterial2.Specular = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 不发射高光
defaultMaterial2.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 不自发光
defaultMaterial2.Power = 0.0f;									// 没有高光区
D3DDevice->SetMaterial(&defaultMaterial2);

// 绘制第一个碰撞盒模拟立方体
D3DCollideBox->DrawSubset(0);

// 定义第二个茶壶位置
D3DXMATRIX worldMatrix2;
D3DXMatrixTranslation(&worldMatrix2, x2, 0.0f, 0.0f);
D3DDevice->SetTransform(D3DTS_WORLD, &worldMatrix2);

// 实体表面显示模型
D3DDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);

// 设置茶壶红色材质
D3DDevice->SetMaterial(&defaultMaterial1);

// 绘制第二个茶壶
D3DTeapt2->DrawSubset(0);

// 线框显示模型
D3DDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);

// 设置灰色材质
D3DDevice->SetMaterial(&defaultMaterial2);

// 绘制第二个碰撞盒模拟立方体
D3DCollideBox2->DrawSubset(0);

为了能够区分茶壶模型和碰撞盒模型,我们分别使用颜色和线框的方式来做区分,运行代码:

为了我们方便观察茶壶的样子,我们的取景变换矩阵参数做了调整。

// 设置取景变换矩阵
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(0.0f, 2.0f, -8.0f);
D3DXVECTOR3 viewLookAt(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);

接下来,我们就开始移动第二个茶壶,让两个茶壶不断靠近并发生碰撞,也就是我们的update函数,该函数主要就是修改第二个茶壶的X轴位置,并同步更新碰撞盒位置,如下:

// 只处理键盘按起事件
if (type != 2) return;

// 左右方向检查
if (wParam == 'A') {

	x2 -= 0.5f;
	collideBox2->updateCollide(-0.5f, 0.0f, 0.0f);
}
if (wParam == 'D') {

	x2 += 0.5f;
	collideBox2->updateCollide(0.5f, 0.0f, 0.0f);
}

这里一定要注意的是,模型移动的同时要同步更新碰撞盒的位置。接下来就是碰撞检测,这部分代码,我们可以放到update函数中,也可以放到renderScene函数中。两者的区别在于游戏代码框架设计的问题,这里不在描述,本案例中将放入到renderScene函数中,如下:

// 碰撞检测
bool flag = collideBox->isCollideBox(collideBox2);
if (flag && isCollide == false) {
	isCollide = true;
	MessageBox(hwnd, L"发生碰撞", L"标题", IDOK);
}

其中isCollide布尔变量只是方便我们进行测试一遍即可,它是一个全局变量而已。运行代码,不停输入键盘“A”,让两个茶壶靠近并发生碰撞,如下:

关于如何使用碰撞球,我们创建新的项目“D3D_07_Collide2”来演示,具体代码就不在详细介绍了。这两个案例都是碰撞盒与碰撞盒,碰撞球与碰撞球的碰撞检测。那么,一个碰撞盒与一个碰撞球发生碰撞的话,如何计算呢?从我们的上面的计算公式来看,碰撞球的计算公式比较简单明了。我们知道一条线与一个圆相交的话,圆心到切点的距离小于等于半径。这个切点就是圆心到线的垂直交叉点。如果一个面和一个球相交的话,球心到面的垂直距离也会小于等于球半径。如果碰撞球和碰撞盒相交的话,那么他们必然有一个相交的面,球心到面的垂直距离也会小于等于球半径。球心到面的垂直交叉点就是切点。我们可以分别从x/y/z三个轴向来判断碰撞球和碰撞盒是否碰撞。比如说X轴,碰撞球的在X轴上的最大值和最小值与球心坐标X值的距离差是否小于等于球半径就可以判断两者在X轴上是否相交碰撞。该算法有待于验证!如果考虑到旋转的话,那么碰撞盒检测将是一件更加复杂耗时的操作。在2D游戏中,碰撞盒就是一个矩形,碰撞球就是一个圆形。如果矩形在圆形的左边,我们只需要计算矩形X最大值与圆心X值得差是否小于等于半径即可。

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

workspace.zip

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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
DirectX是一个由微软公司开发的多媒体应用程序接口(API),它可以用于形成实际的游戏场景。通过DirectX游戏开发者可以利用其提供的丰富功能和工具来创建绚丽多彩的游戏世界。 首先,DirectX可以加入人物动画。游戏中的各种角色可以通过DirectX实现逼真的动态效果。开发者可以通过DirectX提供的图形处理功能和动画技术,精确控制角色的运动、表情和姿态,使其在游戏场景中栩栩如生。这样,玩家就能够更加真切地感受到游戏中的角色与故事情节。 其次,DirectX还可以加入粒子系统粒子系统是一种模拟真实世界中颗粒化物质的技术,通过在游戏场景中添加粒子效果,为游戏增添了更多的细节和真实感。比如,在火焰场景中加入粒子效果可以让火焰更加逼真,而在雨天场景中加入粒子效果可以让雨滴看起来更加真实。 此外,DirectX还可以通过其提供的渲染技术来丰富游戏场景。渲染技术可以帮助开发者将游戏中的模型、贴图、光照等元素更好地呈现在玩家眼前。通过合理的场景设计和渲染处理,可以使游戏场景更加细腻、真实,从而提升玩家的沉浸感。 综上所述,通过使用DirectX游戏开发者可以实现一个实际的游戏场景。它不仅可以加入逼真的人物动画,还可以通过粒子系统和渲染技术来丰富游戏场景,使之更加生动有趣。完成一款使用DirectX开发游戏,玩家能够享受到更加真实、震撼的游戏体验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

咆哮的程序猿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值