【OpenGL】室内3D弹球

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随叫随到

  • 7
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值