OpenGL 室内3D弹球(遇到房间六壁反弹)
实现过程
项目目标
基于OpenGL设计一个房间和2个圆形的弹球,仿真圆球在弹到房间六壁时作出的回弹动画。
包括但不限于:
1)绘制房间的结构,需要尽量的真实和漂亮
2)弹球可以用不同的颜色或者纹理标识,以不同的速度运动
5)逻辑合理
6)弹球可以慢慢的根据摩擦力慢慢的停止(考虑停止的条件)
7)考虑到真实环境下的摩擦力的情况,摩擦力的存在,球的弹力在衰减,速度也在衰减,从来运动越来越慢,以至于停止
8)球可以回弹,左右,前后运动;
先放一下结果图给你们看看
流程介绍
用OpenCV从计算机内部读取一张图片,做为房间材质图如下图:
设置灯光属性以及材质属性
搭建房间
房间设计图如下
如上图所示,房间原点设在坐标系的(0,0,0),整体为一个777的立方体。
在X,Y,Z轴正方向延申。将正方体的前后左右以及顶部设置成之前图片所示的材质。将立方体的底部设置为白灰相间的棋盘
规划小球运动并生成小球 这部分是重点 ,过会儿我会详细解释
小球的运动是一个合运动,分为三个部分:竖直的Y方向运动,以及水平的X、Z方向运动。上图为小球碰撞到边界时的示意图,撞到边界后小球的运动分为三种情况,分别对应不同的解决方案。
撞到上下平面,水平方向的运动情况不变,竖直方向运动方向变反
撞到左右平面,Y轴,Z轴方向运动不变,X轴运动方向变反
撞到前后平面,Y轴,X轴方向运动不变,Z轴运动方向变反
同时,重要的是模拟现实中的物理规律,其中小球需要在Y轴竖直方向受重力影响做自由落体运动,弹起后做自由落体反运动。在水平方向做近似匀速直线运动。为小球添加摩擦力,使得小球最后能停下来。写完运动函数后,生成小球。
搭建摄像机
摄像机路线如下
如图为顶视图,蓝色圈为轨道,蓝色圈外为房间四壁。
需要做一个环绕拍摄轨道,把摄像机固定在轨道上,让摄像机围绕房间的圆心做环形运动,以此把整个房间的内容收入眼底。为此,写出这个圆形轨道的参数方程,坐标以θ为自变量进行变换。
刷新
设置计时器函数,每1/50秒刷新一次屏幕
关键代码及其解释
1.设置材质添加光照
//OpenCV读取图像
Mat I = imread("D://VR.jpg");
int width = I.cols;
int height = I.rows;
GLubyte* otherImage;
static GLubyte checkImage[checkImageHeight][checkImageWidth][4];
void makeCheckImages(void) {
int i, j, c;
for (i = 0; i < checkImageHeight; i++) {
for (j = 0; j < checkImageWidth; j++) {
c = ((((i & 0x8) == 0) ^ ((j & 0x8)) == 0)) * 255;
checkImage[i][j][0] = (GLubyte)c;
checkImage[i][j][1] = (GLubyte)c;
checkImage[i][j][2] = (GLubyte)c;
checkImage[i][j][3] = (GLubyte)255;
}
}
//加载外部图片
int pixelLength = width * height * 3;
otherImage = new GLubyte[pixelLength];
memcpy(otherImage, I.data, pixelLength * sizeof(char));
}
void init() {
//允许深度测试
glEnable(GL_DEPTH_TEST);
//设置散射和镜像反射为白光
glLightfv(GL_LIGHT0, GL_DIFFUSE, WHITE);
glLightfv(GL_LIGHT0, GL_SPECULAR, WHITE);
//设置前表面的高光镜像反射为白光
glMaterialfv(GL_FRONT, GL_SPECULAR, WHITE);
//设置前表面散射光反光系数
glMaterialf(GL_FRONT, GL_SHININESS, 30);
//允许灯光
glEnable(GL_LIGHTING);
//打开0#灯
glEnable(GL_LIGHT0);
}
在display函数中刷新材质设置
makeCheckImages();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glGenTextures(2, texName);
glBindTexture(GL_TEXTURE_2D, texName[1]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, checkImageWidth,
checkImageHeight, 0, GL_RGB, GL_UNSIGNED_BYTE,
otherImage);
glEnable(GL_TEXTURE_2D);
2.搭建房间
用四边形函数创建五个四边形,做为房间的顶部和前后左右四壁,并把墙的材质附上去
//顶盘
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBindTexture(GL_TEXTURE_2D, texName[1]);
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex3f(0.0, 7, 0.0);
glTexCoord2f(0.0, 1.0);
glVertex3f(7.0, 7, 0.0);
glTexCoord2f(1.0, 1.0);
glVertex3f(7.0, 7, 7.0);
glTexCoord2f(1.0, 0.0);
glVertex3f(0.0, 7, 7.0);
glEnd();
//前后左右
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex3f(0.0, 7, 0.0);
glTexCoord2f(0.0, 1.0);
glVertex3f(7.0, 7, 0.0);
glTexCoord2f(1.0, 1.0);
glVertex3f(7.0, 0, 0);
glTexCoord2f(1.0, 0.0);
glVertex3f(0.0, 0, 0);
glEnd();
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex3f(0.0, 7, 7);
glTexCoord2f(0.0, 1.0);
glVertex3f(7.0, 7, 7);
glTexCoord2f(1.0, 1.0);
glVertex3f(7.0, 0, 7);
glTexCoord2f(1.0, 0.0);
glVertex3f(0.0, 0, 7);
glEnd();
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex3f(0.0, 0, 0.0);
glTexCoord2f(0.0, 1.0);
glVertex3f(0, 0, 7);
glTexCoord2f(1.0, 1.0);
glVertex3f(0, 7, 7);
glTexCoord2f(1.0, 0.0);
glVertex3f(0.0, 7, 0);
glEnd();
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex3f(7, 0, 0.0);
glTexCoord2f(0.0, 1.0);
glVertex3f(7, 0, 7);
glTexCoord2f(1.0, 1.0);
glVertex3f(7, 7, 7);
glTexCoord2f(1.0, 0.0);
glVertex3f(7, 7, 0);
glEnd();
做一个灰白相间的地板
//棋盘
GLfloat lightPosition[] = { 4, 3, 7, 1 };
//设置光源位置
glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);
//开始绘制四边形
glBegin(GL_QUADS);
//法向量方向
glNormal3d(0, 1, 0);
for (int x = 0; x < 7; x++) {
for (int z = 0; z < 7; z++) {
//设置每个格子的材质属性
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE,
(x + z) % 2 == 0 ? GRAY : WHITE);
//四边形的4个点坐标
glVertex3d(x, 0, z);
glVertex3d(x + 1, 0, z);
glVertex3d(x + 1, 0, z + 1);
glVertex3d(x, 0, z + 1);
}
}
glEnd();
3.规划小球运动并生成小球详细解释以及代码
先说一下运动机制
依赖OpenGL的 glTranslated函数来控制小球的生成位置
程序运行时每1/50秒刷新一次位置函数,在当前的xyz处生成小球
用运动函数来控制xyz的值,从而实现位置变化,根据变化的位置生成小球从而达到小球运动的效果
总而言之,每秒更新50次位置,在这50个位置上生成50个小球从而达到动画效果
小球运动
小球的运动是一个合运动,分为三个部分:竖直的Y方向运动,以及水平的X、Z方向运动。上图为小球碰撞到边界时的示意图,撞到边界后小球的运动分为三种情况,分别对应不同的解决方案。
撞到上下平面,水平方向的运动情况不变,竖直方向运动方向变反
撞到左右平面,Y轴,Z轴方向运动不变,X轴运动方向变反
撞到前后平面,Y轴,X轴方向运动不变,Z轴运动方向变反
同时,重要的是模拟现实中的物理规律,其中小球需要在Y轴竖直方向受重力影响做自由落体运动,弹起后做自由落体反运动。在水平方向做近似匀速直线运动。为小球添加摩擦力,使得小球最后能停下来。写完运动函数后,生成小球。
每秒更新50次位置,每0.02s更新一下速度
先来说说简单的三个方向中的水平维度运动
在现实世界中,我们在空中水平方向运动时只收到摩擦力的影响,在速度不快的情况下近似做匀速运动,此时的摩擦力很小可以忽略不计。那么放到程序里就是s=v*t,s就是x,z的坐标,v是当前速度,t是运动时间
t += 0.002;
x += directionx * vx * t;
z += directionz * vz * t;
vx *= 0.99879;
vz *= 0.99879;
那么问题来了,我们做的是弹球,房间是有边界的,弹球撞到房间六壁应该回弹,而不是只有一个方向的运动。所以我们还要做边界检测,房间六壁,每一面都要有检测。
如何做边界检测呢?
小球在三个坐标轴上的运动距离应该被加以限制:为了不穿模,应该限制在7(房间的边长就是7)-2r(2倍半径)内,在小球的边界碰到墙壁边界时,立刻做出回应。
碰到边界后,把运动方向分解一下就知道,其中一个方向变为原来的反方向,另外两个方向不变
撞到上下平面,水平方向的运动情况不变,竖直方向运动方向变反
撞到左右平面,Y轴,Z轴方向运动不变,X轴运动方向变反
撞到前后平面,Y轴,X轴方向运动不变,Z轴运动方向变反
把这些结论写成代码就是
//小球在X轴方向的运动
if (x > (xmax - radius)) {
x = (xmax - radius);
directionx = -1;
vx *= 0.99;
t = 0.02;
}
else if (x < radius) {
x = radius;
directionx = 1;
t = 0.02;
vx *= 0.99;
}
//小球在Z轴方向的运动
if (z > (zmax - radius)) {
z = (zmax - radius);
directionz = -1;
vz *= 0.99;
t = 0.02;
}
else if (z < radius) {
z = radius;
directionz = 1;
vz *= 0.99;
t = 0.02;
}
direction就是方向,三个分量都有自己的方向变量。为了模拟摩擦力,小球每0.02秒,速度损失0.121%
每撞到一次边界,速度损失1%。每次撞到墙后,时间清零,把撞墙位置当作新的起始位置,方便运动学函数的计算。
再来看较为复杂的竖直方向运动
竖直方向的运动就需要模拟重力加速度了,这部分其实不需要重力,因为我没有做能量守恒,而且重力加速度跟重力没有关系。给定一个全局变量g=9.8
写出竖直方向的运动公式,根据我们高中的运动学公式s=Vt+0.5att
v是当前速度,t是累计时间,a就是g
设定下落为正速度,弹起为反速度
下面是代码,else里面加了新的选择分支,那个分枝是什么呢?
假如竖直方向速度太小了,那剩余的能量不足以支持下一次回弹,所以此时的小球牢牢地待在了地面上,同时在水平维度开始受滚动摩擦力的约束,速度进一步减小,直至为0
if (vy > 0)//vy大于0,下落
{
y = y - (vy * t + directiony * (0.5 * g * t * t));
}
else//否则回弹
{
if (statusy == 1)//此时弹球已经触底,由于速度太小,无法再竖直方向运动了
{
y = radius;//弹球不在弹起
//弹球开始受滚动摩擦力
vx *= 0.99;
vz *= 0.99;
if (abs(vx) < 1.7)
{
vx = 0;
}
if (abs(vz) < 1.7)
{
vz = 0;
}
}
else
y += abs(vy) * t + (0.5 * g * t * t);
}
在竖直方向上也要进行边界检测
同时,如果触底时速度太低,status置1,表示小球不再弹起
if (y > maximumHeight) //假如初速太快,撞到房顶了
{
y = maximumHeight - 0.01;
t = 0.002;
vy = -1 * vy * 0.95;
t = 0;
}
if (y <= radius) //触底
{
y = radius;
t = 0.002;
vy = -1 * vy * 0.95;
if (abs(vy) < 0.75)//如果速度太小,在竖直方向停止运动
{
statusy = 1;
}
}
4.添加摄像机
键盘上的上下左右键被监听,用于调整摄像机,左右键使得摄像机在一个圆轨道上运动
class Camera {
public:
double theta; //确定x和z的位置
double y; //y位置
double dTheta; //角度增量
double dy; //上下y增量
public:
//类构造函数—默认初始化用法
Camera() : theta(0), y(2), dTheta(0.02), dy(0.2) {}
//类方法
double getX() { return (3.5 + 3.5 * cos(theta)); }
double getY() { return y; }
double getZ() { return (3.5 + 3.5 * sin(theta)); }
void moveRight() { theta += dTheta; }
void moveLeft() { theta -= dTheta; }
void moveUp() { y += dy; }
void moveDown() { if (y > dy) y -= dy; }
};
5.其他重要函数
//键盘处理函数
void onKey(int key, int, int) {
//按键:上下左右
switch (key) {
case GLUT_KEY_LEFT: camera.moveLeft(); break;
case GLUT_KEY_RIGHT: camera.moveRight(); break;
case GLUT_KEY_UP: camera.moveUp(); break;
case GLUT_KEY_DOWN: camera.moveDown(); break;
}
glutPostRedisplay();
}
//自定义计时器函数
void timer(int v) {
//当计时器唤醒时所调用的函数
glutPostRedisplay();
//设置下一次计时器的参数
glutTimerFunc(1000 / 50, timer/*函数名*/, v);
}
//窗口调整大小时调用的函数
void reshape(GLint w, GLint h) {
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(80.0, GLfloat(w) / GLfloat(h), 1, 15);
glMatrixMode(GL_MODELVIEW);
}
运行结果
总结
代码太多了放不全,我这个是opencv+OpenGL,材质部分用到了opencv读图
这个最重要的就是小球的运动部分,我把这部分代码完整放出来
//每帧移动0.05单位
class Ball {
//类的属性
double radius;
GLfloat* color;
double maximumHeight;
double x;
double y;
double z;
int directionx; //方向
int directiony;
int directionz;
double mass;
double GravitionalEnergy;
double unitx = 0.05;
double unity = 0.05;
double unitz = 0.05;
double vx = 10;
double vy = 40;
double vz = 15;
int statusy = 0;
double t = 0;
public:
//构造函数
Ball(double r, GLfloat* c, double x, double y, double z) :
radius(r), color(c), maximumHeight(ymax), directionx(1),
directiony(1), directionz(1), y(y), x(x), z(z), mass((4 / 3)* PI* r* r* r), GravitionalEnergy(mass* g* y) {
}
//更新和绘制方法
void update() {
//弹球运动
//设置初速度,速度为矢量,分解到三个方向
t += 0.002;
x += directionx * vx * t;
z += directionz * vz * t;
vx *= 0.99879;
vy = vy + g * t;
vz *= 0.99879;
//以下为小球在竖直方向的运动公式
if (vy > 0)//vy大于0,下落
{
y = y - (vy * t + directiony * (0.5 * g * t * t));
}
else//否则回弹
{
if (statusy == 1)//此时弹球已经触底,由于速度太小,无法再竖直方向运动了
{
y = radius;//弹球不在弹起
//弹球开始受滚动摩擦力
vx *= 0.99;
vz *= 0.99;
if (abs(vx) < 1.7)
{
vx = 0;
}
if (abs(vz) < 1.7)
{
vz = 0;
}
}
else
y += abs(vy) * t + (0.5 * g * t * t);
}
if (y > maximumHeight) //假如初速太快,撞到房顶了
{
y = maximumHeight - 0.01;
t = 0.002;
vy = -1 * vy * 0.95;
t = 0;
}
if (y <= radius) //触底
{
y = radius;
t = 0.002;
vy = -1 * vy * 0.95;
if (abs(vy) < 0.75)//如果速度太小,在竖直方向停止运动
{
statusy = 1;
}
}
//小球在X轴方向的运动
if (x > (xmax - radius)) {
x = (xmax - radius);
directionx = -1;
vx *= 0.99;
t = 0.02;
}
else if (x < radius) {
x = radius;
directionx = 1;
t = 0.02;
vx *= 0.99;
}
//小球在Z轴方向的运动
if (z > (zmax - radius)) {
z = (zmax - radius);
directionz = -1;
vz *= 0.99;
t = 0.02;
}
else if (z < radius) {
z = radius;
directionz = 1;
vz *= 0.99;
t = 0.02;
}
glPushMatrix();
//单独设置每个球的材质参数
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color);
glTranslated(x, y, z);
//创建球
glutSolidSphere(radius, 30, 30);
glPopMatrix();
}
};
完整版代码加我QQ812515674随叫随到