美文网首页
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