您的位置:首页 > 运维架构

OpenGL/3D图形渲染管线

2017-08-29 10:49 344 查看
http://blog.csdn.NET/shenzi/article/details/5417488

 3D图形渲染管线 
什么是渲染(Rendering)
    渲染简单的理解可能可以是这样:就是将三维物体或三维场景的描述转化为一幅二维图像,生成的二维图像能很好的反应三维物体或三维场景(如图1):


    

图1:Rendering

什么是渲染管线
    渲染管线也称为渲染流水线,是显示芯片内部处理图形信号相互独立的的并行处理单元。一个流水线是一序列可以并行和按照固定顺序进行的阶段。每个阶段都从它的前一阶段接收输入,然后把输出发给随后的阶段。就像一个在同一时间内,不同阶段不同的汽车一起制造的装配线,传统的图形硬件流水线以流水的方式处理大量的顶点、几何图元和片段。
    图2显示了当今图形处理器所使用的图形硬件流水线。三维应用程序传给图形处理器一序列的顶点组成不同的几何图元:典型的多边形、线段和点。正如图3所示,有许多种方法来制定几何图元。



图2:图形硬件渲染管线



图3:几何图形的类型 

一.顶点变换(Vertex Transformation):

    顶点变换是图形硬件渲染管线种的第一个处理阶段。顶点变换在每个顶点上执行一系列的数学操作。这些操作包括把顶点位置变换到屏幕位置以便光栅器使用,为贴图产生纹理坐标,以及照亮顶点以决定它的颜色。
顶点变换中的一些坐标:

坐标系统:



图4:用于顶点处理的坐标系统和变换

物体空间:

    应用程序在一个被称为物体空间(也叫模型空间)的坐标系统里指定顶点位置。当一个美工人员创建了一个物体的三维模型的时候,他选择了一个方便的方向、比例和位置来放置模型的组成顶点。一个物体的物体空间可以与其它物体的物体空间没有任何关系。
世界空间:

    一个物体的物体空间和其它对象没有空间上的关系。世界空间的目的是为在你的场景中的所有物体提供一个绝对的参考。一个世界空间坐标系如何建立可以任意选择。例如:你可以决定世界空间的原点是你房间的中心。然户,房间里的物体就可以相对房间的中心和某个比例和某个方向放置了。
建模变换:

    在物体空间中指定的物体被放置到世界空间的方法要依靠建模变换。例如:你也许需要旋转、平移和缩放一个椅子的三维模型,以使椅子可以正确地放置在你的房间的世界坐标系统里。在同一个房间中的两把椅子可以使用同样的三维椅子模型,但使用不同的建模变换,以使每把椅子放在房间中不同的位置。
眼空间:

    最后,你要从一个特殊的视点(“眼睛”)观看你的场景。在称为眼空间(或视觉空间)的坐标系统里,眼睛位于坐标系统的原点。朝“上”的方向通常是轴正方向。遵循标准惯例,你可以确定场景的方向使眼睛是从z轴向下看。
视变换:

    从世界空间位置到眼空间位置的变换时视变换。典型的视变换结合了一个平移把眼睛在世界空间的位置移到眼空间的原点,然后适当地旋转眼睛。通过这样做,视变换定义了视点的位置和方向。

    我们通常把分别代表建模和视变换的两个矩阵结合在一起,组成一个单独的被称为modelview的矩阵。你可以通过简单地用建模矩阵乘以视矩阵把它们结合在一起。
剪裁空间:

    当位置在眼空间以后,下一步是决定什么位置是在你最终要渲染的图像中可见的。在眼空间之后的坐标系统被称为剪裁空间,在这个空间中的坐标系统称为剪裁坐标。
投影变换:

    从眼空间坐标到剪裁空间的变换被称为投影变换。投影变换定义了一个事先平截体(view frustum),代表了眼空间中物体的可见区域。只有在视线平截体中的多边形、线段和点背光栅化到一幅图形中时,才潜在的有可能被看得见。
