OpenGL/OpenGL ES 图像渲染以及渲染问题

图像渲染的实现

先看用一个平面着色器渲染出的一个甜甜圈

平面着色器渲染甜甜圈效果图

代码实现:

  • main函数,程序入口。所以OpenGL处理图形、图像都是链式形式,以及基于OpenGL封装的图像处理框架也是链式编程
    gltSetWorkingDirectory(argv[0]);
    
    glutInit(&argc, argv);
   // 初始化窗口
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
    glutInitWindowSize(800, 600);
    glutCreateWindow("ZB");
    // 注册函数
    glutReshapeFunc(ChangeSize);
    glutSpecialFunc(SpecialKeys);
    glutDisplayFunc(RenderScene);
    
    GLenum err = glewInit();
    if (GLEW_OK != err) {
        fprintf(stderr, "GLEW Error:%s\n", glewGetErrorString(err));
        return 1;
    }
    // 主动触发,准备工作
    SetupRC();
    // 一个无限执行的循环,负责一直处理窗口和操作系统的用户输入等操作
    glutMainLoop();
    return 0;
  • changeSize 通过glutReshapeFunc注册为重塑函数,当第一次创建窗口或屏幕大小发生改变时,会调用该函数调整窗口大小/视口大小
    // 保证高度不能为0
    if (h == 0) {
        h = 1;
    }
    
    // 将视口设置为窗口尺寸
    glViewport(0, 0, w, h);
    // 创建投影矩阵,并将它载入投影矩阵堆栈中
    viewFrustum.SetPerspective(35, float(w)/float(h), 1, 1000);
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
    // 初始化渲染管线
    transformPipeline.SetMatrixStacks(modelViweMatix, projectionMatrix);
  • SetupRC 设置需要渲染图形相关顶点数据、颜色值等,手动在main函数调用
    // 1. 设置背景色
    glClearColor(0.3, 0.3, 0.3, 1);
    
    // 2. 初始化着色器管理器
    shaderManager.InitializeStockShaders();
    
    // 3. 将相机向后移动7个单元,肉眼到物体的距离
    viewFrame.MoveForward(5.0);
    
    // 4. 创建一个甜甜圈
    /**
     void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
     参数1: GLTriangleBatch 容器帮助类
     参数2: 外边缘半径
     参数3: 内边缘半径
     参数4、5: 主半径和从半径的细分单元数量
     */
    gltMakeTorus(torusBatch, 1, 0.3, 88, 33);
    
    // 5. 点的大小(方便点填充时,肉眼观察)
    glPointSize(4.0);
  • RenderScene 通过glutDisplayFunc注册为渲染函数。当屏幕发生变化或者开发者主动渲染会调用此函数,用来实现数据->渲染过程
    // 1. 清除窗口和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    // 2. 把摄像机矩阵压入模型矩阵中,压栈 -- 存储一个状态
    modelViweMatix.PushMatrix(viewFrame);
    
    // 3. 设置绘图颜色
    GLfloat vRed[] = {1, 0, 0, 1};
    
    // 4. 使用平面着色器
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
    
    // 5. 绘制
    torusBatch.Draw();
    
    // 6. 出栈,绘制完成恢复  出栈 -- 恢复一个状态
    modelViweMatix.PopMatrix();
    
    // 7. 强制执行缓存区
    glutSwapBuffers();

到这里为止,编译运行就能过出现上图所示的效果图。利用的是平面着色器。
相当的low。

下面在此基础上进行酷炫的一波操作。

main函数中注册了一个函数SpecialKeys,顾名思义,特殊键位,这里控制的是上下左右键位

    // 1. 判断方向
    if (key == GLUT_KEY_UP) {
        // 2. 根据方向调整观察者位置
        // 参数1: 旋转的弧度
        // 参数2、3、4:表示绕哪个轴进行旋转
        viewFrame.RotateWorld(m3dDegToRad(-5), 1, 0, 0);
    }
    if (key == GLUT_KEY_DOWN) {
        viewFrame.RotateWorld(m3dDegToRad(5), 1, 0, 0);
    }
    if (key == GLUT_KEY_LEFT) {
        viewFrame.RotateWorld(m3dDegToRad(-5), 0, 1, 0);
    }
    if (key == GLUT_KEY_RIGHT) {
        viewFrame.RotateWorld(m3dDegToRad(5), 0, 1, 0);
    }
    // 3. 重新刷新
    glutPostRedisplay();

