最近在做一个OpenGL的小游戏,想要实现碰撞检测与模拟重力效果,即类似Unity3d的物理引擎。碰撞检测参考了一篇博文: http://blog.csdn.net/zju_fish1996/article/details/51869828 建议大家可以先看看。不过博文写的仅仅是不考虑Y方向信息,即高度信息的碰撞检测,而我想要实现的是有重力+跳跃的,高度信息仍然需要考虑的,所以下面是我的改进,以及引入了重力计算。
另外,下文完整项目代码可以访问我的github查看:https://github.com/MarkMoHR/OpenglGame
编程环境:
Windows10 + VS2015(x86) + freeglut + glm库
效果展示:
(玩家跳跃上木箱子,再跳下来)
(游戏场景图,改进后)
技术实现(以下步骤按照整个物理引擎类PhysicsEngine的实现步骤讲解):
1、布置场景的时候先初始化外部边缘位置坐标,与内部物体边缘位置坐标:
需要先把所有碰撞边缘的坐标传递给PhysicsEngine:
void PhysicsEngine::setSceneOuterBoundary(float x1, float z1, float x2, float z2) {
outerBoundary = glm::vec4(x1, z1, x2, z2);
}
void PhysicsEngine::setSceneInnerBoundary(float x1, float y1, float z1, float x2, float y2, float z2) {
glm::vec3 key(x1 - BoundaryGap, y1 - BoundaryGap, z1 - BoundaryGap);
glm::vec3 value(x2 + BoundaryGap, y2 + BoundaryGap, z2 + BoundaryGap);
innerBoundaryMin.push_back(key);
innerBoundaryMax.push_back(value);
}
2、main函数的glutDisplayFunc()方法由于是循环调用,所以需要在里面每次调用的时候 更新摄像机的水平和竖直方向的移动。在每一帧 水平方向移动结束后,然后先进行水平方向的碰撞检测(外部与内部检测)。
void FPSCamera::updateCameraHoriMovement() {
float dx = 0;
float dz = 0;
if (isWPressing)
dz += 2;
if (isSPressing)
dz -= 2;
if (isAPressing)
dx -= 2;
if (isDPressing)
dx += 2;
if (dz != 0 || dx != 0) {
//行走不改变y轴坐标
glm::vec3 forward = glm::vec3(viewMatrix[0][2], 0.f, viewMatrix[2][2]);
glm::vec3 strafe = glm::vec3(viewMatrix[0][0], 0.f, viewMatrix[2][0]);
cameraPos += (-dz * forward + dx * strafe) * MoveSpeed;
targetPos = cameraPos + (-dz * forward + dx * strafe) * 1.5f;
//每次做完坐标变换后,先进行碰撞检测来调整坐标
physicsEngine->outCollisionTest(cameraPos, targetPos);
physicsEngine->inCollisionTest(cameraPos, targetPos);
}
}
3、 水平方向的 外部/内部边缘碰撞检测:
上面代码可以看到调用了PhysicsEngine的两个碰撞检测方法。具体的实现原理与实现代码大多是参考上面发的第一个链接,大家可以参考
(1) 外部边缘碰撞检测:这个相对简单,就是让出了边界的视点放回来,再做调整:
void PhysicsEngine::outCollisionTest(glm::vec3 & cameraPos, glm::vec3 & targetPos) {
outCollisionTestXZ(outerBoundary[0], outerBoundary[1], outerBoundary[2], outerBoundary[3], cameraPos, targetPos);
}
void PhysicsEngine::outCollisionTestXZ(float x1, float z1, float x2, float z2, glm::vec3 & cameraPos, glm::vec3 & targetPos) {
//先设置包围盒:比空间外部边缘小一点
if (x1 < 0)
x1 += 2;
else x1 -= 2;
if (x2 < 0)
x2 += 2;
else x2 -= 2;
if (z1 < 0)
z1 += 2;
else z1 -= 2;
if (z2 < 0)
z2 += 2;
else z2 -= 2;
//如果目标位置出了包围盒,先放回来
if (targetPos[0] < x1) {
targetPos[0] = x1;
}
if (targetPos[0] > x2) {
targetPos[0] = x2;
}
if (targetPos[2] < z1) {
targetPos[2] = z1;
}
if (targetPos[2] > z2) {
targetPos[2] = z2;
}
float distance = sqrt((cameraPos[0] - targetPos[0])*(cameraPos[0] - targetPos[0]) +
(cameraPos[2] - targetPos[2])*(cameraPos[2] - targetPos[2]));
//若视点与目标距离太小,则固定目标位置,视点沿正对目标的逆方向移动
if (distance <= 2.0f) {
cameraPos[0] = 2.0f*(cameraPos[0] - targetPos[0]) / distance + targetPos[0];
cameraPos[2] = 2.0f*(cameraPos[2] - targetPos[2]) / distance + targetPos[2];
}
bool flag = false;
//再检测视点是否出了包围盒,若是则放回
if (cameraPos[0] < x1) {
flag = true;
cameraPos[0] = x1;
}
if (cameraPos[0] > x2) {
flag = true;
cameraPos[0] = x2;
}
if (cameraPos[2] < z1) {
flag = true;
cameraPos[2] = z1;
}
if (cameraPos[2] > z2) {
flag = true;
cameraPos[2] = z2;
}
//重复上述远离两点距离的操作
if (flag) {
distance = sqrt((cameraPos[0] - targetPos[0])*(cameraPos[0] - targetPos[0]) +
(cameraPos[2] - targetPos[2])*(cameraPos[2] - targetPos[2]));
if (distance <= 2.0f) {
targetPos[0] = 2.0f*(targetPos[0] - cameraPos[0]) / distance + cameraPos[0];
targetPos[2] = 2.0f*(targetPos[2] - cameraPos[2]) / distance + cameraPos[2];
}
}
}
(2) 内部边缘碰撞检测:这个相对复杂。上面提到的博文只是实现了不考虑y方向,即高度信息的xz平面的碰撞检测。但是如果我们加入了重力,高度信息明显也需要考虑进去,所以我做了如下改进:
只有当玩家身体处于碰撞体垂直区域范围内,才进行XZ平面的碰撞检测。而内部边缘的碰撞检测需要用到
线段相交快速算法,以及利用
相似三角形进行camera视点、目标点的调整(具体的上述博文有详细说明
)。
void PhysicsEngine::inCollisionTest(glm::vec3 & cameraPos, glm::vec3 & targetPos) {
//后面可以在这里添加:预处理,排除当前肯定不会产生碰撞的物体
for (int i = 0; i < innerBoundaryMin.size(); i++) {
inCollisionTestWithHeight(innerBoundaryMin[i][0], innerBoundaryMin[i][1], innerBoundaryMin[i][2],
innerBoundaryMax[i][0], innerBoundaryMax[i][1], innerBoundaryMax[i][2], cameraPos, targetPos);
}
}
void PhysicsEngine::inCollisionTestWithHeight(float x1, float y1, float z1, float x2, float y2, float z2, glm::vec3 & cameraPos, glm::vec3 & targetPos) {
//当身体处于碰撞体垂直区域范围内,才进行XZ平面的碰撞检测
if (!(cameraPos[1] <= y1 || cameraPos[1] - HeroHeight >= y2)) {
inCollisionTestXZ(x1, z1, x2, z2, cameraPos, targetPos);
}
}
double Direction(dot pi, dot pj, dot pk) {
return (pk.x - pi.x)*(pj.y - pi.y) - (pj.x - pi.x)*(pk.y - pi.y);
}
bool OnSegment(dot pi, dot pj, dot pk) {
if ((min(pi.x, pj.x) <= pk.x) && (pk.x <= max(pi.x, pj.x))
&& (min(pi.y, pj.y) <= pk.y) && (pk.y <= max(pi.y, pj.y)))
return true;
else return false;
}
//检测线段相交快速算法
bool SegmentIntersect(dot p1, dot p2, dot p3, dot p4) {
int d1, d2, d3, d4;
d1 = Direction(p3, p4, p1);
d2 = Direction(p3, p4, p2);
d3 = Direction(p1, p2, p3);
d4 = Direction(p1, p2, p4);
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2>0)) && ((d3>0 && d4 < 0) || (d3 < 0 && d4>0)))
return true;
else if (d1 == 0 && OnSegment(p3, p4, p1))
return true;
else if (d2 == 0 && OnSegment(p3, p4, p2))
return true;
else if (d3 == 0 && OnSegment(p1, p2, p3))
return true;
else if (d4 == 0 && OnSegment(p1, p2, p4))
return true;
else
return false;
}
void PhysicsEngine::inCollisionTestXZ(float x1, float z1, float x2, float z2, glm::vec3 & cameraPos, glm::vec3 & targetPos) {
const float d = 2.0f;
float tarX = targetPos[0], camX = cameraPos[0], tarZ = targetPos[2], camZ = cameraPos[2];
float len = sqrt((camX - tarX)*(camX - tarX) + (camZ - tarZ)*(camZ - tarZ));
dot d1(cameraPos[0], cameraPos[2]), d2(targetPos[0], targetPos[2]);
dot d3(x1, z1), d4(x1, z2), d5(x2, z1), d6(x2, z2);
if (SegmentIntersect(d1, d2, d4, d6)) {
if (targetPos[2] < cameraPos[2]) {
printf("1\n");
//利用相似三角形原理计算,
//仅改变z坐标
targetPos[2] = z2;
cameraPos[2] += (targetPos[2] - tarZ);
}
else if (targetPos[2] > cameraPos[2]) {
printf("2\n");
cameraPos[2] = z2;
targetPos[2] += (cameraPos[2] - camZ);
}
}
else if (SegmentIntersect(d1, d2, d5, d6)) {
if (targetPos[0]<cameraPos[0]) {
printf("3\n");
targetPos[0] = x2;
cameraPos[0] += (targetPos[0] - tarX);
}
else if (targetPos[0]>cameraPos[0]) {
printf("4\n");
cameraPos[0] = x2;
targetPos[0] += (cameraPos[0] - camX);
}
}
else if (SegmentIntersect(d1, d2, d3, d5)) {
if (targetPos[2] > cameraPos[2]) {
printf("5\n");
targetPos[2] = z1;
cameraPos[2] += (targetPos[2] - tarZ);
}
else if (targetPos[2] < cameraPos[2]) {
printf("6\n");
cameraPos[2] = z1;
targetPos[2] += (cameraPos[2] - camZ);
}
}
else if (SegmentIntersect(d1, d2, d3, d4)) {
if (targetPos[0] > cameraPos[0]) {
printf("7\n");
targetPos[0] = x1;
cameraPos[0] += (targetPos[0] - tarX);
}
else if (targetPos[0] < cameraPos[0]) {
printf("8\n");
cameraPos[0] = x1;
targetPos[0] += (cameraPos[0] - camX);
}
}
}
4、接着是更新竖直方向的移动:
(1) 重力计算:利用公式 v = v0 + g * ∆t 、h = h0 + k * v * ∆t 。把公式转化为代码即可。
(2) y方向的碰撞检测:当加入重力模拟之后,我们就需要考虑以下两种情况了:玩家跳到箱子上,或者在箱子下起跳顶到箱子(上面已有的碰撞检测无法处理这两种情况)。而这两种情况也需要结合物理的知识:
前面的情况,玩家跳到箱子上时(通过当摄像机在XZ平面处于碰撞体XZ平面区域内部时,判断玩家的脚是否落到箱子顶部),提供一个方向向上的加速度(与重力加速度大小相同方向相反),速度设为0,此时Y方向没有加速度,不会下落;
后面的情况,玩家头部顶到箱子底部时(与前面情况类似判断),设置速度为0,玩家之后做自由落体运动。
//判断在xz平面,相机位置是否位于碰撞体内部
bool insideTheCollider(glm::vec3 _cameraPos, glm::vec3 _innerMin, glm::vec3 _innerMax) {
float camX = _cameraPos.x;
float camZ = _cameraPos.z;
float minX = _innerMin.x;
float minZ = _innerMin.z;
float maxX = _innerMax.x;
float maxZ = _innerMax.z;
if (minX <= camX && camX <= maxX && minZ <= camZ && camZ <= maxZ)
return true;
else
return false;
}
void PhysicsEngine::updateCameraVertMovement(glm::vec3 & cameraPos, glm::vec3 & targetPos) {
glm::vec3 acceleration = gravity + accelerUp;
velocity += acceleration * GravityFactor;
cameraPos += velocity * JumpFactor;
targetPos += velocity * JumpFactor;
//if (abs(velocity.y) < 0.1f)
// cout << "#### cameraPos.y " << cameraPos.y << endl;
//检测所有碰撞体
for (int i = 0; i < innerBoundaryMin.size(); i++) {
//如果在XZ平面进入碰撞体所在区域
if (insideTheCollider(cameraPos, innerBoundaryMin[i], innerBoundaryMax[i])) {
if (cameraPos.y - HeroHeight <= innerBoundaryMax[i][1]
&& cameraPos.y >= innerBoundaryMax[i][1]) { //脚接触到碰撞体顶部
//cout << "touch the top of collider" << endl;
isJumping = false;
accelerUp.y = -GravityAcceler;
velocity.y = 0.f;
cameraPos.y = innerBoundaryMax[i][1] + HeroHeight;
break;
}
if (cameraPos.y >= innerBoundaryMin[i][1] &&
cameraPos.y - HeroHeight <= innerBoundaryMin[i][1]) { //头接触到碰撞体底部
//cout << "touch the bottom of collider" << endl;
velocity.y = 0.f;
cameraPos.y = innerBoundaryMin[i][1];
break;
}
}
else {
accelerUp.y = 0.f;
}
}
}
5、按空格键跳跃:
此时只需改变速度和加速度即可。即加一个向上的速度,向上的加速度设为0.
void PhysicsEngine::jumpAndUpdateVelocity() {
velocity += glm::vec3(0.f, JumpInitialSpeed, 0.f);
accelerUp.y = 0.f;
}
6、以上便是整个由重力+碰撞检测构成的简单物理引擎类PhysicsEngine的大致实现过程。下面是该类的定义( PhysicsEngine.h):
#ifndef PHYSICSENGINE_H
#define PHYSICSENGINE_H
#include <glm/glm.hpp>
#include <iostream>
#include <vector>
using namespace std;
#define min(x,y) ((x) < (y) ? (x) : (y))
#define max(x,y) ((x) < (y) ? (y) : (x))
#define HeroHeight 7.5f //玩家视点到脚的高度
#define GravityAcceler -9.8f
#define MoveSpeed 0.15f //玩家移动速度
#define BoundaryGap 1.0f //碰撞间距
#define JumpInitialSpeed 12.0f //起跳初速度
#define JumpFactor 0.04f //跳起速度系数
#define GravityFactor 0.04f //下落速度系数
struct dot {
float x;
float y;
dot(float _x, float _y) :x(_x), y(_y) { }
};
class PhysicsEngine {
public:
PhysicsEngine();
~PhysicsEngine();
//设置空间外部边缘
void setSceneOuterBoundary(float x1, float z1, float x2, float z2);
//外部碰撞检测
void outCollisionTest(glm::vec3 & cameraPos, glm::vec3 & targetPos);
//设置空间内部边缘
void setSceneInnerBoundary(float x1, float y1, float z1, float x2, float y2, float z2);
//内部碰撞检测
void inCollisionTest(glm::vec3 & cameraPos, glm::vec3 & targetPos);
bool isJumping;
void jumpAndUpdateVelocity(); //按下space跳跃时调用
//每帧绘制的时候更新摄像机垂直方向移动
void updateCameraVertMovement(glm::vec3 & cameraPos, glm::vec3 & targetPos);
private:
//空间内部边缘碰撞检测(考虑高度)
void inCollisionTestWithHeight(float x1, float y1, float z1, float x2, float y2, float z2, glm::vec3 & cameraPos, glm::vec3 & targetPos);
//空间内部边缘碰撞检测(不考虑高度,即XZ平面)
void inCollisionTestXZ(float x1, float z1, float x2, float z2, glm::vec3 & cameraPos, glm::vec3 & targetPos);
//空间外部边缘碰撞检测
void outCollisionTestXZ(float x1, float z1, float x2, float z2, glm::vec3 & cameraPos, glm::vec3 & targetPos);
glm::vec3 velocity; //垂直方向速度
glm::vec3 gravity; //重力加速度
glm::vec3 accelerUp; //方向向上的加速度
glm::vec4 outerBoundary;
vector<glm::vec3> innerBoundaryMin; //碰撞器小的x/y/z坐标
vector<glm::vec3> innerBoundaryMax; //碰撞器大的x/y/z坐标
};
#endif // !PHYSICSENGINE_H
引擎类工作流程图: