OpenGL(九)-- 综合案例(公、自转)OpenGL(九)-- 综合案例(公、自转)

2021-08-09 14:34:18 浏览数 (1)

OpenGL(九)-- 综合案例(公、自转)

相信学习过OpenGL的同学应该过玩过这个经典案例:

总和案例.gif

通过观察这个案例中有三部分:

  1. 地板
  2. 自转大球
  3. 公转小球

这篇文章中会省略一部分基本的初始化代码,而且代码都是按模块进行了分割,如果想要了解可以去另一篇文章中了解一下OpenGL (三)--一个"HelloWorld"的执行全过程,也可以直接下载源码来看github

下面就按照这个顺序贴上代码 个人理解:

  • 全局属性
代码语言:javascript复制
//投影视图矩阵堆栈
GLMatrixStack projectMatrix;
//模型视图矩阵堆栈
GLMatrixStack modelViewMatrix;
//mvp变换管道
GLGeometryTransform transform;
//固定着色器管理类
GLShaderManager shaderManager;

//参考帧
//观察者帧
GLFrame cameraFrame;

//批次类
//地板
GLBatch floorBatch;
//大球
GLTriangleBatch sphereBatch;
//小球
GLTriangleBatch sphereSmallBatch;

//纹理标记数组
//纹理对象
GLuint texture[3];
  • 创建球、地板模型
代码语言:javascript复制
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通过该方法来映射地板的纹理坐标
  • 绑定纹理对象
代码语言:javascript复制
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)
代码语言:javascript复制
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没有对象的概念,是面向过程的编程方式,根据代码执行的顺序完成赋值操作。
  • 绘制基本设置
代码语言:javascript复制
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轴的移动会按照向量方向进行。 如果调整这两句代码的顺序就会得到这样的结果:
代码语言:javascript复制
    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();
}
  • 出栈同样是为了保证下次设置,不会受这次设置的影响。
  • 强制重绘来实现所见的动画效果,而不是定时器实现的。
完整的代码见github- 综合案例(公、自转)

0 人点赞