标准化的设备坐标:

    剪裁坐标是齐次形式<x,y,z,w>的,但我们需要计算一个二维位置(一对x和y)和一个深度值(深度值是为了进行深度缓冲,一种硬件加速的渲染可见表面的方法)。
透视除法:

    用w除x,y和z能完成这项工作。生成的结果坐标被称为标准化的设备坐标。现在所有的几何数据都标准化为[-1,1]之间。
窗口坐标:

    最后一步是取每个顶点的标准化的设备坐标,然后把它们转换为使用像素度量x和x的最后的坐标系统。这一步骤命名为视图变换,它为图形处理器的光栅器提供数据。然后光栅器从顶点组成点、线段或多边形,并生成决定最后图像的片段。另一个被称为深度范围变换的变换,缩放顶点的z值到在深度缓冲中使用的深度缓存的范围内。
二.图元装配(Primitive Assembly)和光栅化(Rasterization)
    经过变换的顶点流按照顺序被送到下一个被称为图元装配和光栅化的阶段。首先,在图元装配阶段根据伴随顶点序列的几何图元分类信息把顶点装配成几何图元。这将产生一序列的三角形、线段和点。这些图元需要经过裁剪到可视平截体(三维空间中一个可见的区域)和任何有效地应用程序指定的裁剪平面。光栅器还可以根据多边形的朝前或朝后来丢弃一些多边形。这个过程被称为挑选(culling)。 
    经过裁剪和挑选剩下的多边形必须被光栅化。光栅化是一个决定哪些像素被几何图元覆盖的过程。多边形、线段和点根据为每种图元指定的规则分别被光栅化。光栅化的结果是像素位置的集合和片段的集合。当光栅化后,一个图元拥有的顶点数目和产生的片段之间没有任何关系。例如,一个由三个顶点组成的三角形占据整个屏幕,因此需要生成上百万的片段。
    片段和像素之间的区别变得非常重要。术语像素(Pixel)是图像元素的简称。一个像素代表帧缓存中某个指定位置的内容,例如颜色,深度和其它与这个位置相关联的值。一个片段(Fragment)是更新一个特定像素潜在需要的一个状态。
    之所以术语片段是因为光栅化会把每个几何图元(例如三角形)所覆盖的像素分解成像素大小的片段。一个片段有一个与之相关联的像素位置、深度值和经过插值的参数,例如颜色,第二(反射)颜色和一个或多个纹理坐标集。这些各种各样的经过插值的参数是来自变换过的顶点,这些顶点组成了某个用来生成片段的几何图元。你可以把片段看成是潜在的像素。如果一个片段通过了各种各样的光栅化测试(在光栅操作将做讨论),这个片段将被用于更新帧缓存中的像素。
三.插值、贴图和着色

    当一个图元被光栅化为一堆零个或多个片段的时候,插值、贴图和着色阶段就在片段属性需要的时候插值,执行一系列的贴图和数学操作,然后为每个片段确定一个最终的颜色。除了确定片段的最终颜色,这个阶段还确定一个新的深度,或者甚至丢弃这个片段以避免更新帧缓存对应的像素。允许这个阶段可能丢弃片段,这个阶段为它接收到的每个输入片段产生一个或不产生着过色的片段。
四.光栅操作(Raster
Operations)
    光栅操作阶段在最后更新帧缓存之前,执行最后一系列的针对每个片段的操作。这些操作是OpenGL和Direct3D的一个标准组成部分。在这个阶段,隐藏面通过一个被称为深度测试的过程而消除。其它一些效果,例如混合和基于模板的阴影也发生在这个阶段。
    光栅操作阶段根据许多测试来检查每个片段,这些测试包括剪切、alpha、模板和深度等测试。这些测试涉及了片段最后的颜色或深度,像素的位置和一些像素值(像素的深度值和模板值)。如果任何一项测试失败了,片段就会在这个阶段被丢弃,而更新像素的颜色值(虽然一个模板写入的操作也许会发生)。通过了深度测试就可以用片段的深度值代替像素深度值了。在这些测试之后,一个混合操作将把片段的最后颜色和对应像素的颜色结合在一起。最后,一个帧缓存写操作用混合的颜色代替像素的颜色。
    图5显示了光栅操作阶段本身实际上也是一个流水线。实际上,所有之前介绍的阶段都可以被进一步分解成子过程。



图5:标准OpenGL和Direct3D光栅操作
五.形象化图形流水线
   图6描写了图形流水线的各个阶段。在本图中,两个三角形被光栅化了。整个过程从顶点的变换和着色开始。下一步,图元装配解读那从顶点创建三角形,如虚线所示。之后,光栅用片段填充三角形。最后,从顶点得到的值被用来插值,然后用于贴图和着色。注意仅仅从几个顶点就产生了许多片段。



图6:形象化图形流水线
可编程图形流水线
    当今图形硬件设计上最明显的趋势是在图形处理器内提供更多的可编程性。图7显示了一个可编程图形处理器的流水线中的顶点处理器和片元(像素)处理器。
    图7比图2展示了更多的细节,更重要的是它显示了顶点和片段处理被分离成可编程单元。可编程顶点处理器和片段处理器是图形硬件中执行Vertex Shader和Pixel Shader的硬件单元。



图7:可编程图形流水线

图形渲染管线(Pipeline)

图形渲染管线指的是对一些原始数据经过一系列的处理变换并最终把这些数据输出到屏幕上的整个过程。

图形渲染管线的整个处理流程可以被划分为几个阶段,上一个阶段的输出数据作为下一个阶段的输入数据,是一个串行的,面向过程的执行过程。每一个阶段分别在GPU上运行各自的数据处理程序,这个程序就是着色器。

部分着色器允许我们使用着色语言(OpenGL Shading Language)编写自定义的着色器,这样就可以更为细致的控制图像渲染流程中的特定处理过程了,下图是一个图形渲染管线每一个阶段的抽象表示,蓝色部分代表允许自定义着色器。



顶点数据是一些顶点的集合,顶点一般是3维的点坐标组成。

基本图元(Primitives)包括点,线段,三角形等,是构成实体模型的基本单位,需要在传入顶点数据的同时通知OpenGL这些顶点数据要组成的基本图元类型。

顶点着色器(Vertex Shader)包含对一些顶点属性(数据)的基本处理。

基本图元装配(Primitive Assembly)把所有输入的顶点数据作为输入,输出制定的基本图元。

几何着色器(Geometry Shader)把基本图元形式的顶点的集合作为输入,可以通过产生新顶点构造出新的(或是其他的)基本图元来生成其他形状。

细分着色器(Tessellation Shaders)可以把基本图元细分为更多的基本图形,创建出更加平滑的视觉效果。

光栅化(Rasterization)即像素化,把细分着色器输出的基本图形映射为屏幕上网格的像素点,生成供片段着色器处理的片段(Fragment),光栅化包含一个剪裁操作,会舍弃超出定义的视窗之外的像素。

片段着色器(Fragment Shader)的主要作用是计算出每一个像素点最终的颜色,通常片段着色器会包含3D场景的一些额外的数据,如光线,阴影等。

测试与混合是对每个像素点进行深度测试,Alpha测试等测试并进行颜色混合的操作,这些测试与混合操作决定了屏幕视窗上每个像素点最终的颜色以及透明度。

在整个渲染管线中需要自定义处理的主要是顶点着色器和片段着色器。

顶点缓冲对象(Vertex Buffer Objects,VBO)

顶点缓冲对象VBO是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标,顶点法向量,顶点颜色数据等。在渲染时,可以直接从VBO中取出顶点的各类属性数据,由于VBO在显存而不是在内存中,不需要从CPU传输数据,处理效率更高。

所以可以理解为VBO就是显存中的一个存储区域,可以保持大量的顶点属性信息。并且可以开辟很多个VBO,每个VBO在OpenGL中有它的唯一标识ID,这个ID对应着具体的VBO的显存地址,通过这个ID可以对特定的VBO内的数据进行存取操作。

VBO的创建以及配置

创建VBO的第一步需要开辟(声明/获得)显存空间并分配VBO的ID:

[cpp] view
plain copy

 print?

       //创建vertex buffer object对象  

<span style="white-space:pre">  </span>GLuint vboId;//vertex buffer object句柄  

<span style="white-space:pre">  </span>glGenBuffers(1, &vboId);  

创建的VBO可用来保存不同类型的顶点数据,创建之后需要通过分配的ID绑定(bind)一下制定的VBO,对于同一类型的顶点数据一次只能绑定一个VBO。绑定操作通过glBindBuffer来实现,第一个参数指定绑定的数据类型,可以是GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER或者GL_PIXEL_UNPACK_BUFFER中的一个。

[cpp] view
plain copy

 print?

glBindBuffer(GL_ARRAY_BUFFER, vboId);  

接下来调用glBufferData把用户定义的数据传输到当前绑定的显存缓冲区中。

[cpp] view
plain copy

 print?

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  

顶点数据传入GPU之后,还需要通知OpenGL如何解释这些顶点数据,这个工作由函数glVertexAttribPointer完成:

[cpp] view
plain copy

 print?

glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);  

第一个参数指定顶点属性位置,与顶点着色器中layout(location=0)对应。
第二个参数指定顶点属性大小。
第三个参数指定数据类型。
第四个参数定义是否希望数据被标准化。
第五个参数是步长(Stride),指定在连续的顶点属性之间的间隔。
第六个参数表示我们的位置数据在缓冲区起始位置的偏移量。

顶点属性glVertexAttribPointer默认是关闭的,使用时要以顶点属性位置值为参数调用glEnableVertexAttribArray开启。如glEnableVertexAttribArray(0);

顶点数组对象(Vertex Arrary Object,VAO)

VBO保存了一个模型的顶点属性信息,每次绘制模型之前需要绑定顶点的所有信息,当数据量很大时,重复这样的动作变得非常麻烦。VAO可以把这些所有的配置都存储在一个对象中,每次绘制模型时,只需要绑定这个VAO对象就可以了。

VAO是一个保存了所有顶点数据属性的状态结合,它存储了顶点数据的格式以及顶点数据所需的VBO对象的引用。

VAO本身并没有存储顶点的相关属性数据,这些信息是存储在VBO中的,VAO相当于是对很多个VBO的引用,把一些VBO组合在一起作为一个对象统一管理。

VAO的创建和配置

生成一个VAO对象并绑定:

[cpp] view
plain copy

 print?

       //创建vertex array object对象     

GLuint vaoId;//vertext array object句柄  

glGenVertexArrays(1, &vaoId);  

glBindVertexArray(vaoId);  

执行VAO绑定之后其后的所有VBO配置都是这个VAO对象的一部分,可以说VBO是对顶点属性信息的绑定,VAO是对很多个VBO的绑定。

OpenGL中所有的图形都是通过分解成三角形的方式进行绘制,glDrawArrays函数负责把模型绘制出来,它使用当前激活的着色器,当前VAO对象中的VBO顶点数据和属性配置来绘制出来基本图形。

[cpp] view
plain copy

 print?

glDrawArrays (GLenum mode, GLint first, GLsizei count)  

第一个参数表示绘制的类型,有三种取值:

1.GL_TRIANGLES:每三个顶之间绘制三角形,之间不连接;
2.GL_TRIANGLE_FAN:以V0V1V2,V0V2V3,V0V3V4,……的形式绘制三角形;
3.GL_TRIANGLE_STRIP:顺序在每三个顶点之间均绘制三角形。这个方法可以保证从相同的方向上所有三角形均被绘制。以V0V1V2,V1V2V3,V2V3V4……的形式绘制三角形;

