初窥OpenGL

前言: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_第1张图片


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个对象分配内存,并将对象绑定至上下文的目标位置,如图所示:

初窥OpenGL_第2张图片

glSetViewport设置Viewport的相关参数。接下来glBindViewport(GL_VIEWPORT,viewportId[k+1])为第k+1个对象分配内存,并将上下文中目标切换到该对象上,同样地通过glSetViewport设置新的Viewport状态。

初窥OpenGL_第3张图片

最后的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 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函数

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

你可能感兴趣的:(OpenGL)