OpenGL(九)-- 综合案例(公、自转)
相信学习过OpenGL的同学应该过玩过这个经典案例:
总和案例.gif
通过观察这个案例中有三部分:
- 地板
- 自转大球
- 公转小球
这篇文章中会省略一部分基本的初始化代码,而且代码都是按模块进行了分割,如果想要了解可以去另一篇文章中了解一下OpenGL (三)--一个"HelloWorld"的执行全过程,也可以直接下载源码来看github
下面就按照这个顺序贴上代码 个人理解:
- 全局属性
//投影视图矩阵堆栈
GLMatrixStack projectMatrix;
//模型视图矩阵堆栈
GLMatrixStack modelViewMatrix;
//mvp变换管道
GLGeometryTransform transform;
//固定着色器管理类
GLShaderManager shaderManager;
//参考帧
//观察者帧
GLFrame cameraFrame;
//批次类
//地板
GLBatch floorBatch;
//大球
GLTriangleBatch sphereBatch;
//小球
GLTriangleBatch sphereSmallBatch;
//纹理标记数组
//纹理对象
GLuint texture[3];
- 创建球、地板模型
void setupRC() {
//地板
//使用三角形图元装配
GLfloat texSize = 10.0f;
floorBatch.Begin(GL_TRIANGLE_FAN, 4, 1);
floorBatch.MultiTexCoord2f(0, 0.0f, 0.0f);
floorBatch.Vertex3f(-20.f, -0.41f, 20.0f);
floorBatch.MultiTexCoord2f(0, texSize, 0.0f);
floorBatch.Vertex3f(20.0f, -0.41f, 20.f);
floorBatch.MultiTexCoord2f(0, texSize, texSize);
floorBatch.Vertex3f(20.0f, -0.41f, -20.0f);
floorBatch.MultiTexCoord2f(0, 0.0f, texSize);
floorBatch.Vertex3f(-20.0f, -0.41f, -20.0f);
floorBatch.End();
//大球
//iSlices 将图形分为多少片
//iStacks 将每一层分为多少的图元三角形
//这两个参数越大图像越细腻;一般规律iStacks是iSlices的两倍
gltMakeSphere(sphereBatch, 0.4f, 40, 80);
//小球
gltMakeSphere(sphereSmallBatch, 0.1f, 26, 13);
//计算小球随机位置
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);
spheresLocation[i].SetOrigin(x, 0.0f, z);
}
}
- 球通过OpenGL提供的固定模型来创建,纹理坐标是系统来创建
- 地板通过三角形图元装配来来创建
- MultiTexCoord2f通过该方法来映射地板的纹理坐标
- 绑定纹理对象
void setupRC() {
//绑定纹理对象
glGenTextures(3, texture);
glBindTexture(GL_TEXTURE_2D, texture[0]);
//9.将TGA文件加载为2D纹理。
//参数1:纹理文件名称
//参数2&参数3:需要缩小&放大的过滤器
//参数4:纹理坐标环绕模式
LoadTGATexture("marble.tga", GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_REPEAT);
}
- 项目涉及到3个纹理,所以创建3个纹理对象保存到texture数组中。
- 绑定纹理(LoadTGATexture)
bool LoadTGATexture(const char *szFileName, GLenum minFilter, GLenum magFilter, GLenum wrapMode)
{
GLbyte *pBits;
int iWidth,iHeight,iComponents;
GLenum eFormat;
//1.读取纹理数据
pBits = gltReadTGABits(szFileName, &iWidth, &iHeight, &iComponents, &eFormat);
if (pBits == NULL) {return false;}
//参数1:纹理维度
//参数2:线性过滤
//参数3:wrapMode,环绕模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
//2、设置纹理参数
//参数1:纹理维度
//参数2:为S/T坐标设置模式
//参数3:wrapMode,环绕模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode);
//3.载入纹理
//参数1:纹理维度
//参数2:mip贴图层次
//参数3:纹理单元存储的颜色成分(从读取像素图是获得)-将内部参数nComponents改为了通用压缩纹理格式GL_COMPRESSED_RGB
//参数4:加载纹理宽
//参数5:加载纹理高
//参数6:加载纹理的深度
//参数7:像素数据的数据类型(GL_UNSIGNED_BYTE,每个颜色分量都是一个8位无符号整数)
//参数8:指向纹理图像数据的指针
glTexImage2D(GL_TEXTURE_2D, 0, iComponents, iWidth, iHeight, 0, eFormat, GL_UNSIGNED_BYTE, pBits);
free(pBits);
//只有minFilter 等于以下四种模式,才可以生成Mip贴图
//GL_NEAREST_MIPMAP_NEAREST具有非常好的性能,并且闪烁现象非常弱
//GL_LINEAR_MIPMAP_NEAREST常常用于对游戏进行加速,它使用了高质量的线性过滤器
//GL_LINEAR_MIPMAP_LINEAR 和GL_NEAREST_MIPMAP_LINEAR 过滤器在Mip层之间执行了一些额外的插值,以消除他们之间的过滤痕迹。
//GL_LINEAR_MIPMAP_LINEAR 三线性Mip贴图。纹理过滤的黄金准则,具有最高的精度。
if(minFilter == GL_LINEAR_MIPMAP_LINEAR ||
minFilter == GL_LINEAR_MIPMAP_NEAREST ||
minFilter == GL_NEAREST_MIPMAP_LINEAR ||
minFilter == GL_NEAREST_MIPMAP_NEAREST)
//4.加载Mip,纹理生成所有的Mip层
//参数:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
glGenerateMipmap(GL_TEXTURE_2D);
return true;
}
- 当前载入的纹理保存到当前绑定的纹理对象中。时刻记着:OpenGL是一个巨大的状态机。OpenGL没有对象的概念,是面向过程的编程方式,根据代码执行的顺序完成赋值操作。
- 绘制基本设置
void RenderScene(void) {
// 重置颜色、深度缓存区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 定时器
static CStopWatch rotTimer;
// 旋转角度
float yRot = rotTimer.GetElapsedSeconds()*60.0f;
// 观察者矩阵压栈
M3DMatrix44f cameraMatrix;
cameraFrame.GetCameraMatrix(cameraMatrix);
modelViewMatrix.PushMatrix(cameraMatrix);
}
- 定时器纯粹是为了记录累计时间,并不会导致动画(重绘)。
- 旋转角度会根据时间一直增加,物体的旋转其实每次都是从起始位置重新计算并渲染的,这一点和iOS中的动画还是有一些区别。
- 对于压栈、出栈在OpenGL(五)-- OpenGL中矩阵的变换会有详细的解释。
绘制自转大球
代码语言:javascript复制void drawSomething(GLfloat yRot){
//压栈
modelViewMatrix.PushMatrix();
//旋转
modelViewMatrix.Rotate(yRot, 0, 1, 0);
//绑定对应的纹理
glBindTexture(GL_TEXTURE_2D, texture[1]);
//使用纹理-光源着色器绘制
shaderManager.UseStockShader(GLT_SHADER_TEXTURE_POINT_LIGHT_DIFF, modelViewMatrix.GetMatrix(), transform.GetProjectionMatrix(), vLightPos, vWhite, 0);
sphereBatch.Draw();
modelViewMatrix.PopMatrix();
}
- 在3D模型中自转是根据y轴旋转的所以是:(角度, x, y, x) -> (yRot, 0, 1, 0)
- 因为旋转是针对大球来设置的,所以设置后需要将大球的设置从
modelViewMatrix
进行PopMatrix
,保证小球的设置正确。
绘制公转小球
代码语言:javascript复制void drawSomething(GLfloat yRot){
//压栈
modelViewMatrix.PushMatrix();
//先旋转在移动
modelViewMatrix.Rotate(yRot * -2.0f, 0, 1, 0);
modelViewMatrix.Translate(0.8f, 0, 0);
//绑定对应的纹理
glBindTexture(GL_TEXTURE_2D, texture[2]);
//使用固定着色器-光源着色器绘制
shaderManager.UseStockShader(GLT_SHADER_TEXTURE_POINT_LIGHT_DIFF, modelViewMatrix.GetMatrix(), transform.GetProjectionMatrix(), vLightPos, vWhite, 0);
sphereSmallBatch.Draw();
modelViewMatrix.PopMatrix();
}
- 小球先旋转,旋转后导致物体的方向向量的角度也发生了变化,所以x轴的移动会按照向量方向进行。 如果调整这两句代码的顺序就会得到这样的结果:
modelViewMatrix.Translate(0.8f, 0, 0);
modelViewMatrix.Rotate(yRot * -2.0f, 0, 1, 0);
- 先移动并不会改变物体的方向向量,也就无法完成公转的效果。
绘制镜面部分
代码语言:javascript复制void RenderScene(void) {
//绘制镜面
modelViewMatrix.PushMatrix();
//按照Y轴翻转
modelViewMatrix.Scale(1.0f, -1.0f, 1.0f);
//翻转后物体向量也会反向,所以0.8是向下移动
modelViewMatrix.Translate(0, 0.8, 0);
//指定顺时针为正面
//由于沿y轴旋转,为了保证绘制正常,需要重新定义正反面
glFrontFace(GL_CW);
drawSomething(yRot);
glFrontFace(GL_CCW);
modelViewMatrix.PopMatrix();
}
绘制上方物体部分
代码语言:javascript复制void RenderScene(void) {
drawSomething(yRot);
}
地板
代码语言:javascript复制void RenderScene(void) {
//开启混合功能(绘制地板)
glEnable(GL_BLEND);
//指定glBlendFunc 颜色混合方程式
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
//绑定创建好的第一个纹理对象用于绘制
glBindTexture(GL_TEXTURE_2D, texture[0]);
//半透明色,保证可以看到镜面效果
static GLfloat vFloorColor[] = { 1.0f, 1.0f, 0.0f, 0.75f};
/*
纹理调整着色器(将一个半透明基本色乘以一个取自纹理的单元texture的纹理,达到透明效果)
参数1:GLT_SHADER_TEXTURE_MODULATE
参数2:模型视图投影矩阵
参数3:颜色
参数4:纹理单元(第0层的纹理单元)
*/
shaderManager.UseStockShader(GLT_SHADER_TEXTURE_MODULATE, transform.GetModelViewProjectionMatrix(), vFloorColor,0);
floorBatch.Draw();
glDisable(GL_BLEND);
}
- 镜面效果其实就是对上方物体在对称位置在绘制一份,地板为半透明才可以看到下方镜面物体。
最后
代码语言:javascript复制void RenderScene(void) {
//将之前设置的观察者矩阵出栈
modelViewMatrix.PopMatrix();
//交换缓冲区
glutSwapBuffers();
//强制重绘
glutPostRedisplay();
}
- 出栈同样是为了保证下次设置,不会受这次设置的影响。
- 强制重绘来实现所见的动画效果,而不是定时器实现的。