第二个参数定义从缓存中的哪一位开始绘制,一般定义为0;

第三个参数定义绘制的顶点数量;

索引缓冲对象(Element Buffer Object,EBO)

索引缓冲对象EBO相当于OpenGL中的顶点数组的概念,是为了解决同一个顶点多洗重复调用的问题,可以减少内存空间浪费,提高执行效率。当需要使用重复的顶点时,通过顶点的位置索引来调用顶点,而不是对重复的顶点信息重复记录,重复调用。

EBO中存储的内容就是顶点位置的索引indices,EBO跟VBO类似,也是在显存中的一块内存缓冲器,只不过EBO保存的是顶点的索引。

创建EBO并绑定,用glBufferData(以GL_ELEMENT_ARRAY_BUFFER为参数)把索引存储到EBO中:

[cpp] view
plain copy

 print?

   <span style="font-size:14px;">    GLuint EBO;  

glGenBuffers(1, &EBO);  

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);  

glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);</span>  

当用EBO绑定顶点索引的方式绘制模型时,需要使用glDrawElements而不是glDrawArrays:

[cpp] view
plain copy

 print?

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);  

第一个参数指定了要绘制的模式;
第二个参数指定要绘制的顶点个数;
第三个参数是索引的数据类型;
第四个参数是可选的EBO中偏移量设定。

Talk is cheap,第一个例子是使用VBO,VAO绘制一个矩形图形:

[cpp] view
plain copy

 print?

//使用VAO VBO绘制矩形  

#include <GL/glew.h>    

#include <GL/freeglut.h>    

  

void userInit();  //自定义初始化  

void reshape(int w, int h);   //重绘  

void display(void);  

void keyboardAction(unsigned char key, int x, int y);   //键盘退出事件  

  

GLuint vboId;//vertex buffer object句柄      

GLuint vaoId;//vertext array object句柄      

GLuint programId;//shader program 句柄      

  

int main(int argc, char **argv)  

{  

    glutInit(&argc, argv);  

    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);  

    glutInitWindowPosition(100, 100);  

    glutInitWindowSize(512, 512);  

    glutCreateWindow("Rectangle demo");  

  

    //使用glew,需要执行glewInit,不然运行过程会报错  

    //glewInit要放在glut完成了基本的初始化之后执行  

    glewInit();  

  

    //自定义初始化,生成VAO,VBO对象  

    userInit();  

  

    //重绘函数  

    glutReshapeFunc(reshape);  

  

    glutDisplayFunc(display);  

  

    //注册键盘按键退出事件  

    glutKeyboardFunc(keyboardAction);  

  

    glutMainLoop();  

    return 0;  

}  

  

//自定义初始化函数      

void userInit()  

{  

    glClearColor(0.0, 0.0, 0.0, 0.0);  

    //创建顶点数据      

    const GLfloat vertices[] = {  

        -0.5f,-0.5f,0.0f,1.0f,  

        0.5f,-0.5f,0.0f,1.0f,  

        0.5f,0.5f,0.0f,1.0f,  

        -0.5f,0.5f,0.0f,1.0f,  

    };  

  

    //创建VAO对象  

    glGenVertexArrays(1, &vaoId);  

    glBindVertexArray(vaoId);  

  

    //创建VBO对象     

    glGenBuffers(1, &vboId);  

    glBindBuffer(GL_ARRAY_BUFFER, vboId);  

    //传入VBO数据  

    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  

    //解除VBO绑定  

    glBindBuffer(GL_ARRAY_BUFFER, 0);  

}  

  

//调整窗口大小回调函数      

void reshape(int w, int h)  

{  

    glViewport(0, 0, (GLsizei)w, (GLsizei)h);  

}  

  

//绘制回调函数      

void display(void)  

