本篇结合一个例子,写对OpenGL的理解。包括OpenGL上下文和对象,以及基于OpenGL上下文,OpenGL经过哪些管线步骤渲染出最终图像。
从应用的角度看,OpenGL是一套操作图形渲染的API。OpenGL应用通过这些API设置一系列状态并配置着色器程序,将输入的几何模型——点,线,三角形及patch通过OpenGL管线的每个阶段,最终渲染成一帧图像。OpenGL只负责渲染几何图元,不提供任何描述三维模型的功能。OpenGL独立于硬件,操作系统,窗口系统,由应用自身去使用窗口系统的功能;因此需要基于特定操作系统/窗口系统的辅助API完成外围/窗口相关的工作,例如为应用接收鼠标键盘的输入,以及将帧缓存中的图像显示到窗口中。
从实现的角度看,OpenGL是一个规范,由Khronos组织制定并维护,当前最新的是4.5版本。它详细规定了每个函数的功能及期望的执行效果等。OpenGL给出的是规范,对于细节则允许不同的实现,只要其结果遵从规格,不会使用户感觉差异即可。因此GPU制造商会权衡效率、性能、功耗等架构问题,较灵活地完成API的底层实现。一些特性则较为宽松,允许硬件自己的实现,因此这些特性在不同硬件上可能会表现出细微的差别。通常每一版新出的规范都会支持一些新特性。在新规范出来前,一些GPU厂商会开发属于自己的扩展特性,流行的新特性很可能被收入未来的规范中。
通常几何模型被开发者描述成许多几何图元,几何图元由顶点及其属性数据表示,如三维位置坐标(x,y,z),颜色(R,G,B)或(R,G,B,A),法向量,纹理坐标(u,v)等等,这些数据经过OpenGL一系列阶段的转化/运算,最终得到二维颜色数据,即一帧图像存储于帧缓存中。这些阶段即OpenGL管线(Pipeline),现代GPU通常支持5种着色器,固定的光栅化阶段和输出混合阶段。着色器(Shader)是一段专门编译给GPU执行的小程序,这些小程序可由应用开发者通过GLSL(GL Shader Language)灵活编写。着色器阶段是OpenGL管线中的可编程阶段,这也是当前OpenGL管线区别于早期固定管线之处。OpenGL的着色器包括Vertex Shader,Tessellation Control Shader,Tessellation Evaluation Shader,Geometry Shader和Fragment Shader。通常这5个着色器不需要全部开启,但要完成渲染,开启Vertex Shader和Pixel Shader是必须的。
1)顶点着色器阶段:以顶点为单位,处理每个顶点,例如坐标转换等。
2)曲面细分(Tessellation)阶段:将一系列顶点表示的Patch细分出多个图元。
3)几何着色器(Geometry Shader)阶段:以图元为单位,处理图元数据,或产生新的图元。
4)光栅化阶段:确定图元覆盖的屏幕像素,计算每个像素的属性数据。
5)片元着色器(Fragment Shader)阶段:以像素为单位,根据光栅化得到的像素数据,以及Shader程序的处理,计算得到每个像素的颜色(和深度)。
6)输出混合(Output Merging)阶段:经过指定的混合操作,得到最终的二维平面上的像素颜色
OpenGL是一个状态机,记住这一点能帮我们更好地理解它。管线中每个阶段的行为都由一系列状态控制,应用通过API设置一些选项或操作一些缓冲更改这些状态,从而操作渲染行为,例如设置图元类型为三角形时,将告诉光栅化逐次将三个顶点作为一个图元;将深度测试开启时,输出混合阶段会将深度值大于当前缓存的像素丢掉等等。
所有这些渲染相关的状态,称为OpengGL上下文(Context),从底层的角度看,上下文是在实际渲染前,驱动为硬件寄存器所做的配置。基于上下文,驱动进一步下达命令及顶点数据,GPU管线根据状态处理顶点数据。
对象则是OpenGL上下文的一个子集,把OpenGL上下文当做一个巨大结构体,则对象为该结构体的数据成员。
在应用中渲染不同的模型时,通常只需要切换部分对象而不是整个上下文。OpenGL提供了glGen*()/glBind*().类型的方法,实现对象的绑定与切换,以一个例子说明这种状态操作。假设OpenGL的上下文由结构体GL_Context表示,其中有一个viewport对象:
struct GL_Context { ... object* pViewport; ... };
假设viewport由4个float组成,并且可通过API设置它们:
struct Viewport { GLfloat top; GLfloat left; GLfloat width; GLfloat height; }
当希望把模型渲染到不同的viewport上时,可以定义多个viewport,设置每个viewport(top,left等),并根据需要切换:
// 创建对象 GLuint viewportId[N]; glGenViewport(N, &viewportId); // 绑定第k个对象至上下文 glBindViewport(GL_VIEWPORT, viewportId[k]); // 设置GL_WINDOW_TARGET对象 glSetViewport(GL_VIEWPORT, GL_VIEWPORT_TOP, 0); glSetViewport(GL_VIEWPORT, GL_VIEWPORT_LEFT, 0); glSetViewport(GL_VIEWPORT, GL_VIEWPORT_WIDTH, 64); glSetViewport(GL_VIEWPORT, GL_VIEWPORT_HEIGHT, 64); // 绑定第k+1个对象至上下文 glBindViewport(GL_VIEWPORT, viewportId[k+1]); glSetViewport(GL_VIEWPORT, GL_VIEWPORT_TOP, 0); glSetViewport(GL_VIEWPORT, GL_VIEWPORT_LEFT, 0); glSetViewport(GL_VIEWPORT, GL_VIEWPORT_WIDTH, 128); glSetViewport(GL_VIEWPORT, GL_VIEWPORT_HEIGHT, 128); <pre code_snippet_id="1552620" snippet_file_name="blog_20160109_4_7441420" name="code" class="cpp">// 将上下文中对象设回默认glBindViewport(GL_VIEWPORT,0);
如上,glGenViewport(N,&viewportId)产生了N个viewport对象的Id,存于数组viewportid中,这些Id可用于引用viewport对象,Id号由OpengGL产生,并且时此前未使用过的。通常每种对象都有一个Id=0的默认状态,因此glGen*不会返回值为0的Id。
glBindViewport(GL_VIEWPORT,viewportId[k])为第k个对象分配内存,并将对象绑定至上下文的目标位置,如图所示:
glSetViewport设置Viewport的相关参数。接下来glBindViewport(GL_VIEWPORT,viewportId[k+1])为第k+1个对象分配内存,并将上下文中目标切换到该对象上,同样地通过glSetViewport设置新的Viewport状态。
最后的glBindViewport(GL_VIEWPORT,0)将目标位置的对象id设回0的方式解绑原来的对象,将目标绑定至默认状态。
下载freeglut,根据自己的IDE选择/VisualStudio下2008或2010里的solution进行build(Release),将下列文件拷到下列的系统目录或Visual Studio的引用目录下:
lib/x86/freeglut.dll 系统根目录/system32
lib/x86/freeglut.lib VC根目录/Lib
include/GL/glut.h, freeglut.h, freeglut_ext.h, freeglut_std.h VC根目录Include/GL
下载glew,将下列文件拷到下列的系统目录或Visual Studio的引用目录下:
bin/glew32.dll 系统根目录/system32
lib/glew32.lib VC根目录/Lib
include/GL/glew.h,wglew.h VC根目录/Include/GL
经过glew必要的初始化可调用v1.1.0之外硬件所支持的API。如果电脑没装OpenGL驱动,仅支持v1.1.0,调用高版本API会报出acesss violation的错误,需要更新驱动。硬件所支持的OpenGL版本可通过glGetString(GL_VERSION) 查询,更详细的信息可通过可软件 OpenGL Extension Viewer查看。
《OpengGL编程指南》书中部分例子的源码可到官网下载,获取vgl.h,LoadShaders.h及LoadShaders.cpp等文件,这几个文件可用于参考,不一定刚好无错误通过编译。可能有些地方需要根据自己的环境修改。
#include "LoadShaders.h" #pragma comment(lib, "glew32.lib") enum VAO_IDs {Triangles,NumVAOs}; enum Buffer_IDs{ArrayBuffer, NumBuffers}; enum Attrib_IDs{vPosition=0}; GLuint VAOs[NumVAOs]; GLuint Buffers[NumBuffers]; const GLuint NumVertex = 6; void init() { cout<<glGenVertexArrays<<endl; cout<<glGetString(GL_VERSION)<<endl; glGenVertexArrays(NumVAOs, VAOs); glBindVertexArray(VAOs[Triangles]); GLfloat vertices[NumVertex][2]={ {-0.90f, -0.90f},//Triangle1 {0.85f, -0.90f}, {-0.90f, 0.85f}, {0.90f, -0.85f},//Triangle2 {0.90f, 0.90f}, {-0.85f, 0.90f} }; 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 dispaly(void) { glClear(GL_COLOR_BUFFER_BIT); glBindVertexArray(VAOs[Triangles]); glDrawArrays(GL_TRIANGLES, 0, NumVertex); glFlush(); } //main----------------------- int main(int argc, char**argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutInitWindowSize(512,512); //glutInitContextVersion(4,2);//if this initial is used, glewExperimental = GL_TRUE should also be used. //glutInitContextProfile(GLUT_CORE_PROFILE); glutCreateWindow(argv[0]); /*cout<<glGetString(GL_VERSION)<<endl; cout<<glGetString(GL_RENDERER)<<endl;*/ //glewExperimental = GL_TRUE; if(glewInit()!= GLEW_OK) { cerr<<"Unable to initialize GLEW...exiting"<<endl; exit(EXIT_FAILURE); } while( GL_NO_ERROR != glGetError() ); init(); glutDisplayFunc(dispaly); glutMainLoop(); }
LoadShaders.h
#pragma once // --------------------------------------------------------------------------- // LoadShaders.h // Quick and dirty LoadShader function for the OpenGL Programming Guide 4.3 // Red Book. // // Author: Qoheleth // [url]http://www.opengl.org/discussion_boards/showthread.php/180175-Redbook-8th-sample-code?p=1245842#post1245842[/url] // --------------------------------------------------------------------------- #include <iostream> #include <fstream> #include <sstream> #include <string> #include <vector> using namespace std; #include "GL/glew.h" #include "GL/freeglut.h" struct ShaderInfo { GLenum target; const char*shaderFile; }; GLuint LoadShaders( ShaderInfo* shaderInfo ); const char* getShaderProgram( const char *filePath, string &shaderProgramText ); #define BUFFER_OFFSET(A) ((void*)(A))
LoadShaders.cpp
// --------------------------------------------------------------------------- // LoadShaders.cpp // Quick and dirty LoadShader function for the OpenGL Programming Guide 4.3 // Red Book. // // Author: Qoheleth // http://www.opengl.org/discussion_boards/showthread.php/180175-Redbook-8th-sample-code?p=1245842#post1245842 // --------------------------------------------------------------------------- #include "LoadShaders.h" GLuint LoadShaders( ShaderInfo* shaderInfo ) { // create the shader program GLint status; GLuint program; program = glCreateProgram(); ShaderInfo* pCurShader = shaderInfo; while(pCurShader->target!=GL_NONE) { GLuint shader; shader = glCreateShader( pCurShader->target ); // create a vertex shader object // load and compile vertex shader string shaderProgramText; const char* text = getShaderProgram( pCurShader->shaderFile, shaderProgramText ); glShaderSource( shader, 1, &text, NULL ); glCompileShader( shader ); cout << text<<endl; glGetShaderiv( shader, GL_COMPILE_STATUS, &status ); if ( status != GL_TRUE ) cerr << "\n"<<pCurShader->shaderFile<<" compilation failed..." << '\n'; char *infoLog = new char[ 100 ]; GLsizei bufSize = 100; glGetShaderInfoLog( shader, bufSize, NULL, infoLog ); cout << infoLog<<endl; delete [] infoLog; glAttachShader( program, shader ); pCurShader++; } // link the objects for an executable program glLinkProgram( program ); glGetProgramiv( program, GL_LINK_STATUS, &status ); if (status != GL_TRUE ) cout << "Link failed..." << endl; // return the program return program; } const char* getShaderProgram( const char *filePath, string &shader ) { fstream shaderFile( filePath, ios::in ); if ( shaderFile.is_open() ) { std::stringstream buffer; buffer << shaderFile.rdbuf(); shader = buffer.str(); buffer.clear(); } shaderFile.close(); return shader.c_str(); }
triangles.vert
#version 420 layout(location =0)in vec4 vPosition; void main() { gl_Position = vPosition; }triangles.frag
#version 420 out vec4 fColor; void main() { fColor = vec4(0.0, 0.0, 1.0, 1.0); }
Main函数调用辅助API 相关函数创建窗口和上下文,
glutInit(&argc, argv); 应用中需要第一个调用的GLUT函数,初始化GLUT库,处理命令行参数;
glutInitDisplayMode(GLUT_RGBA); 指定窗口使用RGBA颜色空间;
glutInitWindowSize(512,512); 指定窗口大小;
glutCreateWindow(argv[0]); 按照指定的DisplayMode, WindowSize创建窗口。
glewInit(); 初始化GLEW库
glutDisplayFunc(dispaly); 设置窗口显示回调函数
glutMainLoop()进入无限循环,
glGenVertexArrays(NumVAOs, VAOs); glBindVertexArray(VAOs[Triangles]);
分配了一个Vertex Array对象的名字,接着为该对象分配空间并设为当前对象。
glGenBuffers(NumBuffers, Buffers); glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
前两个调用类似地设置了Vertex Buffer对象,最后的调用则填充实际的数据。
ShaderInfo shaders[]={ {GL_VERTEX_SHADER,"triangles.vert"}, {GL_FRAGMENT_SHADER,"triangles.frag"}, {GL_NONE,NULL} }; GLuint program = LoadShaders(shaders); glUseProgram(program);
通过LoadShaders将vertex shader和fragment shader编译和链接,这两个shader很简单,vertex shader将输入的位置坐标直接输出,fragment shader将像素的颜色设为蓝色。
glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0)); glEnableVertexAttribArray(vPosition);
设置vertex buffer中的数据如何流入vertex shader:连续地从buffer中取出两个Float数作为vertex shader中的第0个属性(shader中的输入格式是4个分量,不足的两个分量按默认值填充),因此将有6个顶点的位置数据流入vertex shader。
glClear(GL_COLOR_BUFFER_BIT); glBindVertexArray(VAOs[Triangles]); glDrawArrays(GL_TRIANGLES, 0, NumVertex); glFlush();将buffer刷成默认颜色(黑色),接着绑定vertex array,并发出读取buffer中从位置0开始的6个顶点,画三角形的命令,因此每次显示回调执行后会在屏幕上画出两个三角形。
由于未在vertex shader中指定任何坐标变化,buffer中坐标将是NDC坐标,它会在光栅化中映射到屏幕上,最后的结果如下:
通过一个例子,梳理了对OpenGL的大致理解。当然,跑通了第一个例子,后面其他特性的实验就方便多了。
1. OpenGL Programming Guide 8th Edition