看实现效果

能够旋转的甜甜圈

在来一波更真实的操作,我们使用默认光源着色器来实现

    // 1. 清除窗口和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    // 2. 把摄像机矩阵压入模型矩阵中
    modelViweMatix.PushMatrix(viewFrame);
    
    // 3. 设置绘图颜色
    GLfloat vRed[] = {1, 0, 0, 1};
    
    // 4. 使用平面着色器
//    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
    
    // 4.1 使用默认光源着色器
    // 通过光源、阴影效果跟体现立体效果
    // 参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
    // 参数2:模型视图矩阵
    // 参数3:投影矩阵
    // 参数4:基本颜色值
    shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
    
    // 5. 绘制
    torusBatch.Draw();
    
    // 6. 出栈,绘制完成恢复
    modelViweMatix.PopMatrix();
    
    // 7. 强制执行缓存区
    glutSwapBuffers();

效果图如下:


未正背面剔除的渲染

可以看出,我们的渲染出了问题。

问题分析

在使用默认光源着色器时,由于产生了光照,有光照的一面,按照原本的颜色显示,而背光面,则是黑暗的,我们看不见的。其实很好理解,太阳光照地球,迎光面是白天,背光面是黑夜。

在绘制3D场景的时候,我们需要决定哪些部分是对观察者可见的,或者哪些部分是对观察者不可见的,对于不可见的部分,应该及早丢弃。例如在一个不透明的墙壁后,就不应该有渲染,这种情况叫做隐藏面消除

下面讨论一下解决这个问题的方案。

解决问题的方案

油画算法

先绘制场景中离观察者较远的物体,在绘制较近的物体,如下图


油画算法

绘制顺序依次是红、黄、灰,这样的话按序渲染能过解决隐藏面消除的问题。

但是随之而来的会有一些不好的问题出现

  • 效率很低,重叠部分会进行多次绘制渲染,浪费资源
  • 对于某些存在场景,无法区别远近顺序的,无法用该方法解决问题,如下图
无法区别远近

正背面剔除

首先需要确定一个问题,任何平面都有2个面,正面/背面,意味着你一个时刻只能看到一面。

一个立方体图形,从任何一个方向去观察,最多可以看到3个面,意味着其他看不到的面,我们不需要去绘制它,如果能以某种方式去丢弃这部分数据,OpenGL在渲染的性能即可提高50%。

没错,OpenGL能够区别正面和背面,通过分析顶点数据的顺序

OpenGL区别正背面

正面/背面区分

  • 正面:按照逆时针顶点链接顺序的三角形面
  • 背面:按照顺时针顶点连接顺序的三角形面

立方体中的正背面


立方体中的正背面

分析:

  • 左侧三角形顶点顺序为:1->2->3; 右侧三角形的顶点顺序为:1->2->3
  • 当观察者在右侧时,则右边的三角形方向为逆时针方向为正面,而左侧的三角形为顺时针则为反面
  • 当观察者在左侧时,则左边的三⻆形方向为逆时针⽅方向为正面,⽽右侧的三角形为顺时针则为背面

总结:
正面和背面是由三角形的顶点定义顺序和观察者方向共同决定的,随着观察者的角度方向的改变,正面背面也会跟着改变

相关代码

// 开启表面剔除(默认背面剔除)
void glEnable(GL_CULL_FACE);

// 关闭表面剔除(默认背面剔除)
void glDisable(GL_CULL_FACE);

// 用户选择剔除那个面(即可自定义剔除,默认为正面)
void glCullFace(GLenum mode);
mode参数为:GL_FRONT, GL_BACK, GL_FRONT_AND_BACK, 默认为GL_BACK

// 用户也可以指定正面
void glFrontFace(GLenum mode);
mode参数为:GL_CW, GL_CCW, 默认为GL_CCW

// 剔除正面实现
glCullFace(GL_BACK);
glFrontFace(GL_CW);
或
glCullface(GL_FRONT);

具体代码实现

    // 1. 清除窗口和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    // 开启正背面剔除
    glEnable(GL_CULL_FACE);
    glFrontFace(GL_CCW);
    glCullFace(GL_BACK);
    
    // 2. 把摄像机矩阵压入模型矩阵中
    modelViweMatix.PushMatrix(viewFrame);
    
    // 3. 设置绘图颜色
    GLfloat vRed[] = {1, 0, 0, 1};
    
    // 后面代码和上面一样,不再重复

