对opengl 3.1一点都不了解的情况下(之前根本没听说过)学习...

现代OpenGL教程 01——入门指南
招聘信息:
译序早前学OpenGL的时候还是1.x版本,用的都是glVertex,glNormal等固定管线API。后来工作需要接触DirectX9,shader也只是可选项而已,跟固定管线一起混用着。现在工作内容是手机游戏,又转到OpenGL ES,发现OpenGL的世界已经完全不同了,OpenGL ES 2.0版本开始就不再支持固定管线,只支持可编程管线。国内很多资料教程参差不齐,旧式接口满天飞。在看到这一系列教程,觉着挺好,就想着一边学顺便翻译下。毕竟手游市场的机遇和竞争压力都在同比猛涨,多了解OpenGL ES肯定没有坏处。浮躁功利的环境下更需要怀着一颗宁静致远的心去提高自身功底,长路漫漫,与君共勉。欢迎大家,这是现代OpenGL教程系列的第一篇。所有代码都是开源的,你可以在GitHub上下载:通过这篇教程,你将会学到如何在Windows下用Visual Studio 2013或Mac下用Xcode搭建OpenGL 3.2工程。该应用包含一个顶点着色器(vertex shader),一个片段着色器(fragment shader)和使用VAO和VBO来绘制的三角形。该工程使用GLEW来访问OpenGL API,用来处理窗口创建和输入,还有使用进行矩阵/矢量相关的数学运算。这听上去有点无聊,但搭建这样的工程确实挺麻烦的,尤其对于初学者。只要解决完这问题,我们就可以开始玩些有趣的东西了。[TOC]获取代码所有例子代码的zip打包可以从这里获取:。这一系列文章中所使用的代码都存放在:。你可以在页面中下载zip,加入你会git的话,也可以复制该仓库。本文代码你可以在source/01_project_skeleton目录里找到。使用OS X系统的,可以打开根目录里的opengl-series.xcodeproj,选择本文工程。使用Windows系统的,可以在Visual Studio 2013里打开opengl-series.sln,选择相应工程。工程里已包含所有依赖,所以你不需要再安装或者配置额外的东西。如果有任何编译或运行上的问题,请联系我。关于兼容性的提醒本文使用OpenGL 3.2,但我会尝试保持如下兼容:向后兼容OpenGL 2.1向前兼容OpenGL 3.X和4.X兼容Android和iOS的OpenGL ES 2.0因为OpenGL和GLSL存在许多不同版本,本文代码不一定能做到100%上述兼容。我希望能兼容99%,并且不同版本之间只要轻微修改即可。想要了解OpenGL和GLSL不同版本间的区别,这里很好得罗列了。Visual Studio下安装代码在Windows 7 32位系统,(免费)下创建和测试。你应该可以打开解决方案并成功编译所有工程。如果有问题请联系我,或者将补丁发我,我会更新工程。Xcode下安装Xcode工程实在OSX 10.10系统,Xcode 6.1下创建并测试的。打开Xcode工程应该可以成功编译所有目标。加入你无法成功编译请联系我。Linux下安装Linux是基于。我在Ubuntu 12.04下简单测试通过。安装GLM,GLFW和GLEW: sudo aptitude install libglm-dev libglew-dev libglfw-dev进入工程目录:cd platforms/linux/01_project_skeleto运行makefile:make运行可执行文件:bin/01_project_skeleton-debugGLEW, GLFW和GLM介绍现在你有了工程,就让我们开始介绍下工程所用到的开源库和为啥需要这些。是用来访问OpenGL 3.2 API函数的。不幸的是你不能简单的使用#include来访问OpenGL接口,除非你想用旧版本的OpenGL。在现代OpenGL中,API函数是在运行时(run time)确定的,而非编译期(compile time)。GLEW可以在运行时加载OpenGL API。允许我们跨平台创建窗口,接受鼠标键盘消息。OpenGL不处理这些窗口创建和输入,所以就需要我们自己动手。我选择GLFW是因为它很小,并且容易理解。是一个数学库,用来处理矢量和矩阵等几乎其它所有东西。旧版本OpenGL提供了类似glRotate, glTranslate和glScale等函数,在现代OpenGL中,这些函数已经不存在了,我们需要自己处理所有的数学运算。GLM能在后续教程里提供很多矢量和矩阵运算上帮助。在这系列的所有教程中,我们还编写了一个小型库tdogl用来重用C++代码。这篇教程会包含tdogl::Shader和tdogl::Program用来加载,编译和链接shaders。什么是Shaders?Shaders在现代OpenGL中是个很重要的概念。应用程序离不开它,除非你理解了,否则这些代码也没有任何意义。Shaders是一段GLSL小程序,运行在GPU上而非CPU。它们使用语言编写,看上去像C或C++,但却是另外一种不同的语言。使用shader就像你写个普通程序一样:写代码,编译,最后链接在一起才生成最终的程序。Shaders并不是个很好的名字,因为它不仅仅只做着色。只要记得它们是个用不同的语言写的,运行在显卡上的小程序就行。在旧版本的OpenGL中,shaders是可选的。在现代OpenGL中,为了能在屏幕上显示出物体,shaders是必须的。为可能近距离了解shaders和图形渲染管线,我推荐Durian Software的相关文章。那shaders实际上干了啥?这取决于是哪种shader。Vertex ShadersVertex shader主要用来将点(x,y,z坐标)变换成不同的点。顶点只是几何形状中的一个点,一个点叫vectex,多个点叫vertices(发音为ver-tuh-seez)。在本教程中,我们的三角形需要三个顶点(vertices)组成。Vertex Shader的GLSL代码如下:#version&150
void&main()&{
&&&&//&does&not&alter&the&vertices&at&all
&&&&gl_Position&=&vec4(vert,&1);
}第一行#version 150告诉OpenGL这个shader使用GLSL版本1.50。第二行in vec3告诉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 ShadersFragment shader的主要功能是计算每个需要绘制的像素点的颜色。一个"fragment"基本上就是一个像素,所以你可以认为片段着色器(fragment shader)就是像素着色器(pixel shader)。在本文中每个片段都是一像素,但这并不总是这样的。你可以更改某个OpenGL设置,以便得到比像素更小的片段,之后的文章我们会讲到这个。本文所使用的fragment shader代码如下:#version&150
out&vec4&finalC
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,而非全局状态的原因。也说VAO是必须的,但我只听说ATI驱动会抛错误。下面描述引用自所有与顶点处理有关的数据定义都应该封装在VAO里。 一般VAO边界包含所有更改vertex array状态的命令,比如VertexAttribPointer和EnableVertexAttribArray;所有使用vertex array进行绘制的命令,比如DrawArrays和DrawElements;所有对vertex array状态进行查询的命令(见第6章)。不管怎样,我也知道为啥会有人认为顶点属性应该放在VAO外部。glVertexAttribPointer出现早于VAO,在这段时间里顶点属性一直被认为是全局状态。你应该能看得出VAO是一种改变全局状态的有效方法。我更倾向于认为是这样:假如你没有创建VAO,那OpenGL通过了一个默认的全局VAO。所以当你使用glVertexAttribPointer时,你仍然是在VAO内修改顶点属性,只不过现在从默认的VAO变成你自己创建的VAO。这里有更多的讨论:代码解释终于!理论已经说完了,我们开始编码。OpenGL对于初学者而言不是特别友好,但如果你理解了之前所介绍的概念(shaders,VBO,VAO)那你就没啥问题。打开main.cpp,我们从main()函数开始。首先,我们初始化GLFW:glfwSetErrorCallback(OnError);
if(!glfwInit())
&&&&throw&std::runtime_error("glfwInit&failed");
glfwSetErrorCallback(OnError)这一行告诉GLFW当错误发生时调用OnError函数。OnError函数会抛一个包含错误信息的异常,我们能从中发现哪里出错了。然后我们用GLFW创建一个窗口。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上下文给刚创建的窗口:glfwMakeContextCurrent(gWindow);无论我们调用哪个OpenGL函数,都会影响到“当前上下文”。我们只会用到一个上下文,所以设置完后,就别管它了。理论上来说,我们可以有多个窗口,且每个窗口都可以有自己的上下文。现在我们窗口有了OpenGL上下文变量,我们需要初始化GLEW以便访问OpenGL接口。glewExperimental&=&GL_TRUE;&//stops&glew&crashing&on&OSX&:-/
if(glewInit()&!=&GLEW_OK)
&&&&throw&std::runtime_error("glewInit&failed");这里的GLEW与OpenGL内核有点小问题,设置glewExperimental就可以修复,但希望再未来永远不要发生。我们也可以用GLEW再次确认3.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。std::vector&
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:glGenVertexArrays(1,&&gVAO);
glBindVertexArray(gVAO);然后我们创建和绑定新的VBO:glGenBuffers(1,&&gVBO);
glBindBuffer(GL_ARRAY_BUFFER,&gVBO);接着,我们上传一些数据到VBO中。这些数据就是三个顶点,每个顶点包含三个GLfloat。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”。我们可以在后续的文章中看到更多类型。glEnableVertexAttribArray(gProgram->attrib("vert"));VAO设置最复杂的部分就是下个函数:glVertexAttribPointer。让我们先调用该函数,等会解释。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都设置完成,我们需要对它们进行解绑定,防止一不小心被哪里给更改了。glBindBuffer(GL_ARRAY_BUFFER,&0);
glBindVertexArray(0);到此,shader,VBO和VAO都准备好了。我们可以开始在Render函数里绘制了。首先,我们先清空下屏幕,让它变成纯黑色:glClearColor(0,&0,&0,&1);&//&black
glClear(GL_COLOR_BUFFER_BIT&|&GL_DEPTH_BUFFER_BIT);然后告诉OpenGL我们要开始使用VAO和shader了:glUseProgram(gProgram->object());
glBindVertexArray(gVAO);最后,我们绘制出三角形:glDrawArrays(GL_TRIANGLES,&0,&3);调用glDrawArrays函数说明我们需要绘制三角形,从第0个顶点开始,有3个顶点被发送到shader。OpenGL会在当前VAO范围内确定该从哪里获取顶点。顶点将会从VBO中取出并发送到vertex shader。然后三角形内的每个像素会发送给fragment shader。接着fragment shader将每个像素变成白色。欢呼!现在绘制结束了,为了安全起见,我们需要将shader和VAO进行解绑定:glBindVertexArray(0);
glUseProgram(0);最后一件事,在我们看到三角形之前需要切换帧缓冲:glfwSwapBuffers(gWindow);在帧缓冲被交换前,我们会绘制到一个不可见的离屏(off-screen)帧缓冲区。当我们调用glfwSwapBuffers时,离屏缓冲会变成屏幕缓冲,所以我们就能在窗口上看见内容了。进一步阅读在后续文章中,我们会对三角形进行贴图。之后,你会学到一点矩阵变换知识,就可以使用vertex shader来实现3D立方体旋转。在这之后,我们开始创建3D场景并提交多个物体。更多现代OpenGL资料不幸的是,我不得不跳过很多内容,防止本教程的篇幅过长。后面还有很多好的现代OpenGL资料能满足你的求知欲: by Joe Groff of Durian Software by Jason L. McKesson by Jakob Progsch by Etay Meiri by Diney Bomfim by Donald Urquhart (Swiftless) by Alexander Overvoorde by Eddy Luten by Sugih Jamin
微信扫一扫
订阅每日移动开发及APP推广热点资讯公众号:CocoaChina
您还没有登录!请或
点击量9023点击量8638点击量8003点击量5557点击量5194点击量5037点击量4331点击量4299点击量3859
&2016 Chukong Technologies,Inc.
京公网安备89OpenGL学习之路(一)
OpenGL学习之路(一)
虽然是计算机科班出身,但从小对几何方面的东西就不太感冒,空间想象能力也较差,所以从本科到研究生,基本没接触过《计算机图形学》。为什么说基本没学过呢?因为好奇(尤其是惊叹于三维游戏的逼真,如魔兽世界、极品飞车),在研究生阶段还专门选修计算机图形学,但也只是听了几堂课,知道了有帧缓存、齐次坐标等零零散散的概念,之后读了一篇论文并上台作报告(压根没读懂)。总之,当时只是觉得计算机图形学或三维渲染很牛,甚至问我什么是渲染都不知道,更不知道如何将3维几何体显示到2维屏幕上。令我现在想来非常可笑的是,当时以为2D图像才是平面的,3D图像就是立体的。
真正接触3维绘制方面的知识是在工作后,因为是搞图像处理与可视化的方面软件的开发,所以开始知道了这些3D图像是通过OpenGL/Direct3D等编程接口来做的。不过公司关于OpenGL接口的调用和绘制方面的代码是另一个组写的,博主基本看不到他们的代码。不过,与相关同事的讨论中还是学到了一些知识。于是,博主便考虑系统地学习一下OpenGL编程。
市面上最好的两本OpenGL的书应该是——《OpenGL编程指南》(红宝书)和《OpenGL编程宝典》(蓝宝书)。于是,就挑选了《OpenGL编程指南(第八版)》作为我的启蒙教材,由此踏上学习之路。本博客希望记录学习过程中的一些新的体会。记下来的知识才是自己的。闲话不多说,让我们踏上OpenGL学习之旅吧!
2 第一个例子
红宝书一开始在什么是OpenGL一节,简要介绍了OpenGL的概念、发展历程、渲染流程等概念。说实话,一开始读这段文字压根就是认字,概念各种不懂。不过没关系,博主觉得,大部分技术书籍的第一章都是这样——罗列一大堆概念,然后写一个简单的小程序,对它分析分析,继续列举更多概念,然后说明将在第几章第几节详细介绍。所以对于我这种初学者,看完第一章完全不知道说什么是也很正常,别急,慢慢的,某一刻你就理解了——所谓一通百通。
之后,咣当扔过来一个例子,这也许就是程序员的风格——先写个Demo看看。其实,刚开始对这个例子比较藐视,太简单了——和我脑海中高大上的游戏界面相去甚远。不过,其实这个例子中还是包含很多东西的。首先来看看,这个程序的运行效果。
就是在一个窗口中绘制两个蓝色的三角形。下面我们就按步骤来绘制这个图形。
第一步:搭一个框架
对于技术书籍,一开始看书就是照着书本敲代码,敲完代码再看代码有没有问题。这个例子虽然简单,但是代码不少。其实我们可以一步一步来写,首先写一个main函数,在里面填一些初始化和创建窗口的代码,如下:
1 #include &iostream&
2 #include &StdAfx.h&
4 void display()
8 int main(int argc, char **argv)
<span style="color:#
glutInit(&argc, argv);
<span style="color:#
glutInitDisplayMode(GLUT_RGBA);
<span style="color:#
glutInitWindowSize(<span style="color:#2, <span style="color:#2);
<span style="color:#
glutInitContextVersion(<span style="color:#, <span style="color:#);
<span style="color:#
glutInitContextProfile(GLUT_CORE_PROFILE);
<span style="color:#
glutCreateWindow(argv[<span style="color:#]);
<span style="color:#
<span style="color:#
if (glewInit())
<span style="color:#
<span style="color:#
std::cerr && &Unable to initialize GLEW... Exiting...& && std::
<span style="color:#
std::exit(EXIT_FAILURE);
<span style="color:#
<span style="color:#
<span style="color:#
glutDisplayFunc(display);
<span style="color:#
glutMainLoop();
<span style="color:# }
&其实这里只是涉及到OpenGL的API,只是用到了第三方库一些函数创建了一个显示图像的窗口。就像我们开始写控制台Demo的时候,先写main函数,然后打印一个“HelloWorld”出来,看看能不能跑。上面这短短的几行代码是能够运行的。运行结果就是一个大小为512×512的空白窗口。代码很简单,就是初始化相关的函数。这里需要说明一下的是:#include &StdAfx.h&是从红宝书的网站上下载下来vgl.h文件,并配置了工程的属性(如头文件目录、lib库目录);另外一个就是:display函数就是我们要调用OpenGL绘制图像的函数。下面就是填充这个函数。
第二步:填充框架
和任何程序一样,OpenGL程序需要输入,然后经过渲染管线,即一系列的着色器(着色器贯穿本书的始终),最后得到一个二维图像(像素矩阵),见下图。
所以在调用OpenGL API进行绘制图像之前,先将所需数据加载到显存中,以便于OpenGL在绘制时对其进行相关处理。填充后的代码如下:
1 #include &iostream&
2 #include &StdAfx.h&
4 GLuint Buffer_ID;
5 const int BUFFER_NUMBER = <span style="color:#;
7 GLuint VAO_ID;
8 GLuint VAO_NUMBER = <span style="color:#;
<span style="color:# const int VERTICES_NUMBER = <span style="color:#;
<span style="color:# const int vPosition = <span style="color:#;
<span style="color:#
<span style="color:# void Initialize()
<span style="color:# {
<span style="color:#
//---------------------准备数据-------------------------------
<span style="color:#
GLfloat vertices[VERTICES_NUMBER][<span style="color:#] =
<span style="color:#
<span style="color:#
{ -<span style="color:#.90, -<span style="color:#.90 },
<span style="color:#
<span style="color:#.85, -<span style="color:#.90 },
<span style="color:#
{ -<span style="color:#.90,
<span style="color:#.85 },
<span style="color:#
<span style="color:#
<span style="color:#.90, -<span style="color:#.85 },
<span style="color:#
<span style="color:#.90,
<span style="color:#.90 },
<span style="color:#
{ -<span style="color:#.85,
<span style="color:#.90 }
<span style="color:#
<span style="color:#
<span style="color:#
// 生成缓存对象
<span style="color:#
glGenBuffers(BUFFER_NUMBER, &Buffer_ID);
<span style="color:#
<span style="color:#
// 绑定缓存对象
<span style="color:#
glBindBuffer(GL_ARRAY_BUFFER, Buffer_ID);
<span style="color:#
<span style="color:#
// 填入数据
<span style="color:#
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
<span style="color:#
<span style="color:#
//-------------------设置顶点数据属性------------------------------
<span style="color:#
// 生成顶点数组对象
<span style="color:#
glGenVertexArrays(VAO_NUMBER, &VAO_ID);
<span style="color:#
<span style="color:#
// 绑定顶点数组对象
<span style="color:#
glBindVertexArray(VAO_ID);
<span style="color:#
<span style="color:#
// 设置顶点属性
<span style="color:#
glVertexAttribPointer(vPosition, <span style="color:#, GL_FLOAT, GL_FALSE, <span style="color:#, BUFFER_OFFSET(<span style="color:#));
<span style="color:#
glEnableVertexAttribArray(vPosition);
<span style="color:# }
<span style="color:#
<span style="color:# void display()
<span style="color:# {
<span style="color:#
glClear(GL_COLOR_BUFFER_BIT);
<span style="color:#
<span style="color:#
glBindVertexArray(VAO_ID);
<span style="color:#
glDrawArrays(GL_TRIANGLES, <span style="color:#, VERTICES_NUMBER);
<span style="color:#
<span style="color:#
glFlush();
<span style="color:# }
<span style="color:#
<span style="color:# int main(int argc, char **argv)
<span style="color:# {
<span style="color:#
glutInit(&argc, argv);
<span style="color:#
glutInitDisplayMode(GLUT_RGBA);
<span style="color:#
glutInitWindowSize(<span style="color:#2, <span style="color:#2);
<span style="color:#
glutInitContextVersion(<span style="color:#, <span style="color:#);
<span style="color:#
glutInitContextProfile(GLUT_CORE_PROFILE);
<span style="color:#
glutCreateWindow(argv[<span style="color:#]);
<span style="color:#
<span style="color:#
glewExperimental = TRUE;
<span style="color:#
if (glewInit())
<span style="color:#
<span style="color:#
std::cerr && &Unable to initialize GLEW... Exiting...& && std::
<span style="color:#
std::exit(EXIT_FAILURE);
<span style="color:#
<span style="color:#
<span style="color:#
Initialize();
<span style="color:#
glutDisplayFunc(display);
<span style="color:#
glutMainLoop();
<span style="color:# }
在原有基础上,添加了加载数据部分和绘制图形部分的代码。主要分了两个步骤:
1. 数据输入步骤
任何系统都有输入输出(I/O)系统,如计算机硬件系统中有输入设备和输出设备;每一个编程语言都有自己的输入命令(类)和输出命令(类);对于一个算法来说,也有其输入和输出。
对于我们图形绘制系统来说,自然也少不了输入和输出。由于数据的输入只需要执行一次就可以,故写在Initialize函数中,并在main函数是执行。
本例要绘制两个三角形,输入的数据自然就是两个三角形的顶点数据。由于绘制的是平面三角形,我们可以不指定z方向的坐标&#20540;(深度&#20540;)。16~25行的二维顶点数组是存放在内存中的,图形绘制是在显卡中执行的,所以需要将这些数据加载到显存中。这里出现了OpenGL编程中第一个重要的概念——缓存对象(Buffer Object)。顾名思义,这一对象主要就是用来存放数据的,在这里,我们使用缓存来存放顶点数据。下面,我们来看看程序中是怎么使用缓存对象来加载顶点数据的。
加载顶点数据到显存用了3条OpenGL API来实现数据的加载。
I:使用glGenBuffer声明一个缓存对象ID。编程语言中通过变量的方式来标识内存中的数据;操作系统中通过各种ID来感知各个实体,如进程标识符PID来标识进程,线程标识符TID来标识线程。OpenGL也是通过ID来标识各种对象。由于这里只要使用一个缓存对象,所以只要生成一个缓存ID即可,但要注意,这条指令可以生成多个缓存对象。
II:使用glBindBuffer来绑定其中一个缓存。刚才已经提到,缓存对象可以有多个,那OpenGL怎么知道要当前操作的是哪个缓存对象呢?这就需要使用glBindBuffer命令——这个命令的作用就是激活(Activate)其中一个缓存对象。参数很简单,就是刚才生成的缓存ID。
III:使用glBufferData来分配内存并拷贝数据到显存。这一步是我们最终目的——将数据从内存拷贝至显存。这个函数和C语言中内存拷贝memcpy很类&#20284;,函数签名为:
void glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
target  ——刚才绑定(激活)的缓存对象,这可以看做memcpy的目的地址;
size   ——这就是数据的大小,这和memcpy中的数据大小是一样的;
data   &——源数据的指针,这和memcpy中的源数据指针是一样的;
usage  ——这个参数指定这个数据的用法,主要是为了优化OpenGL的内存管理——根据使用方法确定最优显存分配方案。
通过使用上述三条OpenGL API,我们就完成了数据从内存加载到缓存的功能。到此为止,故事还没有结束,OpenGL在获取顶点数据时并不知道缓存对象中的数据如何解析,所以需要告诉OpenGL,刚才上传的数据的&#26684;式是怎么样的。这就引入了第二个对象——顶点数组对象及顶点属性的概念。顶点数组对象就是用来描述刚才上传的顶点数据特征的一个对象,下面就继续来分析与顶点数组对象的相关API。
I:使用glGenVertexArray声明一个顶点数组对象ID。这和缓存对象ID是一样的,都是为了便于OpenGL的组织管理;
II:使用glBindVertexArray来激活其中的一个顶点数组对象,和缓存对象也是类&#20284;的;
III:使用glVertexAttribPointer接口来填充当前绑定的顶点数组对象。这个函数的功能和缓存对象的glBindBuffer命令是一样的,只是对于缓存对象来说,只要拷贝一下数据就可以了,而这里需要填充顶点属性数据(就像填充一个结构体一样)。这个函数的参数比较多,其函数签名为:
void glVertexAttribPointer(GLuint index, GLsize size, GLint size, GLenum type, GLboolean normalized, GLsizei stribe, const GLvoid *pointer);
index   &  ——这是指定在该顶点在着色器中的属性。
size   &    &——该参数指定了每个顶点有几个分量,本例中二维顶点,故设为2;
type   &   &——该参数指定了顶点中分量的数据类型,这里顶点的坐标分量是浮点型数据,故设为GL_FLOAT;
normalized  ——该参数表示顶点存储前是否需要进行归一化;
stride     &——该参数指定两个顶点数据之间间隔的字节数,在本例中,顶点是连续存储的,故设为0;
pointer    ——顶点数据在缓存对象中起始地址,在本例中,因为缓存对象中只存放了一个顶点数组,所以这一&#20540;设为0。
IV: 使用glEnableVertexAttribArray来启用与index索引相关联的顶点数组。虽然前面设置了顶点数组属性,但如果没有启用的话,数据依然无法被OpenGL拿到。
2. 图形绘制步骤
数据及其&#26684;式设置后之后,就是根据这一数据进行图形的绘制。这部分代码是写在display函数中的,这一函数可能会调用多次。在这个显示函数中,最重要的一个OpenGL API就是glDrawArray函数——绘制基本图形,其函数签名如下:
void glDrawArray(GLenum mode, GLint first, GLsizei count);
mode  ——指定你要绘制的图元类型,比如三角形是GL_TRIANGLES,直线就是GL_LINES,闭合的直线就是GL_LINE_LOOP,顶点就是GL_POINTS。本例中要绘制三角形,故设为GL_TRIANGLES。
first   ——指定绘制图形时的起始顶点,本例中从第0个顶点开始;
count  ——要绘制的顶点数,本例中设置为6。
给这个函数设置不同的&#20540;,将出现不同的效果——可以使用不同的顶点来绘制不同的图形。
剩下的,三个接口:
glClear(GLbitfield mask);
清空指定的缓存数据。每一次新的绘制,当然需要将上一次绘制过程中产生的一些数据给清空,以防止其对后一次绘制产生影响。在OpenGL中有三种缓存数据,分别是颜色缓存,深度缓存和模板缓存。其中深度缓存只有在三维的情形中才用到。本例中清空了颜色缓存。
glFlush();
这个接口是一个同步接口——等待绘制完成再往下执行。这里需要说明的是,OpenGL采用的是客户机-服务器模式运作的——我们的应用程序就是客户机,显卡就是服务器。每一次执行OpenGL API相当于给显卡发送一条命令,一般情况下,这些命令是以异步的方式执行的。如果我们应用程序需要等显卡命令执行完毕才能往下执行,就需要调用这个函数。
最后一个,glBindVertexArray——绑定操作对象,即glDrawArray绘制的是当前绑定的顶点数组。在本例中(只限本例)是可以不调用的,因为在Initialize函数中已经调用过了,并且display函数中没有其他的绑定。
至此,我们运行程序,应该能够看到绘制出来的是两个白色的三角形。
第三步:添加着色器
我们先来看看OpenGL中的绘制管线,如下图所示:
所谓绘制管线,就是OpenGL在绘制图像过程中所经过的操作步骤,主要有:求&#20540;器、逐顶点操作、图元装配、纹理贴图、光栅化、片元操作等等。这样的绘制管线称为固定绘制管线,因为一旦绘制开始,绘制过程人为无法干预。
着色器的引入,将绘制管线从固定的管线变为可编程的绘制管线。所谓可编程,是指在绘制固定绘制管线的过程上,可以加入我们的逻辑。什么意思呢?相当于OpenGL提供给我们一个编程框架(而不仅仅是一套API),我们可以定制其中的某些部分。这样,可以更灵活的控制绘制管线,实现更好的绘制效果。这样就得到了下面的绘制管线:
可以看出,在固定管线的基础上,增加了顶点着色器、几何着色器、片元着色器,其中顶点着色器和片元着色器最重要。顶点着色器是对输入顶点进行处理的,如对顶点进行三维变换,添加颜色等;片元着色器则是对光栅化后的片元进行处理,如纹理贴图、执行光照计算等等,也就是计算渲染颜色的(我想这也是着色器最根本的含义吧!)。这些着色器使用GLSL语言写的,这个语言语法和C语言很类&#20284;,并定义了一些内置变量和便于我们处理的接口API。
为了将上述白色三角形变为蓝色,我们可以为其添加一个片元着色器,着色器输出的就是片元的颜色。代码很简单,就是输出一个颜色&#20540;,如下:
<span style="color:# #version <span style="color:#0 core
<span style="color:#
<span style="color:# out vec4 fC
<span style="color:#
<span style="color:# void main()
<span style="color:# {
<span style="color:#
fColor = vec4(<span style="color:#.0, <span style="color:#.0, <span style="color:#.0, <span style="color:#.0);
<span style="color:# }
很简单,第1行是GLSL的版本信息。第3行表明该着色器有一个输出——计算后的颜色,在main函数中,对该输出变量赋&#20540;一个4维颜色向量,R通道&#20540;为0.0,G通道&#20540;为0.0,B通道&#20540;为1.0,A通道&#20540;为1.0(透明度),所以最后颜色就是蓝色的。
写完这个着色器程序,显卡并不知道这个着色器的存在,因此还需要一些步骤——对着色器的编译、链接并加载到显卡中。这部分内容蛮多的,我们直接使用本书提供的代码,LoadShader函数来加载着色器程序,就是在Initialize函数的最后加入下面这段代码:
<span style="color:# ShaderInfo shaders[] = {
<span style="color:#
{ GL_FRAGMENT_SHADER, &triangles.frag& },
<span style="color:#
{ GL_NONE, NULL}
<span style="color:#
<span style="color:#
GLuint program = LoadShaders(shaders);
<span style="color:#
glUseProgram(program);
最后,书上还给出了一个顶点着色器,其实这个顶点着色器可以不用的,就是将输入的顶点坐标设置给内置变量gl_Position,有和没有效果都是一样的,为了完整性还是把它贴上来吧!
<span style="color:# #version <span style="color:#0 core
<span style="color:#
<span style="color:# layout(location = <span style="color:#) in vec4 vP
<span style="color:#
<span style="color:# void main()
<span style="color:# {
<span style="color:#
gl_Position = vP
<span style="color:# }
当然,也要把它加载到显卡中。至此,一个简单的OpenGL程序就写完了,其实修改一下顶点数据或者修改一下着色器程序,我们可以画出其他一些效果出来。比如可以画一个五角星出来,或者画一个彩色的图形出来。
最后,总结一下:主要学习了缓存对象和顶点数组对象的创建,向缓存对象拷贝数组数据,设置顶点数组对象属性的相关接口。最后了解了OpenGL渲染管线——固定渲染管线和可编程渲染管线,在我们的程序中加入了片元着色器和集合着色器。
我的热门文章
即使是一小步也想与你分享

我要回帖

更多关于 opengl 3.1 的文章

 

随机推荐