翻译自《 OpenGL Programming Guide》(8th) 第一章,标题为 Introduction to OpenGL。
红宝书第八版和第七版的最大的区别就是OpenGL的版本从OpenGL2.X变成了OpenGL4.X,渲染流水线也从固定流水变化为可编程流水线,shader满天飞...
好,进入正文。
此文主要内容如下:
1.介绍OpenGL的作用,告诉你OpenGL在计算机图形学中能做什么,不能做什么;
2.介绍OpenGL程序的常用结构;
3.介绍OpenGL渲染流水线的每个阶段。
OpenGL是一组应用程接口(Application programming interface),即它是一个能够操纵计算机图形硬件的程序库。OpenGL4.3版包含了3500多个函数接口,用于创建图像,操作物体等,一切都是为了创建交互性三维计算机图形程序。
OpenGL是按照流水线型设计的,和硬件无关,这让它能够运行于各种各样的图形硬件上。同时它也是软件无关的,可以运行于不同的操作系统,而只需操作系统只需提供一个让OpenGL运行的GUI库,同样的OpenGL也还会提供描述三维模型或者读取图片文件的方法,你需要做的是将一系列三维图元(比如点,线,三角形), 来组成三维物体。
OpenGL并不是一个新事物,1.0版本在1994年6月友硅谷图形计算机系统开发出来,后续有很多OpenGL的其它版本,也有很多基于OpenGL开发的软件库用于更简单快速地进行应用程序开发,比如游戏开发,科学或者医学可视化系统地开发,抑或仅仅是为了显示图像。越是新版本的OpenGL,和原版本的OpenGL差异就越大。
下面的一个列表简单地描述了OpenGL渲染一张图片需要用到的操作。
● 初始化用于显示图元的数据;
● 将输入的图元作为输入,在其上执行各种Shaders,计算出图元的位置,颜色和其他的渲染属性;
● 将图元的数学描述转化为用于显示在屏幕上的片段(fragment),这个过程称为栅格化;
● 最后执行fragment shader,处理上面得到的fragments,输出的是fragments的最终颜色和位置;
● 还可能会执行一个额外的fragment处理,进行混合,透明之类的操作。
OpenGL是一个client - server系统,你写的应用程序被当作client,运行子啊图形硬件上的OpenGL实现作为服务端,在一些OpenGL实现中,client和server是运行在不同的机器上的,之间用网络连接,在这种情况下,client的命令通过网络由协议进行传输,服务器收到命令之后生成最终图像。
译者注: OpenGL 内部是一个巨大的状态机,你所做的大部分是对这个状态机进行读取和设置工作。在对物体渲染的时候, OpenGL 会根据状态机中的当前状态来进行渲染,好比我们写OpenGL 程序调用 API 实际上是在写配置文件一样。 OpenGL 的状态很多, 涵盖光照,纹理,隐藏面消除,雾等等。
OpenGL可以用来做很多事,那么一个也可能是非常复杂的,但是OpenGL应用程序的基本结构通常是类似的:
● 初始化状态机中的各种状态变量(译者注:状态变量型别是一些C数据类型的 typedef, 有 GLfloat, GLboolean, GLint, GLuint 等等);
● 指定要渲染的物体。
在看代码之前,我们来介绍一些图形学名词,前面我们提到的渲染,指的是计算机从一些模型创建出一张图像的过程,OpenGL只是渲染系统的一种,还有其他的方式,比如ray-tracing,但是用ray-tracing的系统也可能用OpenGL来显示图像或者用于计算。
我们的模型或者物体,用术语来说的话,它们是由一系列图元来确定的,包括点,线,三角形,它们都是由顶点确定的。
另一个使用OpenGL至关重要的概念是Shader,它们是在图形硬件中执行的程序,最好的理解方式是将shader当成为GPU(Graphics
Processing Unit)特别编译的一些小程序.OpenGL包含了编译shader的工具。
在OpenGL中有四个shader处理阶段可以使用,最普遍的是vertex shaders,用于处理顶点,还有fragment shaders,用于处理栅格化时候的片段,vertex和fragment shaders在每一个OpenGL程序中都会用到。
最终生成的图像包含了显示在屏幕上的一系列像素。一个像素是显示器上的最小显示单位。像素的值存放在 frame buffer中,然后传输至显示设备,frame buffer是由图形硬件管理的一个存储区。
下图显示了一个简单的OpenGL程序的输出,在窗口中渲染了两个三角形,源码如下:
/////////////////////////////////////////////////////////////////////// // // triangles.cpp // /////////////////////////////////////////////////////////////////////// #include <iostream> using namespace std; #include "vgl.h" #include "LoadShaders.h" enum VAO_IDs { Triangles, NumVAOs }; enum Buffer_IDs { ArrayBuffer, NumBuffers }; enum Attrib_IDs { vPosition = 0 }; GLuint VAOs[NumVAOs]; GLuint Buffers[NumBuffers]; const GLuint NumVertices = 6; //--------------------------------------------------------------------- // // init // void init(void) { glGenVertexArrays(NumVAOs, VAOs); glBindVertexArray(VAOs[Triangles]); GLfloat vertices[NumVertices][2] = { { -0.90, -0.90 }, // Triangle 1 { 0.85, -0.90 }, { -0.90, 0.85 }, { 0.90, -0.85 }, // Triangle 2 { 0.90, 0.90 }, { -0.85, 0.90 } }; glGenBuffers(NumBuffers, Buffers); glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); ShaderInfo shaders[] = { { GL_VERTEX_SHADER, "triangles.vert" }, { GL_FRAGMENT_SHADER, "triangles.frag" }, { GL_NONE, NULL } }; GLuint program = LoadShaders(shaders); glUseProgram(program); glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0)); glEnableVertexAttribArray(vPosition); } //--------------------------------------------------------------------- // // display // void display( void) { glClear(GL_COLOR_BUFFER_BIT); glBindVertexArray(VAOs[Triangles]); glDrawArrays(GL_TRIANGLES, 0, NumVertices); glFlush(); } //--------------------------------------------------------------------- // // main // int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutInitWindowSize(512, 512); glutInitContextVersion(4, 3); glutInitContextProfile(GLUT_CORE_PROFILE); glutCreateWindow(argv[0]); if (glewInit()) { cerr << "Unable to initialize GLEW ... exiting" << endl; exit(EXIT_FAILURE); } init(); glutDisplayFunc(display); glutMainLoop(); }
简单地说一下上面的例子做了哪些事,我会在后面详细地解释,所以现在看不懂也不用着急。
● 在最开始,我们添加了相应的头文件,圣米格了一些全局变量和其他的有用的结构体;
● init()是用于初始化后面程序要用到的一些数据,这些大部分是后面渲染要到的定点信息,或者是用于纹理映射要用到的图像信息,在本例的init()中中,我们首先是指定了要渲染的两个三角形的位置信息,之后初始化要使用的shader,这次我们只用到了vertex shader和 fragment shader。LoadShaders函数就是用于加载给GPU执行的Shader。init()最后部分做的事称为 shader plumbing ,在这里将数据和shader中的变量进行绑定;
● display函数是程序中真正执行渲染的部分。函数中通过调用OpenGL的函数进行渲染,基本上所有的display函数会做相同的三个步骤:
1.用glClear() 函数清理窗口;
2.调用OpenGL相应的函数来渲染;
3.将渲染出的图像用于显示。
● 最后,mian函数中做了很多的事情 - 创建窗口,调用init,进入时间循环,这里还有一些gl开头的函数,但和其他的函数又有一些不同,简单地说,它们是用于在不同的操作系统中写OpenGL的工具库:GLUT和GLEW.
OpenGL实现了通常所说的渲染流水线。这个流水线分为一系列不同的阶段,能够将应用程序提供给OpenGL的数据转化为一幅最终的渲染图。下面的图为OpenGL4.3的流水线,这个流水线从发布至今已经进化了非常多。
OpenGL在最初将我们提供的图形数据(顶点和图元)传入到一系列的shader 阶段:vertex shading,tesselation shading,然后是geometry shading,这些都在栅格化之前做完。rasterizer会将在裁剪区域的所有图元生成fragments,然后对每一个生成的fragment执行fragment shader。
就如你所看到的,shaders在创建OpenGL应用程序中扮演了一个非常重要的角色。 你有权利去决定去使用哪个shader 阶段,在每个阶段中做哪些事情。并不是每一个阶段都是必须;实际上,只有vertex shaders和fragment shaders才是一定要用到的。Tessellation 和 geometry shaders只是可选项。
现在,我们对每一个阶段都做一下更加深入地了解,这样你对整体就有更好的拿捏。我知道这些东西对你来说可能有点无法理解,但现在最好是硬着头皮看一下。你最后一定会明白理解一些理论会让你在OpenGL的路上走得更远。
准备向OpenGL发送数据
OpenGL要求将所有的数据都存储在buffer对象中,所谓buffer对象就是OpenGL server维护的内存块。将数据存放在buffer中有很多种方法,但是最常用的一种是用 glBufferData()函数,在初始化buffer之前,还有一些额外的工作要做。
向OpenGL发送数据
当我们初始化好buffers之后,我们可以用OpenGL的绘制函数来绘制几何图元,比如glDrawArray().
OpenGL中的绘制通常意味着将顶点信息传送给OpenGL server。一个顶点意味着一个信息的集合,集合中有你想要的任何信息,几乎一定会包含顶点的位置信息,其他的值(比如法线)将会决定像素的最终值。
Vertex Shading
对于每一个需要渲染的顶点,vertex shader都会去处理和顶点相关的数据。根据在栅格化之前要激活哪些shader,vertex shader可能会非常简单,可能仅仅是将数据拷贝传递到下一个阶段 - 我们常称为是 pass-through shader。对于一个很复杂的vertex shader ,用于计算顶点在屏幕中的位置(通常会用到矩阵变换),计算顶点光照等等。
一个复杂的应用程序可能会有多个vertex shader,但每次只能执行一个。
Tesselation Shading
当vertex shader将每个相关的顶点都处理过一遍之后,如果tessellation shader 阶段被激活,tessellation shader将会继续处理这些数据,tesselation用 patchs来描述一个物体,在这个阶段可以用一些相对简单的patch图形来细分模型来提供更好的外观, Tesselation Shading 阶段可以用两个shader来处理,一个用于处理patch 数据,一个用于生成最终形状。
Geometry Shading
下一个shader阶段是geometry Shading,在这个阶段可以在栅格化之前处理单个的集合图元,比如添加一些图元。这个阶段也是可选的,但是非常有用。
图元组装
前面的所有阶段都是在针对顶点的信息操作,图元组装阶段将顶点组装成一组相关联的几何图元,为后面的裁剪和栅格化做准备。
裁剪
有些点会在视口(你打算渲染的窗口)的外面,所以需要将和顶点相关的图元进行一些处理,将不在视口中的图元裁剪掉,这个过程叫做裁剪,是在OpenGL中自动处理的。
栅格化
裁剪之后马上要做的就是栅格化,裁剪之后的图元都传递到 raseriser 中生成fragment。可以将fragment当作是“候选像素”,这些像素存储在framebuffer中。栅格化之后得到的fragment还是能够改变颜色,处理这些fregments在下面两个阶段,fragment shading和 per fragment 操作。
Fragment Shading
可编程的最后一个阶段是fragment shading,在这里你可以控制fragments的颜色。在这个阶段,shader可以决定fragments的最终颜色(虽然在下一个阶段,per-fragment操作会最后一次改变颜色),fragment shaders非常地有用,在这里可以处理 texture mapping。如果一个fragment不应该被绘制,fragment shader也可以停止一个fragment的处理,这个过程称为 fragment discard。
一个很好的思考顶点shader和片段shader的不同点的方法是:vertex shading决定图元在屏幕中处于什么位置,fragment shading用前面的信息来决定fragment的颜色。
Per-Fragment Operations
这里指的是一些额外的fragment 处理,这是对每个fragment处理的最后阶段。在这里fragments的可见性由深度测试和模板测试决定。
详解例子(略)
《 OpenGL Programming Guide》(8th) 下载