OpenGL实现碰撞检测与模拟重力效果(简单的物理系统)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_33000225/article/details/72861956

最近在做一个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





引擎类工作流程图:


         










猜你喜欢

转载自blog.csdn.net/qq_33000225/article/details/72861956