数学不仅在游戏开发中,甚至在整个软件编程行业中,都占据了非常重要的位置。如果没有数学,那么计算机编程不会得到快速的发展。软件编程的本质就是处理数据,这种处理很大程度上取决于算法,而算法的本质就是数学。游戏开发中使用最多的就是几何数学。坐标系是游戏开发中最基础的概念,包括2D坐标系和3D坐标系,其实就是维度的不同。2D坐标系就是x/y两个维度,它与我们的电脑/手机屏幕是一致的。3D坐标系则是由x/y/z三个维度构成,z维度其实就是垂直我们的电脑/手机屏幕向里或向外的方向。3D坐标系分为左手坐标系和右手坐标系,他们的区别就是z的方向不同而已。左手坐标系Z轴向里为正值,右手坐标系Z轴是向外为正值。DirectX和Unity都是左手坐标系,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就是一个4X4的float数组而已。它的用途主要是变换(平移变换,旋转变换,缩放变换,取景变换,投影变换等等)。一般情况下,我们不会直接去定义一个矩阵。在DirectX中,使用  D3DXMatrixTranslation 函数定义一个平移矩阵,使用D3DXMatrixRotationX,D3DXMatrixRotationY,D3DXMatrixRotationZ三个函数分别定义一个沿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数学中,还有另外的矩阵和欧拉角两种旋转方式。三种方式各有优缺点,视情况而用。在DirectX中DirectXMath 库中有关于四元数的操作函数支持。在Unity中默认使用四元数。

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

射线(Ray),包含一个起始点和一个方向向量。射线的参数方程为P(t)= 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来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!

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

  1. matlab武汉理工大学数值分析线性函数拟合实验_11数值分析第七章数值微积分龙贝格积分大学数学云课堂...

    数值分析基础入门所有知识点相关内容,请按照前面数字依次学习 1第二章数值分析的基本概念之有效数字与秦九韶算法 1数值分析第二章基本概念作业答疑大学数学云课堂 2数值分析第三章线性代数方程组的直接法大学 ...

  2. 高等数学:第七章 空间解析几何(1)空间解析几何与向量代数 向量的加减法、数乘、坐标

    §7.1  空间直角坐标系 一.空间点的直角坐标 平面直角坐标系使我们建立了平面上的点与一对有序数组之间的一一对应关系,沟通了平面图形与数的研究. 为了沟通空间图形与数的研究, 我们用类似于平面解析几 ...

  3. matlab平面抛射方程,MATLAB 数学实验 第七章 微分方程与计算机模拟 PPT注记

    第七章 微分方程与计算机模拟 PPT 注记 (2009-5-24) 只有三个内容,有一定难度和深度.三个内容是: 常微分方程初值问题求数据解和蝴蝶效应(洛仑兹模型)的动态仿真.追击曲线动态仿真.有阻力 ...

  4. Linux内核分析 读书笔记 (第七章)

    第七章 链接 1.链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行. 2.链接可以执行于编译时,也就是在源代码被翻译成机器代码时:也可以执行于 ...

  5. 概率统计:第七章 参数估计

    第七章  参数估计 内容提要: 一.        点估计 1.设为总体的样本,总体的分布函数形式已知,为待估参数, 为对应的样本观测值.点估计问题就是构造一个适当的统计量,用其观测值来估计待估参数的 ...

  6. Kali Linux 网络扫描秘籍 第七章 Web 应用扫描(三)

    第七章 Web 应用扫描(三) 作者:Justin Hutchens 译者:飞龙 协议:CC BY-NC-SA 4.0 7.13 使用 BurpSuite Sequencer(序列器) Web 应用会 ...

  7. 今天开始学Convex Optimization:第2章 背景数学知识简述

    文章目录 第2章 背景数学知识简述 2.1 数学分析和微积分基础 函数性质 集合Sets Norms 线性函数.仿射函数 函数的微分(导数) 2.2 线性代数基础 Matrix Subspaces 正 ...

  8. 《神经网络与深度学习》习题解答(至第七章)

    部分题目的个人解答,参考了github上的习题解答分享与知乎解答.题目是自己做的,部分解答可能出错,有问题的解题部分欢迎指正.原文挂在自己的自建博客上. 第二章 2-1 直观上,对特定的分类问题,平方 ...

  9. 【统计信号处理kay 第七章 两道习题python仿真】

    统计信号处理 第七章 两道习题python仿真 前言 一.蒙特卡洛计算法 二.7.13题 1.题目内容 2.流程图 3.示例代码 4.实验结果 理论分析 三.7.14题 1.题目内容 2.参考代码 3 ...

  10. 【 Real-Time Rendering 3rd 提炼总结】 六 第七章 高级着色 BRDF及相关技术

    本文由@浅墨_毛星云 出品,转载请注明出处.     文章链接: http://blog.csdn.net/poem_qianmo/article/details/75943714 在计算机图形学中, ...

最新文章

  1. cambridge sharing note 1
  2. Vue.js – 基于 MVVM 实现交互式的 Web 界面
  3. crontab 每5分钟_Crontab安装步骤和命令使用详细解说
  4. 雅可比旋转求解对称二维矩阵的特征值和特征向量
  5. aws lambda使用_使用AWS Lambdas扩展技术堆栈
  6. 线程,进程,并发,并行
  7. Caffe源码解析1:Blob
  8. 现有类 成 mfc类_女人不想成“黄脸婆”,4类食物是衰老“催化剂”,女人尽量远离_氧化...
  9. Illustrator 教程,如何将 Illustrator 文档另存为 PDF?
  10. 使用jdk进行数据迁移(sqlite迁移mysql)
  11. win10您的计算机配置文件,Win10系统开机登录提示无法加载用户配置文件如何解决...
  12. 互联网日报 | 58到家正式改名“天鹅到家”;华为“服务日”活动宣布延长一年;特斯拉上线电池回收服务...
  13. android 打包报错 Execution failed for task ‘:app:lintVitalRelease‘.
  14. 投票动态代理proxy案例(java)
  15. 安卓自定义view仿小米商城购物车动画
  16. nodejs-指定长度断句
  17. 推荐(笔记软件、日程安排软件)
  18. Elsevier期刊投稿所遇到的问题及解决方案
  19. Python编程笔记(第三篇)【补充】三元运算、文件处理、检测文件编码、递归、斐波那契数列、名称空间、作用域、生成器...
  20. 《dota2》地精修补匠tinker路人攻略

热门文章

  1. ArcGIS Pro 中检查尖锐角步骤
  2. 杭州银行2018信息科技部面试
  3. 读书有益——》来自古诗词中的成语
  4. 模拟退火算法应用(Java)
  5. photoshop 技巧
  6. 一个让人不得不转的故事-《通宵达旦工资只有3200 博客网架构师艰难浪迹于北京》...
  7. 蓝桥杯 算法训练 调和数列问题
  8. 沟通:如何用沟通解决80%的工作问题?
  9. 《互联网周刊》:中国互联网10年大事记
  10. 如何做好日程管理?实操介绍:不同角色的日程管理方法