效果图:

- 大球:要求自转
- 小球:要求围绕大球转
这个案例将是对前面所有的知识的一个汇总使用。程序员总是很熟练将大问题拆分成各种小问题:
- 画地板
- 画一个大球
- 自转:让大球转起来(需要用到 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 基础变换:向量和矩阵的深入理解【重点】 中已经非常详细地介绍了矩阵变化中的案例
这里需要用到的矩阵和上面这篇文章中的矩阵用法一模一样,这里就直接贴代码了。
- 初始化投影矩阵、变换管道
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);
}
- 使用矩阵: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()
的机制。

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);
总结
-
cameraFrame.MoveForward(-3.f);
,根据需求设置观察者视角的位置 - 批次类实际上是一个画笔
网友评论