美文网首页
03源码--004--综合案例:太阳系

03源码--004--综合案例:太阳系

作者: 修_远 | 来源:发表于2020-07-20 13:43 被阅读0次

    效果图:

    太阳系
    • 大球:要求自转
    • 小球:要求围绕大球转

    这个案例将是对前面所有的知识的一个汇总使用。程序员总是很熟练将大问题拆分成各种小问题:

    • 画地板
    • 画一个大球
    • 自转:让大球转起来(需要用到 OpenGL 中的定时器)
    • 画多个小球
    • 公转:让小球围绕大球转起来
    • 移动:从不同的角度去观察这个星系

    [TOC]

    画地板

    地板
    • SetupRC:初始化地板数据
    floorBatch.Begin(GL_LINES, 324);
    for(GLfloat x = -20.0; x <= 20.0f; x+= 0.5) {
        floorBatch.Vertex3f(x, -0.55f, 20.0f);
        floorBatch.Vertex3f(x, -0.55f, -20.0f);
        floorBatch.Vertex3f(20.0f, -0.55f, x);
        floorBatch.Vertex3f(-20.0f, -0.55f, x);
    }
    floorBatch.End();
    
    • RenderScene:绘制地板
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vFloorColor);
        floorBatch.Draw();
    

    移动

    移动后的地板

    按照一般思路,我们先绘制好各种图形,最后才让坐标系动起来,但是在上面绘制地板的时候已经使用到了矩阵,所以对于矩阵变换更加偏向于在前期的一个准备工作。

    文章 02总结--010--OpenGL 基础变换:向量和矩阵的深入理解【重点】 中已经非常详细地介绍了矩阵变化中的案例

    这里需要用到的矩阵和上面这篇文章中的矩阵用法一模一样,这里就直接贴代码了。

    1. 初始化投影矩阵、变换管道
    void ChangeSize(int w, int h) {
        // 1. 设置视口
        glViewport(0, 0, w, h);
        
        // 2. 创建投影矩阵
        viewFrustum.SetPerspective(35.0f, float(w/h), 1.0f, 100.0f);
        projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
        
        // 变换管道设置2个矩阵堆栈
        transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
    }
    
    1. 使用矩阵:RenderScene
    • 获取观察者矩阵
    modelViewMatrix.PushMatrix();
    M3DMatrix44f mCamera;
    cameraFrame.GetCameraMatrix(mCamera);
    
    • 使用观察者矩阵
    modelViewMatrix.PushMatrix(mCamera);
    
    • 使用MVP矩阵
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vFloorColor);
    
    • 监听观察者矩阵的变化
    void SpecialKeys(int key, int x, int y) {
        static float linear = 0.1f;
        static float angular = float(m3dDegToRad(5.0f));
        
        if (key == GLUT_KEY_UP) {
            cameraFrame.MoveForward(linear);
        }
        if (key == GLUT_KEY_DOWN) {
            cameraFrame.MoveForward(-linear);
        }
        
        if (key == GLUT_KEY_LEFT) {
            cameraFrame.RotateWorld(angular, 0, 1, 0);
        }
        if (key == GLUT_KEY_RIGHT) {
            cameraFrame.RotateWorld(-angular, 0, 1, 0);
        }
    }
    

    按照一般流程,这里在处理特殊键位之后是需要提交重绘 glutPostRedisplay(); 的,但是,这个案例中使用了定时器的操作,将重绘的函数放到了 RenderScene 函数中执行。

    这个流程是处理矩阵变换的一个基础流程,一定要牢记于心!

    画一个大球

    大球

    有了前面的基础,绘制一个大球简直太简单了

    • SetupRC:设置大球数据
    // 2. 设置大球
    gltMakeSphere(torusBatch, 0.4f, 40, 80);
    
    • RenderScene:绘制大球
    // 2. 绘制大球
    shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF,
                                 transformPipeline.GetModelViewMatrix(),
                                 transformPipeline.GetProjectionMatrix(),
                                 vLightPos,
                                 vTorusColor);
    torusBatch.Draw();
    

    细心的你会发现大球下半部有阴影,有阴影肯定是因为有光照嘛,因为这里使用的是 GLT_SHADER_POINT_LIGHT_DIFF 点光源着色器。

    到这一步,你的大球是显示不出来的。效果如下:

    大球无法显示

    这是什么原因造成的呢?回顾一下我们的观察者,此时的观察者在原点,大球在原点,用一句诗来解释——“不识庐山真面目,只缘身在此山中”

    如何解决呢?让观察者“出来”一点

    • 方法一:SetupRC中,cameraFrame.MoveForward(-3.f);
    • 方法二:RenderScene中,在绘制大球之前,让模型视图矩阵发生平移,modelViewMatrix.Translate(0.0f, 0.0f, -3.0f);

    自转

    在之前的案例中,所有的动作都是由键盘发出的,只有当我们触发了特殊键位,才会重新渲染,如果要实现自动旋转,肯定是需要定时器的。

    OpenGL中的“定时器”:CStopWatch

    static CStopWatch    rotTimer;
    float yRot = rotTimer.GetElapsedSeconds() * 6.0f;
    

    GetElapsedSeconds:获取程序运行的时间,s为单位。下面是输出的值。

    elapsed time : 0.046123
    elapsed time : 0.049504
    elapsed time : 0.061152
    elapsed time : 0.062365
    elapsed time : 0.069542
    elapsed time : 0.071048
    elapsed time : 0.087788
    elapsed time : 0.104196
    

    获取旋转的角度:float yRot = rotTimer.GetElapsedSeconds() * 6.0f;

    使用旋转角度:modelViewMatrix.Rotate(yRot, 0.0f, 1.0f, 0.0f);

    ???定时任务呢?这里的定时任务不同于我们在iOS开发中创建一个定时器,然后启动一个定时器的实现逻辑,而是借助了 glutMainLoop() 的机制。

    image
    void RenderScene(void) {
        ……
        // 交换缓存区 - 显示
        glutSwapBuffers();
        // 提交重新渲染 - 会再次触发 RenderScene 方法, 造成一直刷新的效果
        glutPostRedisplay();
    }
    
    • glutSwapBuffers:交换缓存区,显示当前缓冲区里面的内容
    • glutPostRedisplay:提交重绘,会再次触发 RenderScene 方法, 造成一直刷新的效果

    画多个小球

    大球、小球,都是球,不同的只是位置和数量

    • SetupRC:设置小球数据
    // 3. 设置小球
    gltMakeSphere(sphereBatch, 0.1f, 13, 26);
    
    for (int i=0; i<NUM_SPHERES; i++) {
        //y轴不变,X,Z产生随机值
        GLfloat x = ((GLfloat)((rand() % 400) - 200 ) * 0.1f);
        GLfloat z = ((GLfloat)((rand() % 400) - 200 ) * 0.1f);
    
        //在y方向,将球体设置为0.0的位置,这使得它们看起来是飘浮在眼睛的高度
        //对spheres数组中的每一个顶点,设置顶点数据
        spheres[i].SetOrigin(x, 0.0f, z);
    }
    
    • RenderScene:绘制小球
    for (int i=0; i<NUM_SPHERES; i++) {
        modelViewMatrix.PushMatrix();
        // 让所有小球动起来
        modelViewMatrix.Rotate(yRot * -1.2f, 0.0f, 1.0f, 0.0f);
        modelViewMatrix.Translate(0.2f, 0.0f, 0.0f);
        modelViewMatrix.MultMatrix(spheres[I]);
    
        shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF,
        transformPipeline.GetModelViewMatrix(),
        transformPipeline.GetProjectionMatrix(),
        vLightPos,
        vSpereColor);
    
        sphereBatch.Draw();
        modelViewMatrix.PopMatrix();
    }
    

    真正绘制部分,这部分代码跟绘制大球的地方是一模一样的,没啥好说的

    shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF,
    transformPipeline.GetModelViewMatrix(),
    transformPipeline.GetProjectionMatrix(),
    vLightPos,
    vSpereColor);
    
    sphereBatch.Draw();
    

    矩阵堆栈的 push 和 pop 已经讲过很多次了

    modelViewMatrix.PushMatrix();
    modelViewMatrix.PopMatrix();
    

    矩阵的旋转和平移也很熟悉了

    modelViewMatrix.Rotate(yRot * -1.2f, 0.0f, 1.0f, 0.0f);
    modelViewMatrix.Translate(0.2f, 0.0f, 0.0f);
    

    最后发现只有这一行代码是不一样的。按照我们以往的经验,一个图形应该要对应一个批次处理类,如果要画50个球,那应该需要50个批次类,但实际上,只使用了一种。

    modelViewMatrix.MultMatrix(spheres[I]);
    

    下面来重新理解批次类

    sphereBatch.Draw();:这是一个绘制的动作,在现实生活中,执行绘制动作的有一个称呼——画笔。如果批次类是画笔,那画的内容从哪里来呢?在 SetupRC 函数中,我们将所有小球的数据(GLFrame)存在了 spheres 数组中。

    modelViewMatrix.MultMatrix(spheres[i]);:这是一个矩阵乘法,将小球的数据放入当前的模型视图矩阵中,也就是相当于给当前画笔需要绘制的内容中添加了一个小球的内容。最后在调用 Draw 函数时,将MVP矩阵中的内容绘制到屏幕上。

    公转

    如果上面的内容都已经理解,那么小球的公转其实就显得很简单了。都是绕着y轴旋转

    modelViewMatrix.Rotate(yRot * -1.2f, 0.0f, 1.0f, 0.0f);
    

    总结

    1. cameraFrame.MoveForward(-3.f);,根据需求设置观察者视角的位置
    2. 批次类实际上是一个画笔

    相关文章

      网友评论

          本文标题:03源码--004--综合案例:太阳系

          本文链接:https://www.haomeiwen.com/subject/winekktx.html