1.openGL 初探

GLEW, GLFW和GLM介绍

现在你有了工程,就让我们开始介绍下工程所用到的开源库和为啥需要这些。

The OpenGL Extension Wrangler (GLEW)是用来访问OpenGL 3.2 API函数的。不幸的是你不能简单的使用#include来访问OpenGL接口,除非你想用旧版本的OpenGL。在现代OpenGL中,API函数是在运行时(run time)确定的,而非编译期(compile time)。GLEW可以在运行时加载OpenGL API。

GLFW允许我们跨平台创建窗口,接受鼠标键盘消息。OpenGL不处理这些窗口创建和输入,所以就需要我们自己动手。我选择GLFW是因为它很小,并且容易理解。

OpenGL Mathematics (GLM)是一个数学库,用来处理矢量和矩阵等几乎其它所有东西。旧版本OpenGL提供了类似glRotate, glTranslate和glScale等函数,在现代OpenGL中,这些函数已经不存在了,我们需要自己处理所有的数学运算。GLM能在后续教程里提供很多矢量和矩阵运算上帮助。

在这系列的所有教程中,我们还编写了一个小型库tdogl用来重用C++代码。这篇教程会包含tdogl::Shader和tdogl::Program用来加载,编译和链接shaders。

什么是Shaders?

Shaders在现代OpenGL中是个很重要的概念。应用程序离不开它,除非你理解了,否则这些代码也没有任何意义。

Shaders是一段GLSL小程序,运行在GPU上而非CPU。它们使用OpenGL Shading Language (GLSL)语言编写,看上去像C或C++,但却是另外一种不同的语言。使用shader就像你写个普通程序一样:写代码,编译,最后链接在一起才生成最终的程序。

Shaders并不是个很好的名字,因为它不仅仅只做着色。只要记得它们是个用不同的语言写的,运行在显卡上的小程序就行。

在旧版本的OpenGL中,shaders是可选的。在现代OpenGL中,为了能在屏幕上显示出物体,shaders是必须的。

为可能近距离了解shaders和图形渲染管线,我推荐Durian Software的相关文章The Graphics Pipeline chapter

那shaders实际上干了啥?这取决于是哪种shader。

Vertex Shaders

Vertex shader主要用来将点(x,y,z坐标)变换成不同的点。顶点只是几何形状中的一个点,一个点叫vectex,多个点叫vertices(发音为ver-tuh-seez)。在本教程中,我们的三角形需要三个顶点(vertices)组成。

Vertex Shader的GLSL代码如下:


1

2

3

4

5

6

#version 150

in vec3 vert;

void main() {

    // does not alter the vertices at all

    gl_Position = vec4(vert, 1);

}

第一行#version 150告诉OpenGL这个shader使用GLSL版本1.50。

第二行in vec3 vert;告诉shader需要那一个顶点作为输入,放入变量vert。

第三行定义函数main,这是shader运行入口。这看上去像C,但GLSL中main不需要带任何参数,并且不用返回void。

第四行gl_Position = vec4(vert, 1);将输入的顶点直接输出,变量gl_Position是OpenGL定义的全局变量,用来存储vertex shader的输出。所有vertex shaders都需要对gl_Position进行赋值。

gl_Position是4D坐标(vec4),但vert是3D坐标(vec3),所以我们需要将vert转换为4D坐标vec4(vert, 1)。第二个的参数1是赋值给第四维坐标。我们会在后续教程中学到更多关于4D坐标的东西。但现在,我们只要知道第四维坐标是1即可,i可以忽略它就把它当做3D坐标来对待。

Vertex Shader在本文中没有做任何事,后续我们会修改它来处理动画,摄像机和其它东西。

Fragment Shaders

Fragment shader的主要功能是计算每个需要绘制的像素点的颜色。

一个"fragment"基本上就是一个像素,所以你可以认为片段着色器(fragment shader)就是像素着色器(pixel shader)。在本文中每个片段都是一像素,但这并不总是这样的。你可以更改某个OpenGL设置,以便得到比像素更小的片段,之后的文章我们会讲到这个。

本文所使用的fragment shader代码如下:


1

2

3

4

5

6

#version 150

out vec4 finalColor;

void main() {

    //set every drawn pixel to white

    finalColor = vec4(1.0, 1.0, 1.0, 1.0);

}

再次,第一行#version 150告诉OpenGL这个shader使用的是GLSL 1.50。

第二行finalColor = vec4(1.0, 1.0, 1.0, 1.0);将输出变量设为白色。vec4(1.0, 1.0, 1.0, 1.0)是创建一个RGBA颜色,并且红绿蓝和alpha都设为最大值,即白色。

现在,就能用shader在OpenGL中绘制出了纯白色。在之后的文章中,我们还会加入不同颜色和贴图。贴图就是你3D模型上的图像。

编译和链接Shaders

在C++中,你需要对你的.cpp文件进行编译,然后链接到一起组成最终的程序。OpenGL的shaders也是这么回事。

在这篇文章中用到了两个可复用的类,是用来处理shaders的编译和链接:tdogl::Shader和tdogl::Program。这两个类代码不多,并且有详细的注释,我建议你阅读源码并且去链接OpenGL是如何工作的。

什么是VBO和VAO?

当shaders运行在GPU,其它代码运行在CPU时,你需要有种方式将数据从CPU传给GPU。在本文中,我们传送了一个三角的三个顶点数据,但在更大的工程中3D模型会有成千上万个顶点,颜色,贴图坐标和其它东西。

这就是我们为什么需要Vertex Buffer Objects (VBOs)和Vertex Array Objects (VAOs)。VBO和VAO用来将C++程序的数据传给shaders来渲染。

在旧版本的OpenGL中,是通过glVertex,glTexCoord和glNormal函数把每帧数据发送给GPU的。在现代OpenGL中,所有数据必须通过VBO在渲染之前发送给显卡。当你需要渲染某些数据时,通过设置VAO来描述该获取哪些VBO数据推送给shader变量。

Vertex Buffer Objects (VBOs)

第一步我们需要从内存里上传三角形的三个顶点到显存中。这就是VBO该干的事。VBO其实就是显存的“缓冲区(buffers)” - 一串包含各种二进制数据的字节区域。你能上传3D坐标,颜色,甚至是你喜欢的音乐和诗歌。VBO不关心这些数据是啥,因为它只是对内存进行复制。

Vertex Array Objects (VAOs)

第二步我们要用VBO的数据在shaders中渲染三角形。请记住VBO只是一块数据,它不清楚这些数据的类型。而告诉OpenGL这缓冲区里是啥类型数据,这事就归VAO管。

VAO对VBO和shader变量进行了连接。它描述了VBO所包含的数据类型,还有该传递数据给哪个shader变量。在OpenGL所有不准确的技术名词中,“Vertex Array Object”是最烂的一个,因为它根本没有解释VAO该干的事。

你回头看下本文的vertex shader(在文章的前面),你就能发现我们只有一个输入变量vert。在本文中,我们用VAO来说明“hi,OpenGL,这里的VBO有3D顶点,我想要你在vertex shader时,发三个顶点数据给vert变量。”

在后续的文章中,我们会用VAO来说“hi,OpenGL,这里的VBO有3D顶点,颜色,贴图坐标,我想要你在shader时,发顶点数据给vert变量,发颜色数据给vertColor变量,发贴图坐标给vertTexCoord变量。”

给使用上个OpenGL版本的用户的提醒:

假如你在旧版本的OpenGL中使用了VBO但没有用到VAO,你可能会不认同VAO的描述。你会争论说“顶点属性”可以用glVertexAttribPointer将VBO和shaders连接起来,而不是用VAO。这取决于你是否认为顶点属性应该是VAO“内置(inside)”的(我是这么认为的),或者说它们是否是VAO外置的一个全局状态。3.2内核和我用的AIT驱动中,VAO不是可选项 - 没有VAO的封装glEnableVertexAttribArray, glVertexAttribPointer和glDrawArrays都会导致GL_INVALID_OPERATION错误。这就是为啥我认为顶点属性应该内置于VAO,而非全局状态的原因。3.2内核手册也说VAO是必须的,但我只听说ATI驱动会抛错误。下面描述引用自OpenGL 3.2内核手册

所有与顶点处理有关的数据定义都应该封装在VAO里。 一般VAO边界包含所有更改vertex array状态的命令,比如VertexAttribPointer和EnableVertexAttribArray;所有使用vertex array进行绘制的命令,比如DrawArrays和DrawElements;所有对vertex array状态进行查询的命令(见第6章)。

不管怎样,我也知道为啥会有人认为顶点属性应该放在VAO外部。glVertexAttribPointer出现早于VAO,在这段时间里顶点属性一直被认为是全局状态。你应该能看得出VAO是一种改变全局状态的有效方法。我更倾向于认为是这样:假如你没有创建VAO,那OpenGL通过了一个默认的全局VAO。所以当你使用glVertexAttribPointer时,你仍然是在VAO内修改顶点属性,只不过现在从默认的VAO变成你自己创建的VAO。

这里有更多的讨论:http://www.opengl.org/discussion_boards/showthread.php/174577-Questions-on-VAOs

代码解释

终于!理论已经说完了,我们开始编码。OpenGL对于初学者而言不是特别友好,但如果你理解了之前所介绍的概念(shaders,VBO,VAO)那你就没啥问题。

打开main.cpp,我们从main()函数开始。

首先,我们初始化GLFW:


1

2

3

4

glfwSetErrorCallback(OnError);

if(!glfwInit())

    throw std::runtime_error("glfwInit failed");

glfwSetErrorCallback(OnError)这一行告诉GLFW当错误发生时调用OnError函数。OnError函数会抛一个包含错误信息的异常,我们能从中发现哪里出错了。

然后我们用GLFW创建一个窗口。


1

2

3

4

5

6

7

8

glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);

glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);

glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

gWindow = glfwCreateWindow((int)SCREEN_SIZE.x, (int)SCREEN_SIZE.y, "OpenGL Tutorial", NULL, NULL);

if(!gWindow)

    throw std::runtime_error("glfwCreateWindow failed. Can your hardware handle OpenGL 3.2?");

该窗口包含一个向前兼容的OpenGL 3.2内核上下文。假如glfwCreateWindow失败了,你应该降低OpenGL版本。

创建窗口最后一步,我们应该设置一个“当前”OpenGL上下文给刚创建的窗口:


1

glfwMakeContextCurrent(gWindow);

无论我们调用哪个OpenGL函数,都会影响到“当前上下文”。我们只会用到一个上下文,所以设置完后,就别管它了。理论上来说,我们可以有多个窗口,且每个窗口都可以有自己的上下文。

现在我们窗口有了OpenGL上下文变量,我们需要初始化GLEW以便访问OpenGL接口。


1

2

3

glewExperimental = GL_TRUE; //stops glew crashing on OSX :-/

if(glewInit() != GLEW_OK)

    throw std::runtime_error("glewInit failed");

这里的GLEW与OpenGL内核有点小问题,设置glewExperimental就可以修复,但希望再未来永远不要发生。

我们也可以用GLEW再次确认3.2版本是否存在:


1

2

if(!GLEW_VERSION_3_2)

    throw std::runtime_error("OpenGL 3.2 API is not available.");

在LoadShaders函数中,我们使用本教程提供的tdogl::Shader和tdogl::Program两个类编译和链接了vertex shader和fragment shader。


1

2

3

4

std::vector shaders;

shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("vertex-shader.txt"), GL_VERTEX_SHADER));

shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("fragment-shader.txt"), GL_FRAGMENT_SHADER));

gProgram = new tdogl::Program(shaders);

在LoadTriangle函数中,我们创建了一个VAO和VBO。这是第一步,创建和绑定新的VAO:


1

2

glGenVertexArrays(1, &gVAO);

glBindVertexArray(gVAO);

然后我们创建和绑定新的VBO:


1

2

glGenBuffers(1, &gVBO);

glBindBuffer(GL_ARRAY_BUFFER, gVBO);

接着,我们上传一些数据到VBO中。这些数据就是三个顶点,每个顶点包含三个GLfloat。


1

2

3

4

5

6

7

GLfloat vertexData[] = {

    //  X     Y     Z

     0.0f, 0.8f, 0.0f,

    -0.8f,-0.8f, 0.0f,

     0.8f,-0.8f, 0.0f,

};

glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

现在缓冲区包含了三角形的三个顶点,是时候开始设置VAO了。首先,我们应该启用shader程序中的vert变量。这些变量能被开启或关闭,默认情况下是关闭的,所以我们需要开启它。vert变量是一个“属性变量(attribute variable)”,这也是为何OpenGL函数名称中有带“Attrib”。我们可以在后续的文章中看到更多类型。


1

glEnableVertexAttribArray(gProgram->attrib("vert"));

VAO设置最复杂的部分就是下个函数:glVertexAttribPointer。让我们先调用该函数,等会解释。


1

glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL);

第一个参数,gProgram->attrib("vert"),这就是那个需要上传数据的shder变量。在这个例子中,我们需要发数据给vertshader变量。

第二个参数,3表明每个顶点需要三个数字。

第三个参数,GL_FLOAT说明三个数字是GLfloat类型。这非常重要,因为GLdouble类型的数据大小跟它是不同的。

第四个参数,GL_FALSE说明我们不需要对浮点数进行“归一化”,假如我们使用了归一化,那这个值会被限定为最小0,最大1。我们不需要对我们的顶点进行限制,所以这个参数为false。

第五个参数,0,该参数可以在顶点之间有间隔时使用,设置参数为0,表示数据之间没有间隔。

第六个参数,NULL,假如我们的数据不是从缓冲区头部开始的话,可以设置这个参数来指定。设置该参数为NULL,表示我们的数据从VBO的第一个字节开始。

现在VBO和VAO都设置完成,我们需要对它们进行解绑定,防止一不小心被哪里给更改了。


1

2

glBindBuffer(GL_ARRAY_BUFFER, 0);

glBindVertexArray(0);

到此,shader,VBO和VAO都准备好了。我们可以开始在Render函数里绘制了。

首先,我们先清空下屏幕,让它变成纯黑色:


1

2

glClearColor(0, 0, 0, 1); // black

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

然后告诉OpenGL我们要开始使用VAO和shader了:


1

2

glUseProgram(gProgram->object());

glBindVertexArray(gVAO);

最后,我们绘制出三角形:


1

glDrawArrays(GL_TRIANGLES, 0, 3);

调用glDrawArrays函数说明我们需要绘制三角形,从第0个顶点开始,有3个顶点被发送到shader。OpenGL会在当前VAO范围内确定该从哪里获取顶点。

顶点将会从VBO中取出并发送到vertex shader。然后三角形内的每个像素会发送给fragment shader。接着fragment shader将每个像素变成白色。欢呼!

现在绘制结束了,为了安全起见,我们需要将shader和VAO进行解绑定:


1

2

glBindVertexArray(0);

glUseProgram(0);

最后一件事,在我们看到三角形之前需要切换帧缓冲:


1

glfwSwapBuffers(gWindow);

在帧缓冲被交换前,我们会绘制到一个不可见的离屏(off-screen)帧缓冲区。当我们调用glfwSwapBuffers时,离屏缓冲会变成屏幕缓冲,所以我们就能在窗口上看见内容了。

进一步阅读

在后续文章中,我们会对三角形进行贴图。之后,你会学到一点矩阵变换知识,就可以使用vertex shader来实现3D立方体旋转。

在这之后,我们开始创建3D场景并提交多个物体。

更多现代OpenGL资料

不幸的是,我不得不跳过很多内容,防止本教程的篇幅过长。后面还有很多好的现代OpenGL资料能满足你的求知欲:

An intro to modern OpenGL by Joe Groff of Durian Software

Learning Modern 3D Graphics Programming by Jason L. McKesson

A collection of simple single file OpenGL examples by Jakob Progsch

OpenGL Step by Step by Etay Meiri

All about OpenGL ES 2.x by Diney Bomfim

The OpenGL Progamming book on Wikibooks

Tutorials on the OpenGL wiki

OpenGL 4 tutorials by Donald Urquhart (Swiftless)

open.gl by Alexander Overvoorde

OpenGLBook.com by Eddy Luten

The official OpenGL SDK documentation

Compatibility tables for OpenGL, OpenGL ES, GLSL, and GLSL ES by Sugih Jamin

时间: 2024-08-29 16:13:16

1.openGL 初探的相关文章

<五>初探opengl,变换我们的图形

这节主要是对我们的纹理矩形进行一下变换,例如缩放,旋转,中间需要运用到一些线性代数的东西这里就不再阐述,因为我自己也不怎么会...我们直接介绍代码怎么写吧. 矩阵的相乘是从右往左读取的,这点提醒一下自己. GLM 代码中大部分的矩阵处理信息都是通过glm库来处理的,全称OPENGL MATHEMATICS ,去这里下载,把glm文件夹放到工程的include下即可.代码i引入文件库 #include <glm/glm.hpp> #include <glm/gtc/matrix_trans

&lt;五&gt;初探opengl,编写我们的镜头

现在我们尝试编写一个镜头类,有了这个类,我们能上下左右前后移动,感觉在玩fps游戏,很不错,下面开始看看怎么写. 初次接触镜头类是我在魔兽地图编辑中,当时创建一个镜头的步骤就是放到某个位置,调节角度,分别有3个角度可以调节,一个是类似高度一样的东西,一个是环绕着某个点的旋转角度,还有就是镜头的旋转.opengl镜头其实跟这个是差不多的. 1.首先我们需要定一个摄像机位置 ,也就是把摄像机放到什么位置去 2.然后我们要定一个目标位置,这个决定我们摄像机观察的方向 3.然后就是一个上向量,这个一般为

基于Cocos2d-x学习OpenGL ES 2.0系列——编写自己的shader(2)

在上篇文章中,我给大家介绍了如何在Cocos2d-x里面绘制一个三角形,当时我们使用的是Cocos2d-x引擎自带的shader和一些辅助函数.在本文中,我将演示一下如何编写自己的shader,同时,我们还会介绍VBO(顶点缓冲区对象)和VAO(顶点数组对象)的基本用法. 在编写自己的shader之前,我觉得有必要提一下OpenGL渲染管线. 理解OpenGL渲染管线,对于学习OpenGL非常重要.下面是OpenGL渲染管线的示意图:(图中淡蓝色区域是可以编程的阶段) 此图是从wiki中拿过来的

游戏开发中的矩阵初探

游戏开发中的矩阵初探 1.矩阵在3d空间中的作用 (1)长方体A想绕(10,3,4)旋转50°且沿着x方向放大2倍且向(9,-1,8)方向平移2个单位,那么经过上面的变换后,新的长方体各个点的坐标是多少呢?应用矩阵可以很轻松的算出答案. (2)知道子坐标系在父坐标系中的位置,可以求出子坐标系中的店在父坐标系中的位置. 2.矩阵的基础知识 矩阵能描述任意线性变换.线性变换保留了直线和平行线,线性变换保留直线的同时,其他的几何性质如长度.角度.面积和体积可能被变换改变了.简单的说,线性变换可能“拉伸

JavaFx初探

由于项目的须要,实在是没有办法了,试了非常多种方案(RCP,SWT,Flex,Smartinvoke...),终于还是决定開始研究JavaFx...为了给用户更好地体验我们的"智能家居"! 下面是近期搜索得到的一些资料: 1.Oracle 上 JavaFx的下载页面:http://www.oracle.com/technetwork/java/javafx/overview/index.html 眼下最新版本号的SDK是2.1 JavaFX 2.1 Developer Preview

OpenGL编程 基础篇(五)世界窗口和视口

一.基本定义 世界窗口:在世界坐标系中定义一个对齐的矩形(aligned rectangle,即矩阵的边与坐标轴平行)的窗口,这个世界窗口外的部分被裁减并不被绘制.OpenGL会自动地做剪裁. 视口:在显示器的屏幕窗口上定义一个对齐的矩形的视口,OpenGL会自动建立世界窗口和视口的变换(包括缩放和平移).当世界窗口中所有对象都被绘制时,对象在世界窗 口中的部分会被自动地映射到视口中----换句话说,被映射到屏幕坐标中,即像素在显示器上的坐标. 二.相关函数介绍 1.对于二维绘图来说,世界窗口由

进阶之初探nodeJS

一.前言 在"初探nodeJS"随笔中,我们对于node有了一个大致地了解,并在最后也通过一个示例,了解了如何快速地开启一个简单的服务器. 今儿,再次看了该篇随笔,发现该随笔理论知识稍多,适合初级入门node,固萌生一个想法--想在该篇随笔中,通过一步步编写一个稍大一点的node示例,让我们在整体上更加全面地了解node. so,该篇随笔是建立在"初探nodeJS"之上的,固取名为"进阶之初探nodeJS". 好了,侃了这多,那么我们即将实现一个

从273二手车的M站点初探js模块化编程

前言 这几天在看273M站点时被他们的页面交互方式所吸引,他们的首页是采用三次加载+分页的方式.也就说分为大分页和小分页两种交互.大分页就是通过分页按钮来操作,小分页是通过下拉(向下滑动)时异步加载数据. 273这个M站点是产品推荐我看的.第一眼看这个产品时我就再想他们这个三次加载和翻页按钮的方式,那么小分页的pageIndex是怎么计算的.所以就顺便看了下源码. 提到看源码时用到了Chrome浏览器的格式化工具(还是朋友推荐我的,不过这个格式化按钮的确不明显,不会的话自行百度). 三次加载和分

[转载]HDFS初探之旅

转载自 http://www.cnblogs.com/xia520pi/archive/2012/05/28/2520813.html , 感谢虾皮工作室这一系列精彩的文章. Hadoop集群(第8期)_HDFS初探之旅 1.HDFS简介 HDFS(Hadoop Distributed File System)是Hadoop项目的核心子项目,是分布式计算中数据存储管理的基础,是基于流数据模式访问和处理超大文件的需求而开发的,可以运行于廉价的商用服务器上.它所具有的高容错.高可靠性.高可扩展性.高