{  

    glClear(GL_COLOR_BUFFER_BIT);  

  

    //绑定VBO  

    glBindBuffer(GL_ARRAY_BUFFER, vboId);  

    glEnableVertexAttribArray(0);  

  

    //解释顶点数据方式  

    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);  

  

    //绘制模型  

    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);  

  

    glBindBuffer(GL_ARRAY_BUFFER, 0);  

    glDisableVertexAttribArray(0);  

  

    glutSwapBuffers();  

}  

  

//键盘按键回调函数      

void keyboardAction(unsigned char key, int x, int y)  

{  

    switch (key)  

    {  

    case 033:  // Escape key      

        exit(EXIT_SUCCESS);  

        break;  

    }  

}  

编译并执行:



第二个例子使用EBO绘制两个三角形,组成同样的矩形图形:

[cpp] view
plain copy

 print?

//使用EBO绘制矩形(两个三角形)  

#include <GL/glew.h>    

#include <GL/freeglut.h>    

  

void userInit();  //自定义初始化  

void reshape(int w, int h);   //重绘  

void display(void);  

void keyboardAction(unsigned char key, int x, int y);   //键盘退出事件  

  

GLuint eboId;//element buffer object句柄      

GLuint vboId;//vertext buffer object句柄      

GLuint vaoId;//vertext array object句柄      

  

int main(int argc, char **argv)  

{  

    glutInit(&argc, argv);  

    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);  

    glutInitWindowPosition(100, 100);  

    glutInitWindowSize(512, 512);  

    glutCreateWindow("Rectangle demo");  

  

    //使用glew,需要执行glewInit,不然运行过程会报错  

    //glewInit要放在glut完成了基本的初始化之后执行  

    glewInit();  

  

    //自定义初始化,生成VAO,VBO,EBO  

    userInit();  

  

    //重绘函数  

    glutReshapeFunc(reshape);  

    glutDisplayFunc(display);  

    //注册键盘按键退出事件  

    glutKeyboardFunc(keyboardAction);  

    glutMainLoop();  

    return 0;  

}  

  

//自定义初始化函数      

void userInit()  

{  

    glClearColor(0.0, 0.0, 0.0, 0.0);  

  

    //创建顶点数据      

    const GLfloat vertices[] = {  

        -0.5f,-0.5f,0.0f,1.0f,  

        0.5f,-0.5f,0.0f,1.0f,  

        0.5f,0.5f,0.0f,1.0f,  

        -0.5f,0.5f,0.0f,1.0f,  

    };  

    // 索引数据  

    GLshort indices[] = {  

        0, 1, 3,  // 第一个三角形  

        1, 2, 3   // 第二个三角形  

    };  

  

    //创建VAO对象  

    glGenVertexArrays(1, &vaoId);  

    glBindVertexArray(vaoId);  

  

    //创建VBO对象,把顶点数组复制到一个顶点缓冲中,供OpenGL使用  

    glGenBuffers(1, &vboId);  

    glBindBuffer(GL_ARRAY_BUFFER, vboId);  

    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  

  

    //创建EBO对象     

    glGenBuffers(1, &eboId);  

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId);  

    //传入EBO数据  

    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);  

  

    //解释顶点数据方式  

    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);  

    glEnableVertexAttribArray(0);  

  

    //解绑VAO  

    glBindVertexArray(0);  

    //解绑EBO  

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);  

    //解绑VBO  

    glBindBuffer(GL_ARRAY_BUFFER, 0);  

}  

  

//调整窗口大小回调函数      

void reshape(int w, int h)  

{  

    glViewport(0, 0, (GLsizei)w, (GLsizei)h);  

}  

  

//绘制回调函数      

void display(void)  

{  

    glClear(GL_COLOR_BUFFER_BIT);  

    //绑定VAO  

    glBindVertexArray(vaoId);  

    //绘制模型  

    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);  

    glutSwapBuffers();  

}  

  

//键盘按键回调函数      

void keyboardAction(unsigned char key, int x, int y)  

{  

    switch (key)  

    {  

    case 033:  // Escape key      

        exit(EXIT_SUCCESS);  

        break;  

    }  

}  

效果一样:



 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: