现代OpenGL系列教程(一)---旋转的三角形 本是古典 何须时尚 2022-04-14 03:36 215阅读 0赞 ### **【写在前面】** ### 本章主要内容: 1、**基本的矩阵变换** 2、**基本的OpenGL Buffer Object** 3、**基本的GLSL(OpenGL着色语言) ** -------------------- ### **【正文开始】** ### 在正式开始学习之前,我必须要说明的是: 接上一章,我假设你已经搭建好 glfw3 + glad + opengl 的环境。 为了简化开发,我把一些比较繁琐的、乱七八糟的一些 Api 简单的封装了一下,因此在后面的教程中,我将只使用这些,当然也会慢慢完善(如果有需要的话)。。 首先我封装了 glfw 常用的一些 set\*/get\*/create\*,这个 class 为 OpenGLWindow,具体的代码见源码(关注点不应该在这),它有一个虚函数 void render(),在事件循环中自动调用,所以我们自己的 Window 只需要继承 OpenGLWindow 并重新实现render()。 #ifndef MYWINDOW_H #define MYWINDOW_H #include "OpenGLWindow.h" class MyRender; class MyWindow : public OpenGLWindow { public: MyWindow(); ~MyWindow(); protected: void render(); void resizeEvent(int width, int height); private: MyRender *m_render; }; #endif 哦,这里还有一个 void resizeEvent(int width, int height),它在窗口大小改变后自动调用,而 MyRender 就是我们实际的渲染类了,这里只是简单的调用 m\_render 的 render() 和 resizeGL(),如下: #include "MyWindow.h" #include "MyRender.h" MyWindow::MyWindow() { m_render = new MyRender(); } MyWindow::~MyWindow() { if (m_render) delete m_render; } void MyWindow::render() { m_render->render(); } void MyWindow::resizeEvent(int width, int height) { OpenGLWindow::resizeEvent(width, height); m_render->resizeGL(width, height); } 接下来是 OpenGLRender 类了: #ifndef OPENGLRENDER_H #define OPENGLRENDER_H #include <glad/glad.h> #include <GLFW/glfw3.h> #include <string> using std::string; class OpenGLRender { public: enum ShaderType { Vertex = GL_VERTEX_SHADER, Fragment = GL_FRAGMENT_SHADER }; OpenGLRender(); ~OpenGLRender(); public: virtual void render() { } virtual void resizeGL(int w, int h) { } virtual void initializeGL() { } virtual void initializeShader() { } protected: GLuint compileShader(ShaderType type, const string &source); GLuint compileShaderFile(ShaderType type, const string &filename); }; #endif 作为基类,还是四个虚函数,继承它并实现即可,这里我们主要看 GLuint compileShader(ShaderType type, const string &source)。 首先,我们要知道现代 OpenGL 的渲染管线,它是一系列数据处理的过程,并且将应用程序的数据转换到最终渲染的图像。 ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEyODMyMjY_size_16_color_FFFFFF_t_70][] 其中,蓝色的为可编程着色器,其中顶点着色器和片元着色器没有默认实现,因此必须由我们自己实现。 着色器使用一种类似c语言的GLSL来编写,我们来看一下\[创建->使用\]一个着色器程序具体流程: ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEyODMyMjY_size_16_color_FFFFFF_t_70 1][] 现在回过头来看我们的 compileShader(): #include "OpenGLRender.h" #include <iostream> #include <fstream> #include <sstream> OpenGLRender::OpenGLRender() { } OpenGLRender::~OpenGLRender() { } GLuint OpenGLRender::compileShader(ShaderType type, const string &source) { if (!source.empty()) { GLuint shader = glCreateShader((GLenum)type); const GLchar *shaderSource = source.c_str(); glShaderSource(shader, 1, &shaderSource, nullptr); glCompileShader(shader); GLint success; GLchar infoLog[512]; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog); if (type == Vertex) std::cerr << "Compile Vertex Shader Error :" << infoLog << std::endl; else if (type == Fragment) std::cerr << "Compile Fragment Shader Error :" << infoLog << std::endl; return 0; } return shader; } return 0; } GLuint OpenGLRender::compileShaderFile(ShaderType type, const string &filename) { std::ifstream fin; fin.open(filename, std::ios_base::in); if (!fin.is_open()) std::cerr << "Shader File " + filename + " Open Failed! " << std::endl; std::stringstream buffer; buffer << fin.rdbuf(); string source(buffer.str()); if (source.empty()) std::cerr << "Shader File " + filename + " is Empty! " << std::endl; fin.close(); return compileShader(type, source); } 1、我们使用 GLuint glCreateShader(GLenum shaderType)创建了一个着色器对象,shaderType 为着色器类型,可以为以下几个值:GL\_VERTEX\_SHADER,GL\_FRAGMENT\_SHADER,GL\_TESS\_CONTROL\_SHADER,GL\_TESS\_EVALUATION\_SHADER,GL\_GEOMETRY\_SHADER,正确返回非 0 值,返回 0 则发生错误。 2、使用 GLuint glShaderSource(GLuint shader, GLsizei count, const GLchar\*\* string, const GLint\* length) 将着色器代码string 关联到一个 shader 着色器对象上,count 表示 string 的行数,length 为 string 的字符数,如果 length 为 null,则假设 string 以 null 结尾。 3、使用 void glCompileShader(GLuint shader) 编译一个着色器对象,由 void glGetShaderiv() 判断编译状态,由 void glGetShaderInfoLog() 获取最后的编译结果。 最后,成功则返回正确的着色器对象,失败返回0,而我写的 OpenGLRender::compileShaderFile() 则可以让我们从本地文本文件来读取着色器代码。 接下来我们需要看 MyRender,这是真正执行一系列操作的地方: #ifndef MYRENDER_H #define MYRENDER_H #include "OpenGLRender.h" #include <glm/vec3.hpp> #include <glm/mat4x4.hpp> class MyRender : public OpenGLRender { public: MyRender(); ~MyRender(); void render(); void resizeGL(int w, int h); void initializeGL(); void initializeShader(); void initializeTriangle(); private: GLuint m_vbo; GLuint m_program; glm::mat4x4 m_projection; }; #endif 老样子,继承 OpenGLRender 并实现它的几个虚函数,来看其实现: #include "MyRender.h" #include <iostream> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp> struct VertexData { glm::vec3 postion; glm::vec3 color; }; MyRender::MyRender() { initializeGL(); } MyRender::~MyRender() { glDeleteBuffers(1, &m_vbo); glDeleteProgram(m_program); } void MyRender::render() { static GLfloat angle = 0.0f; glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); angle += 1.0f; glm::mat4 modelMatrix(1.0f); modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0f, 0.0f, -5.0f)); modelMatrix = glm::rotate(modelMatrix, glm::radians(angle), glm::vec3(0.0f, 1.0f, 0.0f)); glUseProgram(m_program); GLuint mvp = glGetUniformLocation(m_program, "mvp"); glUniformMatrix4fv(mvp, 1, GL_FALSE, glm::value_ptr(m_projection * modelMatrix)); glBindBuffer(GL_ARRAY_BUFFER, m_vbo); glDrawArrays(GL_TRIANGLES, 0, 3); } void MyRender::resizeGL(int w, int h) { GLfloat aspect = (GLfloat)w / (GLfloat)h; m_projection = glm::perspective(glm::radians(30.0f), aspect, 1.0f, 10.0f); } void MyRender::initializeGL() { initializeShader(); initializeTriangle(); } void MyRender::initializeShader() { GLuint vertexShader = compileShaderFile(Vertex, "../GLSL/vertex_glsl.vert"); GLuint fragmentShader = compileShaderFile(Fragment, "../GLSL/fragment_glsl.frag"); m_program = glCreateProgram(); glAttachShader(m_program, vertexShader); glAttachShader(m_program, fragmentShader); glLinkProgram(m_program); int success; char infoLog[512]; glGetProgramiv(m_program, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(m_program, sizeof(infoLog), nullptr, infoLog); std::cerr << "Shader Program Linking Error :" << infoLog << std::endl; } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); } void MyRender::initializeTriangle() { VertexData vertices[] = { { glm::vec3(-0.5f, -0.5f, 0.0f), glm::vec3(0.9f, 0.0f, 0.9f) }, { glm::vec3( 0.0f, 0.5f, 0.0f), glm::vec3(0.9f, 0.9f, 0.0f) }, { glm::vec3( 0.5f, -0.5f, 0.0f), glm::vec3(0.0f, 0.9f, 0.9f) } }; glGenBuffers(1, &m_vbo); glBindBuffer(GL_ARRAY_BUFFER, m_vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); int location = 0; glVertexAttribPointer(location, 3, GL_FLOAT, GL_TRUE, sizeof(VertexData), (void *)0); glEnableVertexAttribArray(location); glVertexAttribPointer(location + 1, 3, GL_FLOAT, GL_TRUE, sizeof(VertexData), (void *)(sizeof(glm::vec3))); glEnableVertexAttribArray(location + 1); } 我们按着流程走,首先调用 initializeGL() \->调用 initializeShader(),initializeTriangle()。 先看 initializeShader() : 首先我们创建并编译了两个着色器对象,然后使用glCreateProgram()创建了一个着色器程序, 接下来使用 glAttachShader(GLuint program, GLuint shader) 将着色器对象附加到着色器程序上,然后使用glLinkProgram() 来处理所有附加的着色器并生成一个完整的着色器程序, 链接结果由 glGetProgramiv() 和 glGetProgramInfoLog() 查询,最后使用 glDeleteShader() 删除这两个着色器对象。 然后我们来看 initializeTriangle(): 1、使用 glGenBuffers(GLsizei n, GLuint \*buffers) 生成n个未使用的缓存对象,保存到buffers数组中。 2、glBindBuffer(GLenum target, GLuint buffer),我们使用的任何对(target)缓冲调用都会用来配置当前绑定的缓冲对象。 2、glBufferData(GLenum target, GLsizeptr size, const GLvoid\* data, GLenum usage) 是真正为缓存对象分配存储空间的函数。target 是目标缓冲的类型,缓冲对象当前绑定到 target 类型上,size 指定传输数据的大小(以字节为单位),用一个简单的 sizeof() 计算出顶点数据大小就行。data 是我们希望发送的实际数据,usage 则是缓存使用的策略。 这里我们需要存储顶点数据数组,所以这里分配的是数组缓存对象 GL\_ARRAY\_BUFFER。 4、glVertexAttribPointer() 函数的参数非常多,第一个参数指定我们要配置的顶点属性,这里我们放一下,先来看顶点着色器: #version 330 core layout (location = 0) in vec3 position; layout (location = 1) in vec3 color0; out vec3 color; uniform mat4 mvp; void main(void) { gl_Position = mvp * vec4(position, 1.0f); color = color0; } GLSL首行使用 \#version 设置当前使用的 GLSL 版本和模式,这里使用 GLSL 330 (对应OpenGL 3.3) 核心模式 Core, layout:布局控制,这里控制一个 in vec3 position 的位置为 0,in vec3 color0 的位置为1, **现在回到 glVertexAttribPointer() 函数,** 它的第一个参数正是着色器中的 location,我们设置了 0 和 1 ,因此 0 控制顶点位置,1 控制顶点颜色, 第二个参数指定顶点属性的大小,顶点属性是一个vec3,它由3个值组成,所以大小是3, 第三个参数指定数据的类型,这里是 GL\_FLOAT ( GLSL中 vec\* 都是由浮点数值组成的 ), 第四个参数定义我们是否希望数据被标准化( Normalize )。如果我们设置为 GL\_TRUE,所有数据都会被映射到 0(对于有符号型 signed 数据是 -1 )到 1 之间, 第五个参数叫做步长( Stride ),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在 3 个 float 之后,我们把步长设置为 sizeof(VertexData) 即可。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙),当然,我们也可以设置为 0 来让 OpenGL 决定具体步长是多少(只有当数值是紧密排列时才可用), 最后一个参数的类型是 void\*,所以需要我们进行强制类型转换。它表示位置数据在缓冲中起始位置的偏移量( Offset )。由于 postion 数据在数组的开头,所以这里是 0, glEnableVertexAttribArray(location):启用location对应的顶点属性数组,顶点属性数组下章再讲, 而第二个 glVertexAttribPointer() 调用我们改变 location = 1,color数据的偏移量 = position ( 它前面只有 position ), **继续回到着色器:** in out uniform:类型修饰符,in out 字面意思,输入输出修饰,unifom 表示修饰的变量由用户传递,对于着色器而言,这个变量是全局的(Global 不可重复),并且不可修改(常量), vec3 mat4:GLSL内置类型,vec->vector向量( vec3 三维向量 ),mat->matrix 矩阵( mat4 4x4矩阵:四行四列 ), 我们的顶点着色器输出一个顶点位置 gl\_Position,它是 GLSL 内置变量,类型为 vec4,一个颜色 color,类型为 vec3, 这里有一个 uniform mat4 mvp,它是所谓的 model(模型) - view(观察) - projection(投影) 矩阵, 这三个矩阵干嘛的呢?(啊我好累.....给两个链接自己看吧T T....) **OpenGL坐标系统:**[https://learnopengl-cn.github.io/01%20Getting%20started/08%20Coordinate%20Systems/][https_learnopengl-cn.github.io_01_20Getting_20started_08_20Coordinate_20Systems] **矩阵:**[https://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-3-matrices/][https_www.opengl-tutorial.org_cn_beginners-tutorials_tutorial-3-matrices] 我们在 initializeTriangle() 中的坐标是( Local Space 局部空间 ),所以我们需要分别使用 model(模型) - view(观察) - projection ( 投影 ) 进行变换,所以将 mvp矩阵 右乘 position 得到裁剪空间的坐标,最终经 glViewport() 变换到( Screen Space 屏幕空间 )。 **回到MyRender:** 我们 在resizeGL() 中计算了 projection (投影矩阵),通过 glm::perspective() 生成透视投影矩阵, > 【你应该\#include <glm/gtc/matrix\_transform.hpp>】它包含了一系列的矩阵变换。 后面的章节将会有非常多的 glm 的函数,我想你应该下载了 glm 的 doc (文档),所以相关的函数说明我就不再一一介绍了。 接着就是 render() 了: glClearColor() 设置清屏颜色( 是的它是设置函数 ),然后我们调用 glClear() 来清除颜色缓冲。 > 在每个新的渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果(这可能是你想要的效果,但通常来说不是)。我们可以通过调用 glClear() 函数来清空屏幕的颜色缓冲,它接受一个缓冲位( Buffer Bit )来指定要清空的缓冲,可能的缓冲位有 GL\_COLOR\_BUFFER\_BIT,GL\_DEPTH\_BUFFER\_BIT和GL\_STENCIL\_BUFFER\_BIT。由于现在我们只关心颜色值,所以我们只清空颜色缓冲 接着,我们使用 glm 生成一个 mat4 modelMatrix(模型矩阵),接着把它平移到 ( 0.0f, 0.0f, -5.0f ),接着绕 y 轴旋转 glm::radians(angle) 弧度, > 【glm 0.9.9版之后】,所有的角度为弧度。 使用 glUseProgram() 启用一个链接过的着色器程序,然后使用 glGetUniformLocation() 获取一个着色器程序中 uniform 的 location 索引,通过此索引,使用 glUniform\*(),可以设置它的值,\*为后缀,这里是 Matrix4fv,即设置一个 4x4 矩阵(指针形式,第三个参数指示行主序 GL\_TRUE,还是列主序 GL\_FALSE )。 > 【glm::value\_ptr()】返回一个变量的指针形式。 > > 【OpenGL中的矩阵是列主序】 最后,设置好 mvp 矩阵后,绑定 m\_vbo,使用 glDrawArrays() 执行绘制,它使用数组元素建立连续的几何图元序列。 最终,顶点的输出会送至片元着色器,这里直接将接收到的 color 作为最终的颜色输出( 它是 vec4,GLSL 是强类型的,vec3->vec4必须显式转换 ): #version 330 core in vec3 color; out vec4 FragColor; void main(void) { FragColor = vec4(color, 1.0f); } 效果图如下: ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEyODMyMjY_size_16_color_FFFFFF_t_70 2][] -------------------- ### ** 【结语】** ### 这一章的内容实在太多了,我已经很尽力地讲清楚了,但就像之前说过的,这并非零基础的教程,所以很多细节没有讲到。。不过应该不影响。 然后系列代码地址:[https://github.com/mengps/OpenGL-Totural/][https_github.com_mengps_OpenGL-Totural] 最后,推荐两本书:《OpenGL编程指南》和《OpenGL超级宝典》。 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEyODMyMjY_size_16_color_FFFFFF_t_70]: /images/20220414/2a4054e469fc4dd182ad835bdd8e724a.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEyODMyMjY_size_16_color_FFFFFF_t_70 1]: /images/20220414/1a54d6ad374843309a4caa2cb152f15a.png [https_learnopengl-cn.github.io_01_20Getting_20started_08_20Coordinate_20Systems]: https://learnopengl-cn.github.io/01%20Getting%20started/08%20Coordinate%20Systems/ [https_www.opengl-tutorial.org_cn_beginners-tutorials_tutorial-3-matrices]: https://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-3-matrices/ [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEyODMyMjY_size_16_color_FFFFFF_t_70 2]: /images/20220414/020a1e38400e41e8ac7c9d3eb51c67c4.png [https_github.com_mengps_OpenGL-Totural]: https://github.com/mengps/OpenGL-Totural/
还没有评论,来说两句吧...