OpenGL超级宝典(第7版)笔记4 渲染管线介绍 清单2.3-2.7
文章目录
OpenGL超级宝典(第7版)笔记4 渲染管线介绍 清单2.3-2.71 OpenGL简介2 OpenGL渲染管线3 着色器shader4 使用着色器4.1编辑并编译着色器 清单2.3-2.54.2用我们的着色器程序渲染一个点 清单2.6-2.75 总结上一次我们已经将stratup() render() shutdown()添加到了我们通过GLFW建立的框架中去,其实就是建立了三个子函数。从这一篇开始我们将开始学习按照OpenGL超级宝典第7版的内容来进行推进,我将尽力将所有清单都进行一个实操,我会尽力解释每个函数的功能,还有整个流程的顺序(可能其中有些不太恰当的比喻),流程中各个环节的作用。本篇主要是介绍什么是OpenGL,什么是OpenGL的渲染管线,什么是着色器,以及整个程序都在干什么。
1 OpenGL简介
按标准的话说,OpenGL是一种接口,应用程序可以通过它来访问和控制其所在运行设备的图形子系统。简单的理解可以看成是别人提供的函数集(当然不仅仅是函数集),通过他提供的函数,我们就能控制设备的图形系统(让显卡算些东西,让设备显示一些东西)。当然这些函数并不是很简略(毕竟越是简略越缺乏应对复杂图形的能力),并不是调用一两个函数就能进行图像的绘制(对于新手而言绘制一个三角形都是很繁琐的),但是它提供了一个非常值得学习的程序运行流程,按照它的流程当处理很多的图形时可以大大提高运算的效率。
2 OpenGL渲染管线
当代生活中有很多的影视作品,游戏,示意图(房子,汽车等等),都是通过计算机渲染得到的图像(并不是真实拍摄的),其中绝大多数都是通过类似OpenGL的图形接口建立的渲染程序进行渲染的(比如DirectX标准下的很多API、KhronosGroup标准下的很多API),虽然随着图形学的发展,游戏中的人物模型从棱角分明逐渐的变的光滑,最新的光线追踪技术更是让渲染的结果进一步的接近真实的世界,但是看似光滑的表面在电脑看来仍是由传统的三角面组成的,因为当三角面足够多的时候,物体的表面就可以更加的接近光滑的曲面。另一个重要原因是,相对于复杂的曲面方程,计算机更适合计算相对简单的三角面(虽然同一个曲面可能由大量的三角面组成,但相对于电脑大量的计算核心和超高的计算频率,这些三角面也并不可怕)。所以我们的OpenGL渲染管线也是以处理三角面为主的。
首先OpenGL渲染管线就相当于一条流水线一样,将计算分成很多的环节,每个环节各司其职,这个环节的输出将变成下一个环节的输入,通过我们编程来将各种计算任务分配到各个环节中去,这样就形成了一个完整的渲染管线。
如果我们想把一个三角面(或者说是一个空间中的三角形)显示在屏幕上,我们要:
1,告诉程序三角形的三个顶点在哪里(顶点拾取:向管线中输入顶点)
2,我可能需要对三角形进行缩放,根据视角的不同进行旋转或是透视计算(顶点着色器:对顶点的位置等进行修改)
3,我可能需要在这个三角形中细分出更多的小三角形(有时候细分比直接通过顶点传入更省资源)
4,根据我们上面的设置自动计算生成细分的小三角形(曲面细分)
5,我们可能需要对这些小三角形顶点的位置等信息进行修改(细分曲面评估着色器)
6,我们可以对点,线,三角形,进行批量的几何处理,比如在所有传入的点的位置画一个小房子,这样可以实现素材的复用,你能看见游戏中很多的树、房子都是同样的美术素材,可能就是在这里进行的素材复用(几何着色器)
7,我们绘制的图形最终会以一个个像素的形式显示在屏幕上,程序需要计算哪些像素应该显示哪些应该不显示(光栅化)
8,我们在完成光栅化后还需要对每个像素进行计算,比如这个像素对应的是哪个三角形(三角形是否被其他三角形所遮挡),这个三角形是否进行了贴图,是否有光打在上面,是否位于阴影之中等等(片段着色器)
9,帧缓冲(帧缓冲:之前我们提到了的前、后缓冲)
3 着色器shader
如上所述,在管线中(或是理解为图形计算的流水线)有各种各样的环节,其中可编程的环节我们为其所写的小程序就叫做着色器。书上的定义是着色器(shader)为GPU的大量小型可编程处理器上运行的小程序(书第3页下半部分)。
4 使用着色器
4.1编辑并编译着色器 清单2.3-2.5
虽然有5个环节可以由我们来编辑,但是并不是每一个环节都是必须的,其中顶点着色器和片段着色器是每个OpenGL管线必须有的着色器。
注意OpenGL的着色器语言是GLSL,但是GLSL是由C++转变而来的,所以你能看到我们编写的着色器程序非常像C++,注意这并不是C++而是GLSL。
清单2.3我们的第一个顶点着色器:
#version 450 core//这一句表示我们的顶点着色器是用的OpenGL4.5版本,使用的是OpenGL核心模式void main(void){gl_Position = vec4(0.0, 0.0, 0.5, 1.0);//gl_Position是OpenGL顶点着色器内置的变量,我们通过它告诉管线我们的顶点在哪里}
关于核心模式的csdn文章
在main函数中我们赋值给gl_Position,它是连接着色器至OpenGL其余部分的纽带。通过gl_Position,OpenGL其余部分才能根据顶点数据进行计算,每有一个顶点数据传入(虽然这里我们直接把顶点的位置写入了着色器中),顶点着色器就将运行一次。所有以gl_为开头的变量都是OpenGL的一部分。
清单2.4我们的第一个片段着色器:
#version 450 coreout vec4 color;//通过out关键字声明一个我们自己的变量,变量类型为vec4(其实就是4个GLfloat的向量),名字为color//out关键字也表明这个color将从着色器中输出,由于是片段着色器的输出,所以它会将其视为该片段(像素)的颜色void main(void){color = vec4(0.0, 0.8, 1.0, 1.0);}
片段着色器实际上是为了计算每一个像素的颜色(不管是什么样的光照或是特效,最终反映到屏幕上的还是每个像素的颜色),所以对于每个像素都会运行一遍片段着色器。
小尾巴
要是片段着色器有两个out的变量呢?那会怎么样?
好,现在我们编辑好了顶点着色器和片段着色器,我们如何将他们添加到管线中去呢?我们需要创建着色器对象并且放到着色器程序中去,流程图:
其中红色的是主干,黑色的是分支(表示可以附着多种着色器,有些着色器是必须的,有些是可选的)。这里只是编译的流程图,当着色器程序不再使用时需要删除(详见下发shutdown部分)。
按照上面的流程我们再来看一下书中的编译过程(其实和流程相差无几,只是顺序有所改变),清单中把顶点着色器vertex shader和片段着色器fragment shader分别进行了创建,导入文本,编译。并把他们都附着到了着色器程序中(其实就是添加到了管线中)。当然对以后用不到的着色器进行了删除。
清单2.5编译一个简单的着色器:
GLuint compile_shader(void) {GLuint vertex_shader;GLuint fragment_shader;GLuint program;static const GLchar* vertex_shader_source[] = {"#version 450 core\n""void main(void)\n""{\n""gl_Position = vec4(0.0,0.0,0.5,1.0);\n""}\n"};static const GLchar* fragment_shader_source[] = {"#version 450 core\n""out vec4 color;\n""void main(void)\n""{\n""color = vec4(0.0,0.8,1.0,1.0);\n""}\n"};vertex_shader = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex_shader, 1, vertex_shader_source, NULL);glCompileShader(vertex_shader);fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragment_shader, 1, fragment_shader_source, NULL);glCompileShader(fragment_shader);program = glCreateProgram();glAttachShader(program, vertex_shader);glAttachShader(program, fragment_shader);glLinkProgram(program);GLint success;glGetProgramiv(program, GL_LINK_STATUS, &success);if (!success) {GLchar* m_infor = new GLchar[1024];glGetProgramInfoLog(program, 1023, NULL, m_infor);std::cout << "ERROR_IN: Program_ra.build " << std::endl << "\tError in link program " << program << std::endl;std::cout << "ERROR_IN: Program_ra.build " << std::endl << "\terror message : " << m_infor << std::endl;}glDeleteShader(vertex_shader);glDeleteShader(fragment_shader);return program;}
注意我这里用glGetProgramiv();来检查了一下着色器程序连接的情况(检查是否链接错误,并且反馈出错误信息glGetProgramInfoLog();,这里的Program_ra.build是我建立的一个类,这个以后在介绍这里不影响),当然你可能要注意一下new所占用的内存可能需要释放。
当然你如果怕自己的着色器编译出错也可以在编译后调用下面这个,来检查编译是否成功,并返回错误信息:
GLint shader_success;GLchar infoLog[512];//定义一个整型变量来表示是否成功编译,还定义了一个储存错误消息(如果有的话)的容器。glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &shader_success);//检查编译是否成功if (!shader_success){glGetShaderInfoLog(vertex_shader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}
注意
着色器在进行了附着glAttachShader();后就相当于拷贝了一份到着色器程序中了,故在以后调用着色器程序时,虽然着色器被删除了但是仍能正确运行(因为已经Attach进去了)。当然你可以把一个着色器附着到多个着色器程序中,这样就可以做到代码的复用(即我可以编写一种顶点着色器,两个片段着色器,两个着色器程序分别附着两个不同的片段着色器附着同一个顶点着色器,在绘制图像的时候选择一个着色器程序来运行)。
4.2用我们的着色器程序渲染一个点 清单2.6-2.7
当然仅仅是编译管线并不能让我们看到最终的渲染结果,我们还需要安排着色器程序在何时运行,告诉OpenGL我们要怎样进行绘制。
这里就要用到我们的框架了,即startup();render();shutdown();的三个子函数。
清单2.6(我们的startup();render();shutdown();):
首先注意需要几个全局变量:
GLuint rendering_program;//用于存放我们的着色器程序(接受compile_shader()的结果)GLuint vertex_array_object;//用于顶点数组的保存(这个以后会专门说,反正就是和顶点数据有关的东西)
重新编写startup();
void startup() {rendering_program = compile_shader();glCreateVertexArrays(1, &vertex_array_object);glBindVertexArray(vertex_array_object);glPointSize(40.0f);//设置点的大小为40.0f,要不然一个像素的点并不易观察到}
重新编写render();
void render(double currentTime) {static const GLfloat black[] = {0.0f,0.0f,0.0f,1.0f };glClearBufferfv(GL_COLOR, 0, black);glUseProgram(rendering_program);//在绘制点之前一定要运行着色器程序,要不然OpenGL怎么知道你要怎样处理顶点数据呢//当然如果你有多个着色器程序,你可以根据需求从中选一个来运行就行glDrawArrays(GL_POINTS, 0, 1);//告诉OpenGL我们要绘制一个点}
重新编写shutdown();
void shutdown() {glDeleteVertexArrays(1, &vertex_array_object);glDeleteProgram(rendering_program);glDeleteVertexArrays(1, &vertex_array_object);//为什么要删除两次??}
这里我们向子函数startup();render();shutdown();中添加了内容,并且添加了相关的全局变量。
这里我们留下了一些问题,是关于顶点数组,即glCreateVertexArrays(1, &vertex_array_object);和glBindVertexArray(vertex_array_object);的功能问题,这回有一点负责,等到之后绘制三角形的时候再向大家介绍吧,这里这一步并不影响大家的理解。
在以后的清单中,我们还要将顶点数据传入到管线中(本次顶点位置一个写到顶点着色器中了,即(0.0, 0.0, 0.5, 1.0)前三个代表xyz坐标,最后一个w是跟透视计算有关,这里先不做介绍,当然这些都与顶点数组有关)
还要向管线中传入一些数据比如说当前的时间,当前的摄像机位置,甚至是变换矩阵(这个以后再讲,因为当前还只是让大家体验一下让OpenGL画个平面图形出来)
5 总结
这一次我们首先带大家写了一下着色器程序的编译过程,具体详见流程图,之后将stratup() render() shutdown()进行了内容的填充,最终绘制出一个很大的点(注意要启用顶点数组–>glCreateVertexArrays–>glBindVertexArray,要设置点的大小为40.0f–>glPointSize,要运行着色器程序–>glUseProgram,要告诉我们要绘制的是1个点–>glDrawArrays)。
当然别忘了我们留了一个小尾巴,就是顶点数组(VAO)究竟是干什么的。当然这里留下这个小尾巴并不是故意的,而是在本次的清单中,顶点数组并没有起到"实质性"的作用,当我们需要向shader传入顶点数据(而不是将数据直接写在shader中)时我们将用到顶点数组对象(VAO),到时候在解释才更容易懂(预计在笔记6中),毕竟当存在问题的时候解释才显得合理,如果只给出解释反而会让人一头雾水。
下一篇我们带大家绘制你的第一个三角形(仍是将顶点数据编写在shader中),这回相对轻松,当然我还会多渗透一些有关于顶点颜色差值的东西进去。
我们下篇见~~
关于书籍的问题
如果你手中没有该书,我还是建议你购买一本,毕竟书本毕竟更加严谨专业,我这里难免遗漏一些细节,主要是提供实例,并做一个消化,将很混乱的流程为大家理清,但这笔记一定是通俗的,是对新手友好的(当然有时候你需要在某些方面自己努努力,比如后面出现的基本线性代数的内容,还有C语言或是c++的基础知识,虽然我可能也不太懂O(∩_∩)O,慢慢来吧)。
别被吓住
刚开始的时候很容易被OpenGL的巨长的函数和超级复杂的流程吓到,其实并没有那么可怕,只要对这样或那样的流程熟悉之后,一切都变得相当简单(当然如果你能提出一个更好的流程那就更好了,当我们把很多基础的工作做完,我们会不断的提出新问题新点子,用新的技术来实现它,最终完成OpenGL的学习)
虽然我也不知道后面将是怎样的道路,但至少努力学习是没错的。
我看过的相关内容
以下并不是全看完了,大部分看了15%就看不下去了,实在是没看懂。(本人没什么计算机编程基础,算是野生程序员吧,很多内容都不能标准表述,望见谅)
如果你对opengl的工作有了一定的了解,我一开始也是从这里开始的,但是仍然有很多的不懂的,最后至今为止,我杂糅了很多的网站内容包括LearnOpenGL、极客学院、哔哩哔哩的闫令琪计算机图形学、哔哩哔哩的傅老师的OpenGL课程、OpenGL编程指南"也称为红宝书"、OpenGL超级宝典"也称为蓝宝书"、当然还有很多的csdn文章O(∩_∩)O这就不介绍了,等用到是时候我在放链接吧O(∩_∩)O
这里面图形学比较易懂也很基础推荐可以作为开始(如果你是学OpenGL需要马上用,应该可以跳过,但是其中的内容很是很重要,这会让后面涉及变换透视的章节更加易懂,推荐大家看看),之后是蓝宝书或是极客学院翻译的教程比较推荐,这两个还是比较适合你我这样的新手的。
这里不推荐看的是红宝书,这本书我看了有点类似于字典那样的工具书,不太适合新手上手学,而且讲的也并不是很通俗易懂(可能是我的书版本比较老吧…)
加油
当然如果你对我有信心,我也会持续更新(虽然前路漫漫),跟大家一同进步(虽然很可能没人看(╥╯^╰╥),无所谓了,当然如有错误还请大家指正∠(°ゝ°),哪里不懂我会尽力解决,哪里说的不好也可以指出我会及时修改~)
我们下篇见~~