实现效果如下图:

未进行深度测试的甜甜圈

可以看到,之前的问题已经解决了,可是又面临了一个尴尬的问题,这个甜甜圈貌似有个很大的缺口,了解过图形渲染的读者肯定知道,这是深度问题,下面来了解一下。

深度

深度就是该像素点在3D世界中距离摄像机的距离,也就是Z值。
深度缓存区就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值Z。Z越大,则距离屏幕越远。

那么为什么需要深度缓冲区?
在不实用深度测试的时候,如果我们先绘制一个距离比较近的物体,在绘制距离远的物体,则距离远的位图因为后绘制,会把距离近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。上面出现的大缺口,也就是这个问题造成的。

实际上,只要存在深度缓冲区,OpenGL都会把像素的深度值写入到缓冲区中,除非调用glDepthMask(GL_FALSE)来禁止写入。

深度测试
深度缓冲区和颜色缓存区是对应的。颜色缓存区存储像素的颜色信息,而深度缓冲区存储像素的深度信息。在决定是否绘制一个物体表面时,首先要将表面对应的像素的深度值与当前深度缓存区中的值进行比较,如果大雨深度缓存区的值,则丢弃这部分,否则利用这个像素对应的深度值和颜色值,分别更新深度缓存区和颜色缓存区。这个过程称为深度测试

相关代码

// 开启深度测试
glEnable(GL_DEPTH_TEST);

// 在绘制场景前,清除颜色缓存区和深度缓冲区
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_GEPTH_BUFFER_BIT);

清除深度缓冲区默认值为1.0,表示最大的深度值,深度值的范围为(0,1)之间。值越小表示越靠近观察者,反正表示距离观察者越远。

下面有关深度测试的判断式

指定深度测试判断模式
void glDepthFunc(GLEnum mode);
打开/阻断 深度缓存区写入
void glDepthMask(GKBool value);
value : GL_TURE 开启写入 GL_FALSE 关闭写入

深度测试判断模式

最终的实现效果如下:

最终效果图

ZFighting闪烁问题

为什么会出现ZFighting闪烁问题

因为开启深度测试后,OpenGL就不会去绘制模型被遮挡的部分,这样实现现实更加真实,但是由于深度缓存区精度的限制,对于深度相差无几的情况下,OpenGL就可能出现不能正确判断两者深度值,会导致深度测试的结果不可预测,现实出来的现象会交错闪烁。

深度相差无几

解决方式

  • 第一步:启用Polygon Offset方式解决
    让深度值之间产生间隔,可以理解为在执行深度测试前,将立方体的深度值做一些细微的增加,于是就能将重叠的2个图形深度值之间有所区分。
// 启用Polygon Offset方式
glEnable(GL_POLYGON_OFFSET_FILL);

参数列表:
GL_POLYGON_OFFSET_POINT 对应光栅化模式:GL_POINT
GL_POLYGON_OFFSET_LINE 对应光栅化模式:GL_LINE
GL_POLYGON_OFFSET_FILL 对应光栅化模式:GL_FILL

  • 第二步:指定偏移量

    • 通过glPolygon Offset 来指定. glPolygon Offset需要2个参数: factor , units.
    • 每个Fragment 的深度值都会增加如下所示的偏移量:
      Offset = ( m * factor ) + ( r * units);
      m : 多边形的深度的斜率的最大值,理解一个多边形越是与近裁剪⾯平行,m就越接近于0.
      r : 能产生于窗口坐标系的深度值中可分辨的差异最小值.r是由具体是由具体OpenGL平台指定的一个常量.
    • 一个⼤于0的Offset会把模型推到离你(摄像机)更远的位置,相应的⼀个小于0的Offset 会把模型拉近
    • 一般⽽言,只需要将-1.0 和 -1 这样简单赋值给glPolygon Offset 基本可以满⾜足需求.
  • 第三步:关闭Polygon Offset

glDisable(GL_POLYGON_OFFSET_FILL);

OK,到此为止,我们完美的把这个甜甜圈给渲染出来了。上面遇到的一些问题也得已解决。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容