初窥OpenGL

来源:互联网 发布:学初中英语哪个软件好 编辑:程序博客网 时间:2024/05/16 04:54

前言:OpenGL是什么

本篇结合一个例子,写对OpenGL的理解。包括OpenGL上下文和对象,以及基于OpenGL上下文,OpenGL经过哪些管线步骤渲染出最终图像。

从应用的角度看,OpenGL是一套操作图形渲染的API。OpenGL应用通过这些API设置一系列状态并配置着色器程序,将输入的几何模型——点,线,三角形及patch通过OpenGL管线的每个阶段,最终渲染成一帧图像。OpenGL只负责渲染几何图元,不提供任何描述三维模型的功能。OpenGL独立于硬件,操作系统,窗口系统,由应用自身去使用窗口系统的功能;因此需要基于特定操作系统/窗口系统的辅助API完成外围/窗口相关的工作,例如为应用接收鼠标键盘的输入,以及将帧缓存中的图像显示到窗口中。

从实现的角度看,OpenGL是一个规范,由Khronos组织制定并维护,当前最新的是4.5版本。它详细规定了每个函数的功能及期望的执行效果等。OpenGL给出的是规范,对于细节则允许不同的实现,只要其结果遵从规格,不会使用户感觉差异即可。因此GPU制造商会权衡效率、性能、功耗等架构问题,较灵活地完成API的底层实现。一些特性则较为宽松,允许硬件自己的实现,因此这些特性在不同硬件上可能会表现出细微的差别。通常每一版新出的规范都会支持一些新特性。在新规范出来前,一些GPU厂商会开发属于自己的扩展特性,流行的新特性很可能被收入未来的规范中。

OpenGL管线

通常几何模型被开发者描述成许多几何图元,几何图元由顶点及其属性数据表示,如三维位置坐标(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上下文与对象

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的方式解绑原来的对象,将目标绑定至默认状态。

一个例子

准备工作

前面说到,OpenGL只负责渲染,应用需要辅助API的协助,这里用到glut和glew。
glut(OpenGL Utility Toolkit)能在多种平台上为OpenGL应用创建和管理窗口及读取鼠标键盘输入的输入。glut库已经不再更新,不过freeglut是其基础上继续开发的版本。
glew(OpenGL Extension Wrangler Library)是一个跨平台的C/C++库帮助应用加载平台上所支持的OpenGL扩展。

1.配置GLUT 

下载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

2.配置GLEW

下载glew,将下列文件拷到下列的系统目录或Visual Studio的引用目录下:                 

bin/glew32.dll                          系统根目录/system32

lib/glew32.lib                            VC根目录/Lib

include/GL/glew.h,wglew.h   VC根目录/Include/GL

3.驱动支持

经过glew必要的初始化可调用v1.1.0之外硬件所支持的API。如果电脑没装OpenGL驱动,仅支持v1.1.0,调用高版本API会报出acesss violation的错误,需要更新驱动。硬件所支持的OpenGL版本可通过glGetString(GL_VERSION) 查询,更详细的信息可通过可软件 OpenGL Extension Viewer查看。

4.下载文件

《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 programGLint 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 shaderstring 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 programglLinkProgram( program );glGetProgramiv( program, GL_LINK_STATUS, &status );if (status != GL_TRUE )cout << "Link failed..." << endl;// return the programreturn 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 420layout(location =0)in vec4 vPosition;void main(){    gl_Position = vPosition;}
triangles.frag
#version 420out vec4 fColor;void main(){    fColor = vec4(0.0, 0.0, 1.0, 1.0);}

Main函数

Main函数调用辅助API 相关函数创建窗口和上下文,

glutInit(&argc, argv);                            应用中需要第一个调用的GLUT函数,初始化GLUT库,处理命令行参数;

glutInitDisplayMode(GLUT_RGBA); 指定窗口使用RGBA颜色空间;

glutInitWindowSize(512,512);           指定窗口大小;

glutCreateWindow(argv[0]);               按照指定的DisplayMode, WindowSize创建窗口。

glewInit();                                              初始化GLEW库

glutDisplayFunc(dispaly);                   设置窗口显示回调函数

glutMainLoop()进入无限循环,

init函数

先说明两个概念:

  • Vertex Buffer Object:VBO,该对象表示一个存储数据的缓存,前面讨论了多次的顶点数据,就是存放在Vertex Buffer对象中。
  • Vertex Array Object:Vertex Array对象用于管理Vertex Buffer。因此,一个Vertex Buffer对象要绑定到当前的Vertex Array对象上。

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。

display函数

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

0 0
原创粉丝点击