本篇将会介绍一个大球的自转以及一个小球围绕大球公转的demo,效果如下图:
实现过程
如上图所示,整个项目的基本流程较之前几个例子没有太多的变化。都是:
- 初始化窗口;
- 注册各函数的监听,如 重塑函数、重绘函数等;
- 调用setupRC,初始化窗口背景、着色器管理器、顶点数据等;
- 开启glut的mainloop,类似iOS的runloop。
1、SetupRC方法
#pragma mark - 顶点数据私有方法
void vertextDataFloor() {
floorBatch.Begin(GL_LINES, 324);
/*
GL_LINES 每两个点画一条线
画法是
第一次循环
(-20,-0.5,20) 到(-20,-0.5,-20) 画一条直线
(20,-0.5,-20)到(-20,-0.5,-20)画一条直线
第二次循环
(-19.5,-0.5,20) 到(-19.5,-0.5,-20) 画一条直线
(20,-0.5,-19.5)到(-20,-0.5,-19.5)画一条直线
直到最后一次循环,闭合整个网格
*/
for (GLfloat x = -20.f; x <= 20.f; x+=0.5f) {
floorBatch.Vertex3f(-x, commonY, 20.f);
floorBatch.Vertex3f(-x, commonY, -20.f);
floorBatch.Vertex3f(20.f, commonY, x);
floorBatch.Vertex3f(-20.f, commonY, x);
}
floorBatch.End();
}
void vertextDataBigBall() {
gltMakeSphere(torusBatch, 0.5f, 40, 80);
}
void vertextDataSmallBallRotate() {
gltMakeSphere(sphereBatch, 0.2, 20, 40);
}
void vertextDataRandomSmallBall(){
for (int i = 0; i < SMALL_BALL_NUMBER; i ++) {
GLfloat randowX = ((random()%400) - 200) * 0.1;
GLfloat randowZ = ((random()%400) - 200) * 0.1;
sphereFrames[i].SetOrigin(randowX,0,randowZ);
}
}
void SetupRC() {
glClearColor(1, 1, 1, 1);
glEnable(GL_LINE_SMOOTH);
shaderManager.InitializeStockShaders();
vertextDataFloor();
vertextDataBigBall();
vertextDataSmallBallRotate();
vertextDataRandomSmallBall();
}
在这个方法中,主要做一些初始化的工作,如初始化窗口背景色、着色器管理器、顶点数据等。
2、ChangeSize方法
void ChangeSize(int nWidth, int nHeight) {
glViewport(0, 0, nWidth, nHeight);
viewFrustum.SetPerspective(35.f, float(nWidth)/float(nHeight), 1.f, 500.f);
//将投影矩阵载入投影矩阵堆栈
projectionMatrixStack.LoadMatrix(viewFrustum.GetProjectionMatrix());
//可以不调用,默认会有一个单元矩阵
modelViewMatrixStack.LoadIdentity();
transformPipeline.SetMatrixStacks(modelViewMatrixStack, projectionMatrixStack);
}
如代码所示,在这个方法中依然是常规的设置视口的位置和大小、设置投影方式、载入投影矩阵到投影矩阵堆栈、载入单元矩阵进模型视图矩阵堆栈,然后将这两个堆栈设置到变化管道对象里,方便使用和管理。
3、RenderScence方法
void RenderScence() {
//GL_STENCIL_BUFFER_BIT 在这个demo中,可以不清
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
//开启深度测试
glEnable(GL_DEPTH_TEST);
modelViewMatrixStack.PushMatrix();
//画地面
static GLfloat vBlue[] = {0.f,0.5f,1.f,1.f};
static GLfloat vBigBall[] = {0,0,1,1};
static GLfloat vSmallBall[] = {1,0.5,0,1};
static GLfloat vSmallBallRandom[] = {0,0.5,1,1};
static CStopWatch watchObj;
GLfloat angle = watchObj.GetElapsedSeconds() * 60.f;
M3DMatrix44f viewMatrix;
viewFrame.GetCameraMatrix(viewMatrix);
modelViewMatrixStack.MultMatrix(viewMatrix);
shaderManager.UseStockShader(GLT_SHADER_FLAT,transformPipeline.GetModelViewProjectionMatrix(),vBlue);
floorBatch.Draw();
//栈顶矩阵z轴负方向方向平移
modelViewMatrixStack.Translate(0, 0, -3);
//复制一份,画完大球之后,pops栈顶数据,然后栈顶数据是设置过平移的矩阵
modelViewMatrixStack.PushMatrix();
modelViewMatrixStack.Rotate(angle, 0, 1, 0);
M3DVector4f pointPosition = {0,10,5,1};
shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF,transformPipeline.GetModelViewMatrix(),
transformPipeline.GetProjectionMatrix(),pointPosition,vBigBall);
torusBatch.Draw();
modelViewMatrixStack.PopMatrix();
for (int i = 0; i < SMALL_BALL_NUMBER; i ++) {
GLFrame frame = sphereFrames[i];
modelViewMatrixStack.PushMatrix();
modelViewMatrixStack.MultMatrix(frame);
shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF,transformPipeline.GetModelViewMatrix(),
transformPipeline.GetProjectionMatrix(),pointPosition,vSmallBallRandom);
sphereBatch.Draw();
modelViewMatrixStack.PopMatrix();
}
//无需压栈,因为这是当前绘制的最后一个图形
modelViewMatrixStack.Rotate(angle * -2.f, 0, 1, 0);
//z值的绝对值越大,离大球越远。
modelViewMatrixStack.Translate(0, 0, 1.1);
shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF,transformPipeline.GetModelViewMatrix(),
transformPipeline.GetProjectionMatrix(),pointPosition,vSmallBall);
sphereBatch.Draw();
modelViewMatrixStack.PopMatrix();
glutSwapBuffers();
glutPostRedisplay();
//关闭深度测试
glDisable(GL_DEPTH_TEST);
}
这个方法是本次案例中,与之前的案例相比最大的方法。
3.1 画地板
- 清理颜色缓存区和深度缓存区,这里的模板缓存区在本demo中可以不清。
- 开启深度测试
- 栈顶矩阵copy一份压栈
- 初始化各颜色值以及一个定时器,定时器用于计算当前旋转角度
- 从观察者角色帧获取观察者矩阵,并用当前模型视图矩阵堆栈栈顶矩阵乘以观察者矩阵,得到结果覆盖栈顶矩阵
- 使用平面着色器处理数据
- 通过批次类画出地板
- 在全部图形绘制完之后,会交换缓存区
3.2 画大球
- 栈顶矩阵沿z轴负方向移动3;
- 栈顶矩阵copy一份压栈(后续计算完成出栈之后,栈顶矩阵依然是之前沿着z轴负方向移动3之后的矩阵)
- 沿着y轴旋转
- 设置光源位置
- 使用点光源着色器处理数据
- 通过三角形批次类画出大球
- 针对最近一次的入栈操作进行出栈,保证栈顶矩阵为之前沿着z轴负方向移动3之后的矩阵。
- 在全部图形绘制完之后,会交换缓存区
3.3 画多个分散的小球
在上述的SetupRC方法中调用的vertextDataRandomSmallBall方法里,做了很多顶点数据的初始化,这些就是随机小球的位置数据。
在RenderScence方法中也需要将他们绘制出来。
- 开启for循环
- 每次循环都复制一份栈顶矩阵压栈
- 将栈顶矩阵乘以当前小球的角色帧数据,赋值给栈顶数据
- 使用点光源着色器处理数据
- 使用三角形批次类进行绘制
- 将每次循环的栈顶矩阵出栈
- 在全部图形绘制完之后,会交换缓存区
3.4 画公转的小球
- 栈顶矩阵绕y轴旋转一定的角度,角度根据当前的计时器的时间来计算
- 栈顶矩阵沿z轴平移操作,无论正负,绝对值越大,离大球越远
- 使用点光源着色器处理数据
- 使用三角形批次类进行绘制
- 将当前栈顶矩阵出
3.5 收尾
- 交换缓存区
- 提交重新渲染,保证重复调用RenderScence方法,形成旋转动画
- 关闭深度测试
总结:
RenderScence方法中,会有各种矩阵变换的计算操作,将原始的顶点数据,经过各种变换,全部到达一个新的位置,最终实现我们要的效果。在计算的过程中,需要注意的几个点:
- 整体的入栈和出栈要成对出现,有入栈就必须有出栈,否则下次绘制的时候数据会错乱
- 对于整个变换过程,拿公转小球举例,虽然它是经过了
modelViewMatrixStack.Translate(0, 0, -3);
modelViewMatrixStack.Rotate(angle * -2.f, 0, 1, 0);
modelViewMatrixStack.Translate(0, 0, 1.1);
这三部,但是在OpenGL实现的时候,由于OpenGL采用的是右乘的方式,所以,其实小球是经过了先z轴平移,再旋转,再z轴平移的操作。
而对于大球来说,它经过了
modelViewMatrixStack.Translate(0, 0, -3);
modelViewMatrixStack.PushMatrix();
modelViewMatrixStack.Rotate(angle, 0, 1, 0);
这三部操作,而OpenGL实现的时候,其实大球是先旋转,再z轴平移。
因此,对于大球来说,它是自转,而对于小球来说,它是公转。
最终总结
上述为个人实现与总结,如有错漏之处,欢迎并感谢批评指正。