龙岗网站建设公司哪家口碑好,网站做友链有什么用,wordpress2017备案号,网站建设相关的工作The Cherno——OpenGL
1. 欢迎来到OpenGL
OpenGL是一种跨平台的图形接口#xff08;API#xff09;#xff0c;就是一大堆我们能够调用的函数去做一些与图像相关的事情。特殊的是#xff0c;OpenGL允许我们访问GPU#xff08;Graphics Processing Unit 图像处理单元API就是一大堆我们能够调用的函数去做一些与图像相关的事情。特殊的是OpenGL允许我们访问GPUGraphics Processing Unit 图像处理单元也就是显卡可以更好地绘制图形。 实际上为了利用在电脑或其他设备比如手机中强大的图形处理器需要调用一些API访问固件。OpenGL正好是允许访问和操作GPU的许多接口中的一种。当然还有其他接口Direct3D、Vulcan和Metal等。总的来说在一定程度上OpenGL允许我们控制显卡。
需要澄清一下一些人的误解许多人称它为一个类库或一种引擎或其他的比如框架但这些都不是OpenGL核心本身只是一种规范算起来跟CPP规范差不多。实际上它没有确定任何代码和类似的事情本身就是规范比如这个函数需要哪些参数返回什么值。它只是一种你能利用这种API干什么的规范没有任何具体的实现这意味它绝不是一个类库因为OpenGL本身没有代码它只是一种规范所以OpenGL本身不需要下载。那么谁去实现它呢谁去为你调用的OpenGL函数写代码呢答案是GPU制造商。如果你使用的是NVIDIA显卡那么你的显卡驱动也就是NAVIDIA驱动实际上包含了OpenGL的实现。
OpenGL的实现是每个显卡制造商比如AMD、Intel等都有自己的实现每一家关于OpenGL的实现都会有些不同。这也就是有些游戏能在NAVIDIA驱动的NVIDIA显卡上运行但在其他显卡设备上运行有些区别甚至出问题的原因。但不管怎么说关键在于你的显卡制造商实现的OpenGL。这又可能导致下一个有关OpenGL常见的误解它是开源的。它根本不是开源的你看不到OpenGL的源码。因为首先它是由GPU制造商实现的他们肯定不会公布他们的驱动源码。
总结就是OpenGL是在你的显卡驱动中实现的并且它只是我们去控制显卡的规范。
传统OpenGL和现代OpenGL之间的区别 OpenGL于90年代发布那时的GPU是不可编程的不能随心所欲尤其在使用这些底层的API时。但现在人们可以很大程度去控制它制造商给了程序员和开发者更多的控制权这显然很好因为我们可以为它做更多的优化。现代的OpenGL更像一个循环一个低级的法则它能给你比之前更多的控制权。传统的OpenGL更像是一套程序。 所以比如要画一个三角形并且想要添加光源基本上就是让光源等于true就可以激活OpenGL的光源然后就再告诉OpenGL光源加在哪里就可以了。所以传统的OpenGL真的很像一套预设很容易使用代码比较少。但这样就会造成不能给你太多的控制权。但其实我们是想要更多的控制权的。 传统OpenGL和现代OpenGL之间最大的区别是可编程的着色器。 着色器是程序它是运行在GPU上的代码。当我们使用cpp或Java等语言写代码时这些代码是运行在CPU上的但当我们开始处理图形的大部分时间里我们想要更为精确的控制显卡运行我们可能要将大部分代码从CPU转到GPU上因为它在GPU上运行更快这就是着色器存在的意义允许我们在GPU上运行代码。
为什么说OpenGL是一种图形API又说它本身不是而是一种规范 说OpenGL是一种图形API是因为它提供了一组函数和规范用于描述和操作图形硬件。然而它本身并不是API而是一种规范这个规范定义了函数和操作的方式以及它们应该如何工作。
2. 设置OpenGL和在C中创建一个窗口
GLFW是一个轻量级的类库它能为我们做的事就是创建窗口、创建OpenGL的上下文以及给我们访问一些像输入之类的基础东西。
当我们需要下载GLFW的二进制文件时可以选择32位或64位的但我们应该如何选择呢有些人可能会想我使用的是64位Windows系统所以就选择64位的Windows二进制文件。然而这是一个错误的想法。实际上需要哪种架构问题在于你项目需要构建的架构。如果我们设置我们的VS项目去构建win32或x86那么我们就需要下载32位的二进制文件。如果我们的应用架构的是64位或x64那么就需要选择64位二进制文件。所以实际上这个问题的关键在于架构你的应用的平台架构所以你如何编译你的应用不是你正在使用的操作系统当然64位的应用不会在一个32位操作系统上运行。但是如果在一个64位操作系统上就可以运行32位和64位的应用。所以实际上下载哪个取决于你在哪个平台编译你的应用。
3. 在C中使用现代OpenGL
当我们使用一些库函数时实际上我们需要去访问这些驱动取出函数并且调用它们。注意这里并不是字面上说的把函数取出来实际上需要做的就是得到函数声明然后设置链接链接到对应的函数上。所以我们需要访问驱动的动态链接库文件然后只检索库里面那些函数的函数指针这就是我们需要做的。
其实我们可以自己来实现这个事情但它有一些问题首先它不可能是跨平台的访问显卡驱动并从中取出这些函数我们需要使用一些win32接口调用当我们在Windows上的时候载入库或载入函数指针等等。但这并不太好因为它只能在Windows上用。第二个问题就是如果超过一千个函数或之类的一些事情那么我们需要手动去完成这些操作并且为它们写代码这将是一个糟糕的计划。
所以实际上我们做的就是我们实际上需要使用到一些库现在确实有一些现成的库可以帮助我们完成一些操作。
接下来将介绍另外一个库GLEW。基本上它能做的就是为你提供OpenGL接口规范各种函数声明、符号声明和常量等诸如此类的东西。 这个库的实际实现就是进入EDI在你使用的显卡驱动签名中查找对应的动态链接文件然后载入所有这些函数指针这就是它所能做的。不要认为这些库实现了一些函数或其他的东西它们并没有只是为了访问这些函数而这些函数早就以二进制的形式存在你电脑上了并且我们使用的这个库只是为我们做了一些事情比如GLEWOpenGL扩展管理和GLAD库OpenGL的一个比较特殊的扩展。 说glew 的实际实现就是进入 EDI在使用的显卡驱动签名中查找对应的动态链接文件然后载入所有这些函数指针有些过于准确但大致描述了glew库的实现原理。 具体来说GLEW库通过封装OpenGL的底层函数调用提供了一组易于使用的接口使得开发者可以方便地使用OpenGL的扩展功能。在实现过程中GLEW库需要加载OpenGL函数库如gl.dll或libgl.so并且需要查询和注册OpenGL扩展函数。 在Windows平台上GLEW库使用Windows API函数来访问和加载动态链接库并且通过查找和使用特定的函数指针来加载OpenGL函数。在Linux平台上GLEW库使用dlopen函数来加载OpenGL库文件并通过查找和使用特定的函数指针来加载OpenGL函数。 因此可以简单地说glew库的实现是通过进入EDI电子数据交换标准协议使用相应的显卡驱动签名查找并加载对应的动态链接文件然后载入所有这些函数指针从而使得开发者可以方便地使用最新的OpenGL功能而不需要关心底层的细节。 使用GLEW的注意事项
首先需要创建一个有效的渲染OpenGL上下文然后调用glewInit()去初始化扩展的入口。所以不能直接从GLEW中直接调用OpenGL函数直到你调用了glewInit()。在调用glewInit()之前需要先创建一个渲染OpenGL的上下文。当需要用到glew.h头文件时要在包含任何其他OpenGL相关的头文件之间就要先包含glew.h。也就是glew.h要放在前面。
glewInit()的返回值是一个整数如果是GLEW_OK则说明GLEW初始化成功。如果不是则初始化失败。 不要认为这两个都是静态库上面那个是动态库下面的是静态库。从大小就可以看出来。但从技术上说他们确实都是静态库。但如果需要链接dll。就要链接glew32.lib这个glew32s.lib是链接静态库时需要用到的。
总结 因为不同的平台OpenGL函数存放的地方会有所不同、文件结构也会有所不同所以需要有一种方式可以自动找到OpenGL函数。GLEW就应运而生GLEW库通过封装OpenGL的底层函数调用提供了一组易于使用的接口使得开发者可以方便地使用OpenGL的扩展功能。在实现过程中GLEW库加载OpenGL函数库并且查询和注册OpenGL扩展函数。
4. 顶点缓冲区和在现代OpenGL中画一个三角形
用现代OpenGL画三角形的话相比传统的OpenGL会比较复杂传统的只需要调用函数设置参数就行但现代的需要能够创建一个顶点缓冲区vertor buffer还要创建一个着色器shader。
buffer就是一块用来存字节的内存一个内存字节数组。顶点缓冲区跟C中的字符数组的内存缓冲区不太一样区别在于它是 OpenGL 中的内存缓冲区这意味着它实际上在显卡上在我们的VRAM显存中也就是Video RAM。所以使用现代的OpenGL表示三角形的话需要定义一些数据来表示三角形然后把它们放到显卡GPU的 VRAM 中还需要发出 DrawCall 指令这是一个绘制指令意思就是告诉电脑你的显存中有一堆数据读取它并把它绘制在屏幕上实际上我们还需要告诉显卡如何读取和解释这些数据以及如何把它放到我们屏幕上。当我们在CPU这边做了所有事情我们用C写的东西都是在CPU上运行的。当我们写完这些东西还要用某种方式告诉显卡一旦从CPU发出了DrawCall指令且一旦从显卡读到了这些数据我希望你在屏幕上给我绘制出三角形。因为我们需要告诉显卡它要做什么所以需要对显卡编程。并且还有着色器。
着色器是一个运行在显卡上的程序是一堆我们可以编写的且可以在显卡上运行的代码。 它可以在显卡上以一种非常特殊又非常强大的方式运行。
注意 OpenGL具体的操作就是一个状态机不需要把它看成对象或类似的东西。 我们所做的就是设置一系列的状态然后当我们说一些事情比如说给我绘制个三角形那是与上下文相关的先告诉它绘制三角形所需要的数据然后它才去绘制这种是分状态的。换句话说选择一个缓冲区和一个着色器让电脑帮我绘制一个三角形。电脑会根据你选择的缓冲区和着色器决定绘制什么样的三角形绘制在哪里等等。这就是OpenGL的原理它是一个状态机。
顶点缓冲区的好处 顶点缓冲区包含这些顶点的数据传到OpenGL的VRAM然后发出一个DrawCall指令电脑就可以根据缓冲区画出图形了。我们可以在gl渲染循环外面先定义好缓冲区在gl渲染循环中就可以绘制已经存在的数据了。如果出于某些原因我们需要改变数据帧或者其他东西我们也可以通过更新缓冲区来做到这一点。
查OpenGL文档的链接 https://docs.gl/。
#includeGL/glew.h
#include GLFW/glfw3.h
#includeiostreamint main(void)
{GLFWwindow* window;/* Initialize the library */if (!glfwInit())return -1;/* Create a windowed mode window and its OpenGL context */window glfwCreateWindow(640, 480, Hello World, NULL, NULL);if (!window){glfwTerminate();return -1;}/* Make the windows context current */glfwMakeContextCurrent(window);if (glewInit() ! GLEW_OK)std::cout Error! std::endl;std::cout glGetString(GL_VERSION) std::endl;float positions[6] { //三个顶点画一个三角形-0.5f, -0.5f,0.0f, 0.5f,0.5f, -0.5f};//在OpenGL中生成的所有东西都会被分配一个唯一的标识符它只是一个整数。比如0、1、2。0通常是一个无效状态但不都是。//但基本上会得到一个数字比如1、2、3等这是实际对象的id不管它是顶点缓冲区、顶点数组、着色器还是其他东西都会得到一个整数来代表它。//当想要使用这个对象时就用这个数字unsigned int buffer; //定义一个缓冲区无符号int类型用来存放缓冲区的地址glGenBuffers(1, buffer); //定义顶点缓冲区然后指定要多少个缓冲区因为我们只需要一个所以输入 1第二个参数需要一个无符号整型指针。因为这个函数的返回类型是 void所以函数不返回生成的缓冲区id我们要给它提供一个整数指针。函数会把id写入这个整数的内存这就是为什么需要指针。glBindBuffer(GL_ARRAY_BUFFER, buffer); //绑定缓冲区第一个参数是目标GL_ARRAY_BUFFER表示这是一个数组第二个参数是我们要绑定的缓冲区glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float),positions,GL_STATIC_DRAW); //第一个参数是target第二个参数是指我们希望缓冲区多大或者数据有多大/* Loop until the user closes the window */while (!glfwWindowShouldClose(window)) //glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出如果是的话该函数返回true然后游戏循环便结束了之后为我们就可以关闭应用程序了。{/* Render here */glClear(GL_COLOR_BUFFER_BIT);//传统的OpenGL绘制一个三角形只需要下列函数,通过指定三个顶点来画三角形/*glBegin(GL_TRIANGLES);glVertex2f(-0.5f, -0.5f); //按住altshiftglVertex2f( 0.0f, 0.5f);glVertex2f( 0.5f, -0.5f);glEnd();*//* Swap front and back buffers */glfwSwapBuffers(window); //交换颜色缓冲/* Poll for and process events */glfwPollEvents(); //glfwPollEvents函数检查有没有触发什么事件比如键盘输入、鼠标移动等然后调用对应的回调函数可以通过回调方法手动设置。我们一般在游戏循环的开始调用事件处理函数。}glfwTerminate(); //释放glfw分配的内存return 0;
}5. 在OpenGL中顶点的属性和布局
经过上一节的学习我们还需要学习两个东西顶点属性和着色器。顶点属性就是OpenGL管道的工作方式。
OpenGL管道的工作原理是我们为我们的显卡提供数据我们在显卡上存储一些内存它包含了我们想要绘制的所有数据然后我们使用一个着色器利用在显卡上执行的程序来读取数据并且完全显示在屏幕上。通常我们画几何图形的方式就是使用一个叫顶点缓冲区的东西这基本上是存储在显卡上的内存缓冲区。所以当对着色器编程时实际上是从读取顶点缓冲区开始的。当然着色器需要知道缓冲区的布局还需要知道是否有纹理坐标、法线之类。目前这个缓冲区包含的就是一堆的浮点数它们指定了每个顶点的位置。
实际上我们不得不告诉OpenGL内存中有什么又是如何布局的。如果我们不这样做OpenGL看到的只是一堆字节。glVertexAttribPointer可以为我们做这件事只有让OpenGL知道了我们的内存布局它才知道怎么去正确地解析它。 index是需要修改的顶点属性的索引。着色器读取信息的方式就是通过索引索引是一个快照。index几乎像一个数组但里面的类型可能不同。一个索引表示实际引用的是哪个属性。顶点不仅由位置组成有可能还有纹理坐标、法线、颜色等等。这些中的每一个就和位置一样都是一个属性。所以index就是告诉我们这个属性的索引是什么。一般来说如果我们有一个位置例如在索引0处我们需要把它作为索引0来引用。
size是每个通用顶点属性的组件数顶点属性总的要用多少个分量来表示它跟实际大小和字节没有关系或者是说和它们实际占用了多少内存也没关系。它基本上是计数。
type是类型比如我们实际的顶点位置GL_FLOAT浮点型。则size表示的就是浮点数的数量。例如我们每个顶点是由两个浮点数组成则size是2。因为我们提供了一个两个分量的向量来表示我们的位置如果我们切换到3D坐标系则size为3因为是xyz。
normalized标准化如果我们处理的是浮点数。则不需要规范化因为它们已经被规范化了。不需要规范化的话这个参数可以为GL_FALSE。
stride步幅就是每个顶点之间的字节数。假如用上面的例子一个顶点三个属性位置、纹理坐标和法线假如位置是一个三个分量浮点数4字节的向量纹理坐标是一个两个分量的矢量或两个浮点数法线是三个分量。这样子一个顶点加起来就有32字节这就是stride它是每个顶点的字节大小。有了stride系统就知道从索引0到索引1要增加32字节。
pointer指针是指向实际属性的指针。例如上面的例子中因为顶点中位置这个属性在32字节的开始所以位置的pointer为0如果是纹理坐标则它的pointer为12因为从开始到纹理坐标有12字节的距离法线的话为20。但事实上这些不用我们自己去计算我们可以使用宏偏移量。
注意 当要启用或禁用通用的顶点属性数组时要调用glEnableVertexAttribArray()。这个函数只需要传入index就可以。通用顶点的索引绝对是启用或禁用的。
OpenGL是一个状态机它不会去检查glVertexAttribPointer()是否被启用。也不会去检查它要是没启用会有什么关系。glEnableVertexAttribArray()在任何地方调用都可以只要缓冲区已经绑定。
6. 在OpenGL中着色器的原理
当我们没有提供自己的着色器时其实也可以画出图形。因为一些显卡驱动实际上会为你提供一个默认的着色器。但这会出现问题就是有可能不同的显卡会出现不一样的效果。
一个着色器基本上就是一个运行在你的显卡上的一个程序代码。而为什么我们一定要在代码能在显卡上运行呢因为我们是在学图形编程显卡在这方面发挥了重要作用。还有就是我们想要能够为显卡编程是因为我们希望能够告诉显卡该做什么想要利用显卡的能力在屏幕上绘制图形。但这并不意味着我们所作的一切都得在显卡上做或必须以着色器的形式使用显卡CPU在某些方面还是更快。有时我们更喜欢在CPU上去干一些事情可能只是将结果数据发送给显卡同时仍然在CPU上进行处理。
虽然话虽如此但有些事情是不可否认的图形编程只很多都与图形相关显卡的速度要快得多。所以还是需要用到着色器。不仅仅在我们想要把东西从CPU上拿出来放到显卡上从根本上我们得给显卡编程因为即使在画一个简单三角形的时候还是需要能够告诉显卡如何画这个三角形例如就像顶点在哪是什么颜色应该怎么画。我们需要去告诉显卡如何处理我们发送的数据。这就是着色器的本质。
两种比较常见的着色器顶点着色器和片段着色器也称为像素着色器还有其他的曲面着色器、几何着色器、计算着色器等。
图形渲染管道的大概流程假如我们是绘制一个三角形 在CPU上写了一堆数据后向显卡发送了一些数据绑定某些状态之后我们会发出一个叫做DrawCall指令就进入了着色器的阶段处理DrawCall指令并在屏幕上绘制一些东西就能看到一个三角形了。这个特定的过程基本上就是渲染管道。 我们如何在屏幕上从有数据到有结果的当显卡开始绘制三角形是着色器就派上用场了。
顶点着色器和片段着色器是顺着管道的两种不同的着色器类型。所以当我们真正发出DrawCall指令时顶点着色器会被调用然后就会调用片段着色器然后我们就会在屏幕上看到结果。为了简单起见这中间省略了很多东西在顶点着色器调用之前有很多处理在调用顶点和片段着色器之间也有很多处理以及在片段着色器和光栅化阶段之间也有很多处理。所以从发出DrawCall指令到顶点着色器再到片段着色器最终为了能在屏幕上看到像素。
那么顶点着色器干了什么它会被我们试图渲染的每个顶点调用。例如我们要画一个三角形的话有三个顶点这就意味着顶点着色器会被调用三次每个顶点调用一次并且顶点着色器的主要目的是告诉OpenGL顶点在屏幕空间的什么位置如果有必要我们需要能够提供一些转换以便OpenGL能把这些数字转换成屏幕坐标。
着色器只是一个程序和实际的图形没有任何关系。顶点着色器会把我们在缓冲区中指定的顶点属性包含进去。所有的顶点着色器只是为了指定你想要的位置的方式被用来解析数据从属性到下一阶段。延续上面简化的过程的话下一个阶段是片段着色器像素着色器 片段着色器会为每个需要光栅化的像素运行一次。
例如当我们画一个三角形的时候我们指定的那三个顶点组成三角形需要用实际的像素填充这就是光栅化阶段做的事。并且片段着色器就是对三角形中需要填充的每个像素调用一次并且片段着色器的主要目标是决定这个像素应该是什么颜色。
但有一个性能优化的问题就是例如还是在这个三角形中因为顶点只有三个所以只会调用顶点着色器三次而像素点的数量会取决于三角形的大小如果三角形比较大则像素点会比较多而每个像素都会调用一次片段着色器。所以有时可以考虑一些事情在顶点着色器处理就好不用去到片段着色器。片段着色器里的东西代价要高得多因为片段着色器会为每个像素运行。不过话虽如此有些东西显然需要像素计算比如说光源计算光源时每个像素都有一个颜色值这个值是由很多东西决定的例如光源、环境、纹理、提供给表面的材质等这些一起来确定一个特定像素的正确颜色。
片段着色器的作用就是精确到每个像素的颜色根据一些输入例如相机的位置在哪、所有的表面属性、环境属性等所有汇聚在一起在片段着色器中确定单个像素的颜色。片段着色器是一个程序运行来确定一个像素应该是什么颜色的。一旦片段着色器计算出结果你的颜色基本上会出现在屏幕上。
总结顶点着色器为每个顶点运行决定了它们在屏幕上的位置。片段着色器为每个像素运行决定了颜色输出。
OpenGL着色器中的一切都是基于状态机工作的这意味着当你想画一个三角形时需要把数据从CPU发给GPU我们设置了所有的状态启用着着色器然后画出三角形。这就是OpenGL的工作方式。
#includeGL/glew.h
#include GLFW/glfw3.h
#includeiostreamint main(void)
{GLFWwindow* window;/* Initialize the library */if (!glfwInit())return -1;/* Create a windowed mode window and its OpenGL context */window glfwCreateWindow(640, 480, Hello World, NULL, NULL);if (!window){glfwTerminate();return -1;}/* Make the windows context current *///创建OpenGL的上下文glfwMakeContextCurrent(window);if (glewInit() ! GLEW_OK)std::cout Error! std::endl;std::cout glGetString(GL_VERSION) std::endl;float positions[6] { //三个顶点画一个三角形-0.5f, -0.5f,0.0f, 0.5f,0.5f, -0.5f};//在OpenGL中生成的所有东西都会被分配一个唯一的标识符它只是一个整数。比如0、1、2。0通常是一个无效状态但不都是。//但基本上会得到一个数字比如1、2、3等这是实际对象的id不管它是顶点缓冲区、顶点数组、着色器还是其他东西都会得到一个整数来代表它。//当想要使用这个对象时就用这个数字unsigned int buffer; //定义一个缓冲区无符号int类型用来存放缓冲区的地址glGenBuffers(1, buffer); //定义顶点缓冲区然后指定要多少个缓冲区因为我们只需要一个所以输入 1第二个参数需要一个无符号整型指针。因为这个函数的返回类型是 void所以函数不返回生成的缓冲区id我们要给它提供一个整数指针。函数会把id写入这个整数的内存这就是为什么需要指针。glBindBuffer(GL_ARRAY_BUFFER, buffer); //绑定缓冲区第一个参数是目标GL_ARRAY_BUFFER表示这是一个数组第二个参数是我们要绑定的缓冲区glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW); //第一个参数是target第二个参数是指我们希望缓冲区多大或者数据有多大//以上从40行开始就是我们用来给OpenGL传数据的所有代码。然而当我们给OpenGL数据时我们并没有告诉它数据是什么。//我们需要告诉OpenGL我们的数据是怎么布局的同时需要为缓冲区发出一个DrawCall指令。//启用顶点属性数组glEnableVertexAttribArray(0);//告诉OpenGL数据的内存布局glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);/* Loop until the user closes the window */while (!glfwWindowShouldClose(window)) //glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出如果是的话该函数返回true然后游戏循环便结束了之后为我们就可以关闭应用程序了。{/* Render here */glClear(GL_COLOR_BUFFER_BIT);//传统的OpenGL绘制一个三角形只需要下列函数,通过指定三个顶点来画三角形/*glBegin(GL_TRIANGLES);glVertex2f(-0.5f, -0.5f); //按住altshiftglVertex2f( 0.0f, 0.5f);glVertex2f( 0.5f, -0.5f);glEnd();*///这个函数可以为缓冲区发出DrawCall指令这是一个没有索引缓冲区时可以用的方法因为我们没有索引缓冲区所以用这个方法//第一个参数指定绘制模式指定什么图元第二个参数用来指定使用的数组中的起始索引从0开始-0.5f, -0.5f这样算一个索引//第三个参数是要渲染的索引的数量因为是三角形有三个顶点所以是3个索引glDrawArrays(GL_TRIANGLES, 0, 3);//这是一个有索引缓冲区时使用的函数第二个参数是指有多少个索引数据第三个参数是indices这个几乎不用所以写NULL//glDrawElements(GL_TRIANGLES,3,null)/* Swap front and back buffers */glfwSwapBuffers(window); //交换颜色缓冲/* Poll for and process events */glfwPollEvents(); //glfwPollEvents函数检查有没有触发什么事件比如键盘输入、鼠标移动等然后调用对应的回调函数可以通过回调方法手动设置。我们一般在游戏循环的开始调用事件处理函数。}glfwTerminate(); //释放glfw分配的内存return 0;
}7. 在OpenGL中写一个着色器
着色器可以来自不同地方的变体可以从文件中读取它们也可以从网上下载作为二进制数据读入等多种方式可以编译着色器。进入着色器编译阶段。
在这一节中我们会为OpenGL提供一个字符串。归根结底我们还是需要为OpenGL提供一个字符串那就是你着色器的源码。
基本上我们需要向OpenGL提供我们实际的着色器源码文本我们想让OpenGL编译这个程序将顶点着色器和片段着色器链接到一个独立的着色器程序然后返回一个唯一标识符给我们这样我们就可以绑定这个着色器程序并使用它。
//创建着色器对象
static unsigned int CompileShader(unsigned int type, const std::string source) {//创建一个着色器对象unsigned int id glCreateShader(type);const char* src source.c_str();//将着色器的源代码传递给指定的着色器对象glShaderSource(id, 1, src, nullptr);//编译指定着色器对象中的源代码glCompileShader(id);//编译源代码不会返回任何数据。所以无法判断它是否出了问题int result;//获取着色器对象的特定参数值glGetShaderiv(id, GL_COMPILE_STATUS, result);if (result GL_FALSE) { //GL_FALSE等于0表示失败int length;glGetShaderiv(id, GL_INFO_LOG_LENGTH, length);//要创建一个长度为length的数组有两种方法//在堆上动态分配需要手动回收 char* messsage new char[length];//在栈上动态分配char* message (char*)alloca(length * sizeof(char));glGetShaderInfoLog(id, length, length, message);std::cout 编译(typeGL_VERTEX_SHADER ? vertex : fragment) 着色器失败 std::endl;std::cout message std::endl;glDeleteShader(id);return 0;}return id;
}//不希望此函数链接到其他编译单元或c文件所以设置为静态函数
//将着色器的源码作为字符串传入。vertexShader接收顶点着色器的源码fragmentShader接收片段着色器的源码
static unsigned int CreateShader(const std::string vertexShader, const std::string fragmentShader) {//创建着色器程序对象并返回一个可用于引用的非0值的函数。//着色器程序对象用于附加着色器对象并提供机制将指定的着色器对象链接到创建的着色器程序。unsigned int program glCreateProgram(); //如果使用OpenGL规范unsigned int应写成GLunit//调用自己定义的函数创建顶点着色器对象unsigned int vs CompileShader(GL_VERTEX_SHADER, vertexShader);//调用自己定义的函数创建片段着色器对象unsigned int fs CompileShader(GL_FRAGMENT_SHADER, fragmentShader);//把这两个着色器附加到我们的程序上glAttachShader(program, vs);glAttachShader(program, fs);//链接程序glLinkProgram(program);glValidateProgram(program);//删除着色器因为它们已经链接到一个program程序中所以可以删除这些中间文件glDeleteShader(vs);glDeleteShader(fs);return program;
}使用如下
//创建着色器//在C中字符串之间的连接也可以不使用加号std::string vertexShader #version 330 core\n\nlayout(location 0) in vec4 position; //layout(location) 限定符用于确保顶点数据与着色器中的变量正确对应从而实现数据的正确传递和处理\nvoid main()\n{\n gl_Position position;\n}\n;std::string fragmentShader #version 330 core\n\nlayout(location 0) out vec4 color; \nvoid main()\n{\n color vec4(1.0,0.0,0.0,1.0);\n}\n;unsigned int shader CreateShader(vertexShader, fragmentShader);//绑定着色器glUseProgram(shader);8. 自己的总结 说OpenGL是一种图形API是因为它提供了一组函数和规范用于描述和操作图形硬件。然而它本身并不是API而是一种规范这个规范定义了函数和操作的方式以及它们应该如何工作。 使用GLEW的注意事项
1首先需要创建一个有效的渲染OpenGL上下文然后调用glewInit()去初始化扩展的入口。所以不能直接从GLEW中直接调用OpenGL函数直到你调用了glewInit()。
2在调用glewInit()之前需要先创建一个渲染OpenGL的上下文。
3当需要用到glew.h头文件时要在包含任何其他OpenGL相关的头文件之间就要先包含glew.h。也就是glew.h要放在前面。 OpenGL管道的工作原理是我们为我们的显卡提供数据我们在显卡上存储一些内存它包含了我们想要绘制的所有数据然后我们使用一个着色器利用在显卡上执行的程序来读取数据并且完全显示在屏幕上。实际上我们不得不告诉OpenGL内存中有什么又是如何布局的。如果我们不这样做OpenGL看到的只是一堆字节。glVertexAttribPointer可以为我们做这件事只有让OpenGL知道了我们的内存布局它才知道怎么去正确地解析它。 着色器 着色器是运行在GPU上的程序可编程的有顶点着色器和片段着色器。顶点着色器是有多少个顶点就运行多少次而片段着色器是有多少个像素就运行多少次。 顶点着色器确定顶点的最终位置中一般以顶点位置、顶点法线、颜色、纹理坐标作为输入具体的视具体情况而定。而一般以gl_Position作为输出用于表示变换后的顶点位置。 片段着色器中确定每个像素的颜色一般以顶点属性如位置、法线、纹理坐标等和uniform变量作为输入。一般以像素颜色为输出。 关于有一个性能优化的问题 就是例如还是在这个三角形中因为顶点只有三个所以只会调用顶点着色器三次而像素点的数量会取决于三角形的大小如果三角形比较大则像素点会比较多而每个像素都会调用一次片段着色器。所以有时可以考虑一些事情在顶点着色器处理就好不用去到片段着色器。片段着色器里的东西代价要高得多因为片段着色器会为每个像素运行。 完整的图形渲染管线流程 1准备阶段初始化glfw设置窗口属性glfwWindowHint()、创建窗口glfwCreateWindow()、设置OpenGL渲染上下文、初始化glew和渲染之前告诉OpenGL渲染窗口的尺寸大小glfwGetFramebuffersize()和glViewPort(),把顶点数据传递给顶点着色器 2顶点着色器把一个单独的顶点作为输入确定每个顶点的位置。根据要求对每个顶点进行变换例如平移、旋转、缩放等 3图元装配将顶点着色器输出的所有顶点作为输入并将所有顶点装配成指定的图元形状。 4几何着色器把图元形式的一系列顶点的集合作为输入对图元进行下一步的变换和处理例如生成新的顶点、改变图元形状等。 5光栅化将图元映射为最终屏幕上相应的像素生成供片段着色器使用的片段。 6片段着色器计算每个像素的最终颜色包括纹理、材质、光照等。 7Alpha测试和混合这个阶段检测片段的对应的深度和模板(Stencil)值用它们来判断这个像素是其它物体的前面还是后面决定是否应该丢弃。这个阶段也会检查alpha值alpha值定义了一个物体的透明度并对物体进行混合(Blend)。Alpha测试是指当不同物体重叠且在屏幕的不同深度时会根据物体的深度和透明度对物体的不同显示或忽略。当两个物体一前一后时前面物体又是透明的话此时就会对它们进行混合。 纹理 为了能够把纹理映射到图形上我们需要指定图形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标用来标明该从纹理图像的哪个部分采样采集片段颜色。之后在图像的其他片段上进行片段插值。纹理坐标在x和y轴上范围为0到1之间注意我们使用的是2D纹理图像。使用纹理坐标获取纹理颜色叫做采样。它可以采用几种不同的插值方式所以我们需要自己告诉OpenGL该怎样对纹理采样。 对于纹理一定要指定这四个参数过滤器放大、缩小两个和环绕方式GL_TEXTURE_WRAP_S水平环绕、GL_TEXTURE_WRAP_T垂直环绕有点像x和y轴如果不指定就会得到一个黑色的纹理因为OpenGL有给它们默认值默认情况下就是黑色。 在坐标变换中通常会设定一个坐标范围再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器将它们变换为屏幕上的二维坐标或像素。将坐标变换为标准化设备坐标接着再转化为屏幕坐标的过程通常是分步进行的。过程通常是这样 局部坐标就是我们自己定义的坐标 —根据模型矩阵由平移、旋转和缩放操作组成— 世界坐标 —根据观察矩阵(用于定义虚拟摄像机的位置和朝向将场景从全局坐标系转换到摄像机的局部坐标系 — 观察坐标 —根据投影矩阵用于将三维场景中的物体坐标转换为二维屏幕坐标实现透视效果和深度感— 裁剪坐标经过裁剪和透视除法等操作将坐标变换为标准化设备坐标的范围-1.0,1.0 —视口变换和垂直翻转— 屏幕坐标。 以上状态是一个顶点在最终被转化为片段之前需要经历的。 摄像机 当讨论摄像机/观察空间时是在讨论一摄像机的视角作为场景远点所有顶点的坐标观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机需要确定它在世界空间中的位置、观察的方向、一个指向它右侧的向量以及一个指向它上方的向量。摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。 颜色 当在OpenGL中创建一个光源时希望给光源一个颜色利用颜色反射的定律我们先将光源设置为白色。然后用光源的颜色与物体的颜色值相乘所得到的就是这个物体所反射的颜色也就是我们所感知到的颜色。这里说的颜色都是一个向量。 光照 在OpenGL中当要使用光照时可以使用冯氏光照模型。该模型的主要结构由3个分量组成环境、漫反射和镜面光照。环境光照分量会永远给物体一些颜色不至于使物体完全黑暗漫反射光照是为了模拟光源对物体方向性的影响如物体的某部分越是正对着光源它就越亮镜面光照是为了模拟有光泽物体上面出现的亮点它的颜色相比于物体颜色会更倾向于光的颜色。 材质: 当描述一个表面时可以分别为三个光照分量环境、漫反射和镜面定义一个材质颜色。通过为每个分量指定一个颜色我们就能够对表面的颜色输出有细粒度的控制。再添加一个反光度分量结合上上述的三个颜色就有了全部所需的材质属性了。ambient环境材质向量定义了在环境光照下这个表面反射的是什么颜色通常与表面的颜色相同。diffuse漫反射材质向量定义了在漫反射光照下表面的颜色。漫反射颜色和环境光照一样也被设置为我们期望的物体颜色。specular镜面材质向量设置的是表面上镜面高光的颜色或者甚至可能反映一个特定表面的颜色。最后shininess影响镜面高光的散射/半径。 材质是用于描述物体的外观和反射特性的属性集合。它定义了物体的颜色、光照响应以及其他与表面特性相关的属性。