美文网首页
视频特效学习03-OpenGL渲染技巧解析

视频特效学习03-OpenGL渲染技巧解析

作者: 小破孩丫 | 来源:发表于2020-04-15 17:57 被阅读0次
    学习目标:
    • 1.渲染过程产生的问题(掌握)
      1. 油画渲染(了解)
      1. 正面&背面剔除(掌握)
      1. 深度测试(掌握)
      1. ZFighting闪烁(了解)
      1. 窗口、视口、裁剪区域(掌握)
      1. 颜色混合(掌握)

    1.渲染过程产生的问题

    • 核心代码
    void setupRC(void){
        glClearColor(0.5, 0.5, 0.7, 1);
        shaderManager.InitializeStockShaders();
        viewFrame.MoveForward(10.0f);
        //创建“环”
        gltMakeTorus(torusBatch, .5f, .3f, 60, 30);
        
    }
    void renderScene(void){
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
        modelViewMatix.PushMatrix(viewFrame);
        
        GLfloat vColor[] = { 0.2f, 0.4f, 0.6f, 1.0f };
        //使用默认光源着色器
        //通过光源、阴影效果跟提现立体效果
        //参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
        //参数2:模型视图矩阵
        //参数3:投影矩阵
        //参数4:基本颜色值
        shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT,transformPipeline.GetModelViewMatrix(),transformPipeline.GetProjectionMatrix(),vColor);
        modelViewMatix.PopMatrix();
        torusBatch.Draw();
        glutSwapBuffers();
    }
    
    • 运行结果


      甜甜圈运行结果.png
    • 问题分析:旋转一定的角度后,甜甜圈中不可见的部分与可见的部分混在一起,导致了结果的混乱。在绘制3D场景的时候,我们需要决定哪些部分对观察者是可见的,或者哪些部分对观察者不可见的。对于不可见的部分,应该及早丢弃。例如在一个不透明的墙壁后,就不应该渲染。这种情况叫做“隐藏面消除”。
    • 解决方法:油画算法(不可取);正背面剔除。

    2.油画渲染

    • 油画算法:先绘制场景中的离观察者较远的物体,在绘制较近的物体。例如下图:先绘制红色部分,再绘制黄色部分,最后再绘制灰色部分,即可解决隐藏面消除的问题。


      油画算法.png
    • 弊端:无法处理图形叠加一起的情况。


      图形叠在一起.png

    3.正面&背面剔除

    • 背面剔除:在渲染的时候,背对观察者的面丢弃,只将正面朝向观察者进行计算。将看不到的面进行剔除,节省CPU和GPU的资源。
    • 正/背面区分


      顶点渲染顺序.png
    • 正面:按照逆时针顶点顺序连接
    • 背面:按照顺时针顶点顺序连接
    • 案例:分析立方体的正背面


      立方体正背面.png

      分析:左侧三角形顶点顺序为:1->2->3;右侧三角形顶点顺序为:1->2->3;当观察者在右侧时,则右边的三角形为逆时针方向,则为正面,而左侧的三角形为顺时针,则为背面;当观察者在左侧时,则左边的三角形为逆时针,则为正面,而右侧的三角形为顺时针,则为背面。
      总结:正面和背面是由三角形的顶点定义顺序和观察者方向共同决定的。随着观察者的角度方向改变,正背面也会改变。

    • 相关代码
      • 开启表面剔除(默认背面剔除)
      void glEnable(GL_CULL_FACE);
      
      • 关闭表面剔除(默认背面剔除)
      void glDissable(GL_CULL_FACE);
      
      • 用户选择剔除哪个面(正面/背面)
      void glCullFace(GLenum mode);
      mode参数为:GL_FRONT,GL_BACK(默认),GL_FRONT_AND_BACK,
      
      • 用户指定顺序哪个为正面
      void glFrontFace(GLenum mode);
       mode参数为:GL_CW,GL_CCW,默认值为GL_CCW
      
    • Demo中开启背面剔除:
    //召唤场景
    void renderScene(void){
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
        modelViewMatix.PushMatrix(viewFrame);
        //开启背面剔除
        glEnable(GL_CULL_FACE);
        GLfloat vColor[] = { 0.2f, 0.4f, 0.6f, 1.0f };
        shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT,transformPipeline.GetModelViewMatrix(),transformPipeline.GetProjectionMatrix(),vColor);
        modelViewMatix.PopMatrix();
        torusBatch.Draw();
        glutSwapBuffers();
    }
    

    运行结果:


    背面剔除后的结果.png

    4. 深度测试

    • 深度:深度其实就是该像素点在3D世界中距离摄像机的距离Z值。
    • 深度缓冲区:深度缓冲区就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值。深度值(Z值)越大离摄像机就越远。在不使用深度测试的时候,如果我们先绘制一个距离比较近的物体,在绘制距离比较远的物体,则距离远的位图因为后绘制,会把距离近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。实际上,只要存在深度缓冲区,OpenGL都会把像素的深度值写入到缓冲区。除非调用glDepthMask(GL_false)来禁止写入。
    • 深度测试:深度缓冲区(DepthBuffer)和颜色缓冲区(ColorBuffer)是对应的。颜色缓冲区存储像素的颜色信息,而深度缓冲区存储像素的深度信息。在决定是否绘制一个物体表面时,首先要将表面的当前像素的深度值与深度缓冲区的值进行比较。如果当前的深度值大于深度缓冲区的值,这丢弃这部分。否者利用这个像素对应的深度值和颜色值,分别更新深度缓冲区和颜色缓冲区。这个过程称为”深度测试“。
    • 深度值计算:
      • 深度值一般由16位、24位或者32位值表示,通常是24位。位数越高,深度的精确值越好。深度值的范围在[0,1]之间,值越小表示越靠近观察者,值越大表示越远离观察者。
      • 在OpenGL中,屏幕空间坐标的Z值即是深度缓冲中的深度值。深度缓冲包含了一个介于0.0和1.0之间的深度值,它将会与观察者视角所看见的场景中所有物体的z值进行比较。我们因此需要一些方法转换这些视图空间z值到[0,1]的范围内,下面的线性方程把z值转换为0.0-1.0之间的值:


        z值转换到0-1.png
    • 使用深度测试:
      • 深度缓冲区,一般由窗口管理系统,GLFW创建。深度值一般由16位、24位和32位值表示。位数越高,深度精确度越好。
      • 开启深度测试:
       glEnable(GL_DEPTH_TEST);
      
      • 在绘制场景前,清除颜色缓存区,深度缓冲区
       glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      
      • 指定深度测试判断模式
      void glDepthFunc(GLEnum mode);
      
    深度测试判断模式.png
    • Demo中开启深度测试:
     glEnable(GL_DEPTH_TEST);
    

    运行结果:


    深度测试结果.png

    *5. ZFighting闪烁

    • ZFighting闪烁问题原因:开启深度测试后,OpenGL就不会再去绘制模型被遮挡的部分。这样实现的显示更加真实。但是,由于深度缓冲区精度的限制,对于深度非常小的情况下,就可能导致判断深度值不正确,导致深度测试的结果不可预测。显示时交错闪烁。如下图时而渲染出绿色,时而渲染出红色,无法确定,交错闪烁)。


      ZFighting01.png
    ZFighting02.png
    • 解决方案:
      • 第一步:启用Polygon Offet方式解决
      //参数列表:
      //GL_POLYGON_OFFSET_POINT 对应光栅化模式:GL_POINT
      //GL_POLYGON_OFFSET_LINE 对应光栅化模式:GL_LINE
      //GL_POLYGON_OFFSET_FILL 对应光栅化模式:GL_FILL
      glEnable(GL_POLYGON_OFFSET_FILL)
      
      • 第二步:指定偏移量
      void glPolygonOffset(Glfloat factor,Glfloat units);
      //应⽤到片段上总偏移计算⽅方程式:
      Depth Offset = (DZ * factor) + (r * units); 
      //DZ:深度值(Z值)
      //r:使得深度缓冲区产生变化的最小值负值,将使得z值距离我们更近,而正值,将使得z值距离我们更远,我们  设置factor和units设置为-1,-1
      
      • 第三步:关闭Polygon Offset
       glDisable(GL_POLYGON_OFFSET_FILL)
      
    • 问题预防:
      • 不要将两个物体靠的太近,避免渲染时三角形叠在一起。这种方式要求对场景中物体插入一个少量的偏移,那么就可能避免ZFighting现象。例如立方体和平面问题中,将平面下移0.001f就可以解决这个问题。当然手动去插入这个小的偏移是要付出代价的。
      • 尽可能将近裁剪面设置的离观察者远一些,会使得整个裁剪范围内的精度变高一些。但是这种方式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪面参数。

    6. 窗口、视口、裁剪区域

    • 窗口:就是显示界面。
    • 视口:就是窗口中用来显示图形的一块矩形区域,它可以和窗口等大,也可以比窗口大或小。只有绘制在视口区域中的图形才能被显示,如果图形有一部分超出了视口区域,那么那一部分是看不到的。通过glViewport()函数设置。
    • 裁剪区域(平行投影):就是视口矩形区域的最小最大x坐标(left,right)和最小最大y坐标(bottom,top),而不是窗口的最小最大x坐标和y坐标。通过glOrtho()函数设置,这个函数还需指定最近最远z坐标,形成一个立体的裁剪区域。
    • 裁剪:是OpenGL提高渲染的一种方式,只刷新屏幕上发生变化的部分,OpenGL允许将要进行渲染的窗口只去指定一个裁剪框。基本原理:用于渲染时限制绘制区域,通过此技术可以在屏幕(帧缓冲)指定一个矩形区域。启用裁剪测试之后,不在此矩形区域内的片元被丢弃,只有在此矩形区域内的片元才有可能进入帧缓冲。因此实际达到的效果就是在屏幕上开辟了一个小窗口,可以在其中进行指定内容的绘制。
       //1.开启裁剪测试
       glEnable(GL_SCISSOR_TEST);
       //2.关闭裁剪测试
       gldisable(GL_SCISSOR_TEST);
       //3.指定裁剪窗口 x,y:指定裁剪框左下角位置  width,height:指定裁剪尺寸
       void glScissor(GLint x, GLSize width,GLSize height);
      

    裁剪核心代码:

    void renderScene(void){
        glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        //裁剪成红色分区
        //1.设置裁剪区域的颜色
       glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
       //2.设置裁剪尺寸
       glScissor(100, 100, 600, 400);
       //3.开启裁剪测试
       glEnable(GL_SCISSOR_TEST);
       //4.开启清屏,执行裁剪
       glClear(GL_COLOR_BUFFER_BIT);
       
       //裁剪成绿色分区
       //1.设置裁剪区域的颜色
       glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
       //2.设置裁剪尺寸
       glScissor(200, 200, 400, 200);
       //3.开启裁剪测试
       glEnable(GL_SCISSOR_TEST);
       //4.开启清屏,执行裁剪
       glClear(GL_COLOR_BUFFER_BIT);
       //关闭裁剪区域
       glDisable(GL_SCISSOR_TEST);
       
       glutSwapBuffers();
    }
    

    7. 颜色混合

    • 混合:在OpenGL中,物体透明技术通常被叫做混合(Blending)。OpenGL渲染时会把颜色值存在颜色缓冲区中,每个片段的深度值也是放在深度缓冲区中。当深度缓冲区被关闭时,新的颜色将简单的覆盖原来颜色缓冲区的颜色值,当深度缓冲区再次打开时,新的颜色片段比原来的值更邻近的裁剪平面时,才会替换原来的颜色片段。
       // 使用OpenGL的混合功能
        glEnable(GL_BLEND);
       // 关闭OpenGL的混合功能
       glDisable(GL_BLEND);
      // 设置混合因子,需要用到glBlendFun函数
      // S:源混合因子
      // D:目标混合因子
      glBlendFunc(GL enum S,GLenum D);
      
    • 组合颜色
      OpenGL 会把源颜色和目标颜色各自取出,并乘以一个系数(源颜色乘以的系数称为“源因子”,目标颜色乘以的系数称为“目标因子”),然后相加,这样就得到了新的颜色。(也可以不是相加,新版本的OpenGL可以设置运算方式,包括加、减、取两者中较大的、取两者中较小的、逻辑运算等)。
      • 目标颜色:已经存储在颜色缓冲区的颜色值(之前的颜色)。
      • 源颜色:当前的要进入颜色缓存区的颜色值(后来要画上去的颜色)。

      也可以理解成源颜色和目标颜色是跟绘制的顺序有关的。假如先绘制了一个红色的物体,再在其上绘制绿色的物体。则绿色是源颜色,红色是目标颜色。如果顺序反过来,则 红色就是源颜色,绿色才是目标颜色。在绘制时,应该注意顺序,使得绘制的源颜色与设置的源因子对应,目标颜色与设置的目标因子对应。(联想射箭 目标是靶心 箭是源)

    用数学公式来表达一下这个运算方式:
    。则混合产生的新颜色可以表示为:

    //Cf:最终计算参数的颜色
    //Cs:源颜色
    //S:源混合因子
    //D:目标混合因子
    Cf = (Cs * S) + (Cd * d)
    //假设源颜色的四个分量(指红色,绿色,蓝色,alpha值)是(Rs, Gs, Bs, As)
    //目标颜色的四个分量是(Rd, Gd, Bd, Ad),
    //源因子为(Sr, Sg, Sb, Sa)
    //目标因子为(Dr, Dg, Db, Da)
    Cf = (Rs*Sr+Rd*Dr, Gs*Sg+Gd*Dg, Bs*Sb+Bd*Db, As*Sa+Ad*Da)
    
    OpenGL混合因子.png
    常用混合函数
    glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
    • 混合的Demo
    #include <stdio.h>
    #include "GLShaderManager.h"
    #include "GLTools.h"
    #include <GLUT/GLUT.h>
    GLBatch     squareBatch;
    GLBatch     redBatch;
    GLBatch     blackBatch;
    GLBatch     blueBatch;
    GLBatch     greenBatcch;
    GLShaderManager shaderManager;
    GLfloat blockSize = 0.2f;
    GLfloat vVerts[] = { -blockSize, -blockSize, 0.0f,
        blockSize, -blockSize, 0.0f,
        blockSize,  blockSize, 0.0f,
        -blockSize,  blockSize, 0.0f};
    
    //重塑函数
    void changeSize(int w, int h){
        glViewport(0, 0, w, h);
    }
    
    //召唤场景
    void renderScene(void){
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
        //定义4种颜色
        GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 0.5f };
        GLfloat vGreen[] = { 0.0f, 1.0f, 0.0f, 1.0f };
        GLfloat vBlue[] = { 0.0f, 0.0f, 1.0f, 1.0f };
        GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };
        
        shaderManager.UseStockShader(GLT_SHADER_IDENTITY,vRed);
        redBatch.Draw();
        
        shaderManager.UseStockShader(GLT_SHADER_IDENTITY,vGreen);
        greenBatcch.Draw();
        
        shaderManager.UseStockShader(GLT_SHADER_IDENTITY,vBlue);
        blueBatch.Draw();
        
        shaderManager.UseStockShader(GLT_SHADER_IDENTITY,vBlack);
        blackBatch.Draw();
        
        //固定矩形的颜色
        shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vRed);
        squareBatch.Draw();
        
        //核心代码 颜色混合
        //1.开启混合
        glEnable(GL_BLEND);
        //2.开启组合函数 计算混合颜色因子
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        
        glutSwapBuffers();
        
    }
    
    //设置渲染环境
    void setupRC(void){
        glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        shaderManager.InitializeStockShaders();
        
        //绘制移动的矩形
        squareBatch.Begin(GL_TRIANGLE_FAN, 4);
        squareBatch.CopyVertexData3f(vVerts);
        squareBatch.End();
        
        //绘制4个固定矩形
        GLfloat vBlock[] = {
            0.25f, 0.25f, 0.0f,
            0.75f, 0.25f, 0.0f,
            0.75f, 0.75f, 0.0f,
            0.25f, 0.75f, 0.0f
        };
        redBatch.Begin(GL_TRIANGLE_FAN,4);
        redBatch.CopyVertexData3f(vBlock);
        redBatch.End();
        
        GLfloat vBlock1[] = {
            -0.75f, 0.25f, 0.0f,
            -0.25f, 0.25f, 0.0f,
            -0.25f, 0.75f, 0.0f,
            -0.75f, 0.75f, 0.0f
        };
        greenBatcch.Begin(GL_TRIANGLE_FAN,4);
        greenBatcch.CopyVertexData3f(vBlock1);
        greenBatcch.End();
        
        GLfloat vBlock2[] = {
            -0.75f, -0.75f, 0.0f,
            -0.25f, -0.75f, 0.0f,
            -0.25f, -0.25f, 0.0f,
            -0.75f, -0.25f, 0.0f
        };
        blueBatch.Begin(GL_TRIANGLE_FAN,4);
        blueBatch.CopyVertexData3f(vBlock2);
        blueBatch.End();
        
        GLfloat vBlock3[] = {
            0.25f, -0.75f, 0.0f,
            0.75f, -0.75f, 0.0f,
            0.75f, -0.25f, 0.0f,
            0.25f, -0.25f, 0.0f
        };
        blackBatch.Begin(GL_TRIANGLE_FAN,4);
        blackBatch.CopyVertexData3f(vBlock3);
        blackBatch.End();
        
    }
    
    //特殊键位函数
    void specialKeys(int key, int x, int y){
        
        //每一步移动的距离
        GLfloat stepSize = 0.025f;
        //以左上角的坐标为参照
        GLfloat blockX = vVerts[0];
        GLfloat blockY = vVerts[7];
        
        if (key == GLUT_KEY_UP) {
            blockY += stepSize;
        }
        if (key == GLUT_KEY_DOWN) {
            blockY -= stepSize;
        }
        if (key == GLUT_KEY_LEFT) {
            blockX -= stepSize;
        }
        if (key == GLUT_KEY_RIGHT) {
            blockX += stepSize;
        }
        //边界处理
        if(blockX < -1.0f) blockX = -1.0f;
        if(blockX > (1.0f - blockSize * 2)) blockX = 1.0f - blockSize * 2;;
        if(blockY < -1.0f + blockSize * 2)  blockY = -1.0f + blockSize * 2;
        if(blockY > 1.0f) blockY = 1.0f;
        vVerts[0] = blockX;
        vVerts[1] = blockY - blockSize*2;
        
        vVerts[3] = blockX + blockSize*2;
        vVerts[4] = blockY - blockSize*2;
        
        vVerts[6] = blockX + blockSize*2;
        vVerts[7] = blockY;
        
        vVerts[9] = blockX;
        vVerts[10] = blockY;
        squareBatch.CopyVertexData3f(vVerts);
           
        glutPostRedisplay();
    }
    
    //根据空格次数。切换不同的“窗口名称”
    void keyPressFunc(unsigned char key, int x, int y){
        
    }
    
    //检查OpenGL API是否安全可用
    int checkOpenGLInit(void){
       GLenum status = glewInit();
        if(status != GLEW_OK){
            printf("GLEW Error:%s\n",glewGetErrorString(status));
            return 1;
        }
        return 0;
    }
    
    
    int main(int argc,char *argv[])
    {
        gltSetWorkingDirectory(argv[0]);
        glutInit(&argc, argv);
        glutInitWindowSize(600, 600);
        //申请一个双缓存区、颜色缓存区、深度缓存区、模板缓存区
        glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
        //创建window的名称
        glutCreateWindow("移动矩形,观察颜色");
        
        //注册回调函数(改变尺寸)
        glutReshapeFunc(changeSize);
        //点击空格时,调用的函数
        glutKeyboardFunc(keyPressFunc);
        //特殊键位函数(上下左右)
        glutSpecialFunc(specialKeys);
        //显示函数
        glutDisplayFunc(renderScene);
        //判断一下是否能初始化glew库,确保项目能正常使用OpenGL 框架
        checkOpenGLInit();
        //绘制
        setupRC();
        //runloop运行循环
        glutMainLoop();
        
        return 0;
    }
    

    相关文章

      网友评论

          本文标题:视频特效学习03-OpenGL渲染技巧解析

          本文链接:https://www.haomeiwen.com/subject/qdmcvhtx.html