本篇结合一个例子,写对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);
// 将上下文中对象设回默认
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<
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
#include
#include
#include
#include
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<shaderFile<<" compilation failed..." << '\n';
char *infoLog = new char[ 100 ];
GLsizei bufSize = 100;
glGetShaderInfoLog( shader, bufSize, NULL, infoLog );
cout << infoLog<
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