1 引子
这些天公司一次次的软件发布节点忙的博主不可开交,另外还有其它的一些事也占用了很多时间。现在坐在电脑前,在很安静的环境下,与大家分享自己的OpenGL学习笔记和理解心得,感到格外舒服。这让我回忆起了童年时期的一些情景,在群山环绕的农村,方圆不足一两公里,当时感觉自己面对的世界好小,很想到去看看外面世界的精彩;上大学后就来到大城市了,至今算来也近十年了。现在看来外面的世界不见得比家乡好:地铁站、大街上,人们步履匆匆,争分按秒;食品安全问题、生活买房压力……让我再次思考——大城市一定适合我吗?现在越来越觉得安静的小城市小乡村的生活是多么的惬意。呵呵,发发感慨!也许这世界本不存在完美,大城市有大城市的好处和缺点,小城镇有小城镇的特色和不足;也许人生之路就是每走一步都要尝试着去用心发现生活中的美好。
读书笔记(二)中我们整理了坐标系的定义,然后着重推导了一下平移、旋转、缩放三种基本的变换矩阵,所讲内容比较抽象,但确实非常重要。这一次读书笔记我们就应用这些矩阵来做几个简单的例子。整装待发,让我们再次踏上OpenGL的学习之旅吧!
2 着色器的编译链接
着色器语言是一门高级语言,语法上和C系列语言很类似。既然是高级语言,那么用它写的程序代码就不能被机器直接执行,需要编译、连接,最后生成可执行文件才能被CPU/GPU调度执行。着色器语言的编译、连接过程与C语言十分类似,所以在介绍GLSL程序的链接接之前来看看C语言程序是怎么链接成一个可执行程序的。C语言的编译与链接过程如下图所示:
类似的,GLSL编译过程如下:
咋看之下,似乎两者相差很大,GLSL的编译链接过程更为复杂。但其实两者本质是一样的,只不过C语言的编译器如gcc为我们默默地做了很多幕后工作了——如读入源程序、编译、链接——一气呵成。而GLSL语言的编译则需要我们自己将这些步骤拼起来,而且这其中还要创建着色器对象和着色器程序对象,以便于OpenGL的管理。下面,我们就GLSL的编译过程具体展开看看——一个着色器程序是怎么变成可以让GPU调度执行的程序。
C语言中,会先把各个源文件编译为目标文件,然后将各个目标文件链接为可执行文件。GLSL中则通过两种对象——着色器对象和着色器程序对象——来分别处理编译过程和连接过程。这里,我使用了一个类GPUProgram来对GLSL着色器程序的编译、链接过程进行了封装:类的使用者只需要创建对象,通过AddShader添加着色器类型和着色器源代码文件名,然后执行CreateGPUProgram函数即可得到编译链接之后的可执行程序。我们先看看这个对象的代码,然后再深入分析涉及到的API。
GPUProgram.h文件声明了类:
1 #pragma once
2
3 #include <map>
4 #include <string>
5
6 #ifndef GLEW_STATIC
7 #define GLEW_STATIC
8 #endif
9
10 #include "GL/glew.h"
11 #include "ResourceCommon.h"
12
13 #ifdef __cplusplus
14 extern "C" {
15 #endif // __cplusplus
16
17 /// \brief
18 class GPUProgram
19 {
20 public:
21 /// \brief constructor & destructor
22 RESOURCE_EXPORT GPUProgram();
23 RESOURCE_EXPORT ~GPUProgram();
24
25 void RESOURCE_EXPORT AddShader(GLenum SHADER_TYPE, const std::string& sFileName);
26
27 GLuint RESOURCE_EXPORT CreateGPUProgram();
28
29 private:
30 std::string _ReadShaderSourceCode(const std::string& sFileName) const;
31
32 void _RecordShaderLog(GLuint shader) const;
33
34 void _CompileShader(GLuint shader) const;
35
36 void _LineGPUProgram() const;
37
38 void _RecordProgramLog() const;
39
40 private:
41 std::map<GLenum, std::string> m_mapShaderFileName;
42
43 GLuint m_uiProgramID;
44 };
45
46 #ifdef __cplusplus
47 }
48 #endif // __cplusplus
GPUProgram.cpp是上述的实现:
1 #include "GPUProgram.h"
2
3 #include <fstream>
4 #include <iostream>
5 #include <sstream>
6 #include <memory>
7 #include "GameFramework/StdAfx.h"
8
9 #ifdef __cplusplus
10 extern "C" {
11 #endif // __cplusplus
12
13 GPUProgram::GPUProgram()
14 {
15
16 }
17
18 GPUProgram::~GPUProgram()
19 {
20 // -----------------删除着色器程序对象-----------------
21 glDeleteProgram(m_uiProgramID);
22 }
23
24 void GPUProgram::AddShader(
25 GLenum SHADER_TYPE, const std::string& sFileName )
26 {
27 m_mapShaderFileName.insert(std::make_pair(SHADER_TYPE, sFileName));
28 }
29
30 GLuint GPUProgram::CreateGPUProgram()
31 {
32 if (GLEW_OK != glewInit())
33 {
35 std::exit(EXIT_FAILURE);
36 }
37
38 // -----------------创建着色器程序对象-----------------
39 m_uiProgramID = glCreateProgram();
40
41 for (auto aPair : m_mapShaderFileName)
42 {
43 // -----------------创建着色器对象-----------------
44 GLuint shader = glCreateShader(aPair.first);
45
46 // -----------------读取着色器程序文件内容-----------------
47 std::string sShaderSrc = _ReadShaderSourceCode(aPair.second);
48
49 // -----------------关联着色器对象和着色器源代码-----------------
50 const GLchar *src = const_cast<GLchar *>(sShaderSrc.c_str());
51 glShaderSource(shader, 1, &src, NULL);
52
53 // -----------------编译着色器源代码-----------------
54 _CompileShader(shader);
55
56 // -----------------关联着色器对象到着色器程序对象-----------------
57 glAttachShader(m_uiProgramID, shader);
58 }
59
60 // ----------------连接着色器程序对象-----------------
61 _LineGPUProgram();
62
63 return m_uiProgramID;
64 }
65
66 std::string GPUProgram::_ReadShaderSourceCode( const std::string& sFileName ) const
67 {
68 std::ifstream is;
69 is.open(sFileName);
70
71 std::stringstream ss;
72 ss << is.rdbuf();
73
74 return ss.str();
75 }
76
77 void GPUProgram::_RecordShaderLog(GLuint shader) const
78 {
79 // -----------------获取Log长度----------------
80 GLsizei len;
81 glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
82
83 // -----------------获取Log并打印----------------
84 GLchar* LOG = new GLchar[len+1];
85 glGetShaderInfoLog(shader, len, &len, LOG);
86 std::cerr << "Program link failed: " << LOG << std::endl;
87 delete [] LOG;
88 }
89
90 void GPUProgram::_CompileShader( GLuint shader ) const
91 {
92 glCompileShader(shader);
93
94 GLint compileSucceed;
95 glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSucceed);
96 if (!compileSucceed)
97 {
98 _RecordShaderLog(shader);
99 throw std::exception("Failed to compile the shader!");
100 }
101 }
102
103 void GPUProgram::_LineGPUProgram() const
104 {
105 glLinkProgram(m_uiProgramID);
106
107 GLint linkSucceed;
108 glGetProgramiv(m_uiProgramID, GL_LINK_STATUS, &linkSucceed);
109 if (!linkSucceed)
110 {
111 _RecordProgramLog();
112 throw std::exception("Failed to link the GPUProgram!");
113 }
114 }
115
116 void GPUProgram::_RecordProgramLog() const
117 {
118 // -----------------获取Log长度----------------
119 GLsizei len;
120 glGetProgramiv(m_uiProgramID, GL_INFO_LOG_LENGTH, &len);
121
122 // -----------------获取Log并打印----------------
123 std::shared_ptr<GLchar> LOG(
124 new GLchar[len + 1], std::default_delete<GLchar>());
125 glGetProgramInfoLog(m_uiProgramID, len, &len, LOG.get());
126 std::cerr << "Program link failed: " << *LOG << std::endl;
127 }
128
129 #ifdef __cplusplus
130 }
131 #endif // __cplusplus
2.1 着色器对象的管理
OpenGL管理着一系列的对象,如之前我们接触到的缓存对象、顶点数组对象,以及以后要接触的纹理对象、采样器对象、帧缓存对象等等,使用这些对象来管理我们的数据。我们在客户端通过OpenGL的API创建这些对象,并设置其中的数据,然后在GPU端的着色器程序中实现对这些对象的访问。对于每一个对象,都有与之对于的一套API,如创建glCreateXXX,销毁glDeleteXXX,有一些对象还有绑定glBindXXX,以及其他一些和具体对象相关的API。着色器对象和这些对象一样,不过它管理的数据是用GLSL语言写的着色器源代码。
首先看CreateGPUProgram函数,这个函数主要封装了GLSL编译链接的过程。第一个调用的OpenGL API便是glCreateProgram,顾名思义,这个借口就是创建一个着色器程序对象。创建完着色器程序对象后,遍历一个map容器,在这个map容器中存放着着色器类型到着色器文件名的映射。
- 对每一个着色器类型,通过glCreateShader创建一个管理着色器源代码的着色器对象,该函数的函数签名为:
GLuint glCreateShader(GLenum type);
type ——待创建的着色器对象类型
返回值 ——着色器对象的ID
- 对每一个着色器文件名,通过调用_ReadShaderSourceCode读入文件中的着色器程序源代码。
创建完对应类型的着色器对象并从文件读取着色器的源代码(注:着色器代码也可以通过定义字符串的方式给出,见红宝书<中文版>p50-51),接下来就是建立着色器对象和着色器源代码之间的关系,通过glShaderSource实现,其函数签名为:
void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLchar* length);
shader ——标识着色器对象的ID(可理解为Handler)
count ——着色器源代码的数量
string ——着色器源代码(可能由多个,根据count而定)
length ——每个着色器源代码的长度
一切准备工作都已经完毕——着色器对象已经有了,着色器源代码也已经有了,并和着色器对象做了关联,接下来就是把编译着色器源代码了,这就是glCompileShader的功能,其函数签名为:
void glCompileShader(GLuint shader)
shader ——标识着色器对象的ID
最后一步,就是将编译好的着色器对象关联到着色器程序对象,所使用 的OpenGL API函数签名如下:
void glAttachShader(GLuint program, GLuint shader)
program ——标识着色器程序对象的ID
shader ——标识着色器对象的ID
2.2 着色器程序对象的管理
着色器程序对象相当于着色器对象的容器,和任何OpenGL管理的对象一样,在使用着色器程序对象之前,需要通过glCreateProgram接口来创建出一个着色器程序对象。一般,在一次OpenGL绘制中,只能使用一个着色器程序对象。创建着色器程序对象的函数签名为:
GLuint glCreateProgram()
返回值 ——标识着色器程序对象的ID
创建完着色器程序对象之后,可以通过刚才介绍的glAttachShader将编译完的着色器对象与着色器程序对象关联起来。关联完成后,就可以将这些着色器对象链接成为一个可被GPU调度执行的二进制程序了。链接命令的函数全面如下:
void glLinkProgram(GLuint program)
program ——标识着色器程序对象的ID
执行完上述命令后,我们得到了一个可执行的着色器程序,但一个OpenGL程序中可能有多个很多个可执行的着色器程序,那么这时我们就要告诉OpenGL,使用哪一个可执行的着色器程序,这就引出了下面的API:
void GLUseProgram(GLuint program)
program ——标识着色器程序对象的ID
至此,OpenGL在执行渲染管线时就会调用相应的着色器程序了。写到这里,我们可以知道,编译好的着色器程序就像dll一样,不能单独运行,只能嵌入到OpenGL的渲染管线中管线中才能被执行。上面介绍的是在编译链接过程中最重要的一些API,还有其它API都是比较简单的,在此就不作详细介绍了。
看到这里,大家也许有点疲惫了,休息一会,来看看静谧的海滩、蔚蓝的天空和美丽的椰子树~~
3 着色器输入之变换矩阵
3.1 存储限制符与uniform变量的引入
课堂笔记(一)讲述了OpenGL程序的基本结构——和大多数程序一样,OpenGL程序一般由三部分组成(如下图):输入层、处理层和输出层。其中输入部分主要是将顶点数据、坐标变换矩阵(本节将要讲述)以及以后要讲述的纹理数据、光照数据输入给OpenGL——巧妇难为无米之催,OpenGL也同样需要数据或资源才能绘制图像;处理层则主要是运行OpenGL运行渲染管线——这部分以前是固定的、不可编程的;现在引入了着色器程序之后就可编程了,也就是着色器程序的作用主要是对这些输入进行处理,比如顶点变换、光照明暗处理、纹理读取balabala……,也就是对输入层进行加工;最后从帧缓存中取出的就是OpenGL给我们绘制出来的图像了。这就是计算机绘图,曾经觉得很神秘,现在也慢慢地懂了其中的一些流程,慢慢觉得这些也不过如此!^_^。
从上图中可以看出:笔记(二)得到的变换矩阵是作为输入层的一部分。在笔记(一)中,我们知道OpenGL为顶点数据提供了缓冲区对象(Buffer Object)和顶点数组属性分别用来上传顶点数据和描述顶点的数据(元数据)给GPU。那OpenGL为变换矩阵的输入提供了什么工具呢?——Uniform变量。在讲述uniform变量前,让我们先来回顾一下变量的概念。
从大一接触C语言开始,我们就学习了程序设计中变量的概念。所谓变量,其实是一段内存区域的刻画——变量名、变量类型、变量初始值(数据)。变量在编程中的重要性,不言而喻:变量里保存的是数据,我们编程的目的不就是对这些数据进行处理吗?所以,在任何一门编程语言中,变量(包括声明、赋值、类型等)相关的知识点是学习编程语言的一个重点。
在C语言中变量定义时,碰到第一个单词描述的就是变量的类型,如int a = 1。我们从变量类型开始,GLSL着色语言的变量类型和普通的C系列语言没有太大差别,主要是多了两个在计算机图形学中使用十分广泛的类型——向量vec和矩阵mat,下图给出了GLSL定义的基本数据类型:
变量名和变量初始值这些和C系列语言是完全相同的,在这里就不再赘述了。
定义变量语句给出的是变量类型、变量名和变量值,这在GLSL中还不够!变量在内存/显存中是存储在哪块区域中的?从物理存储器的角度看,内存只是一系列连续的存储单元,通过地址对其访问;从编程逻辑的角度看,操作系统或编译系统对内存的管理其实是分了不同的逻辑区域的——栈区、堆区、全局数据区、常量区和代码区。例如:全局变量存储在全局数据区;局部变量及函数输入输出变量存储在栈区;常量存储在常量区。在C语言中,可以将全局变量定义在任何函数之外来表示这是一个全局变量;局部变量则定义在函数中;函数的输入变量定义在函数的输入参数列表中等等。那GLSL中,怎么描述一个变量是局部变量还是全局变量,怎么描述着色器的输入变量还是输出变量。这就引入了存储限制符的概念——用来描述变量存储区域和作用,存储限制符主要用下面这些关键字描述:
const ——这应该是最简单的存储限制符了,它出现在变量声明表达式中,意味着该变量一旦初始化之后,就不能改变了,如:const float PI = 3.1415926;
in ——在着色器程序中的主函数(main)中,是没有输入参数的,如何向着色器传入外部数据呢?这时就要用到in存储修饰符来表示那些变量是来自外部程序或其他着色器的输入;
out ——和in存储修饰符相反,out存储修饰符用于标识着色器的输出变量,或传给下一阶段的着色器,或传给着色器外部程序;
uniform ——总算出来了,红宝书上是这么写的:uniform修饰符可以指定一个在应用中设置好的变量,它不会在图元处理的过程中发生变化,且在所有的着色阶段之间都是共享的——着色器中的全局变量。
buffer ——和uniform变量类似,不过它可以被修改。
所以,如果希望应用程序客户端向GPU的着色器程序传递数据,可以使用uniform变量。关于uniform变量,这涉及到两个知识点:应用程序端调用什么借口上传数据?着色器中应该怎么定义uniform变量?下面通过一段程序来看看uniform变量的使用。
3.2 Uniform变量的使用
下面的程序绘制的是一个正方形轮廓,代码和读书笔记(一)中的程序差不多,主要是增加了一个变换矩阵,并通过Uniform变量上传至GPU中,使着色器程序能访问到该矩阵变量;另外,在读书笔记(一)中我们使用的是红宝书给出的方法来加载着色器,这次读书笔记中使用的是刚才在第二小节中给出的GPUProgram对象对着色器程序的加载、编译与链接。程序代码如下:
1 #include <iostream>
2
3 #include "AlgebraicEntity/Matrix.h"
4
5 #include "GameFramework/StdAfx.h"
6 #include "Resource/GPUProgram.h"
7
8 GLint location;
9 GPUProgram gpuProgram;
10
11 void initialize_04()
12 {
13 // ----------------准备顶点数据----------------
14 GLfloat vertices_array[4][4] =
15 {
16 { -0.5, 0.5, 0.0, 1.0 },
17 { 0.5, 0.5, 0.0, 1.0 },
18 { 0.5, -0.5, 0.0, 1.0 },
19 { -0.5, -0.5, 0.0, 1.0 },
20 };
21
22 // ----------------建立缓存对象加载数据----------------
23 GLuint Buffer_ID;
24 glGenBuffers(1, &Buffer_ID);
25 glBindBuffer(GL_ARRAY_BUFFER, Buffer_ID);
26 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_array), vertices_array, GL_STATIC_DRAW);
27
28 // ----------------建立顶点数组对象----------------
29 GLuint VAO_ID;
30 glGenVertexArrays(1, &VAO_ID);
31 glBindVertexArray(VAO_ID);
32 glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, BUFFER_OFFSET(0));
33 glEnableVertexAttribArray(0);
34
35 // ----------------创建着色器程序对象----------------
36 gpuProgram.AddShader(GL_VERTEX_SHADER, "F:/VC++游戏编程/OpenGLGuide/OpenGL04/square.vert");
37 gpuProgram.AddShader(GL_FRAGMENT_SHADER, "F:/VC++游戏编程/OpenGLGuide/OpenGL04/square.frag");
38 GLuint program = gpuProgram.CreateGPUProgram();
39 glUseProgram(program);
40
41 location = glGetUniformLocation(program, "mat_simple_transform");
42 }
43
44 void display_04()
45 {
46 // ----------------创建缩放矩阵----------------
47 Matrix4X4 mat = Matrix4X4::CreateRotateMatrix(PI / 4, Vector3D(0.0, 0.0, 1.0));
48
49 // ----------------通过uniform上传至GPU----------------
50 glUniformMatrix4fv(location, 1, GL_FALSE, mat._m);
51
52 // ----------------清空颜色缓存----------------
53 glClear(GL_COLOR_BUFFER_BIT);
54
55 // ----------------绘制正方形图像----------------
56 glDrawArrays(GL_LINE_LOOP, 0, 4);
57
58 // ----------------同步执行结束----------------
59 glFlush();
60 }
61
62 int main(int argc, char **argv)
63 {
64 try
65 {
66 glutInit(&argc, argv);
67 glutInitDisplayMode(GLUT_RGBA);
68 glutInitWindowSize(512, 512);
69 glutInitContextVersion(3, 3);
70 glutInitContextProfile(GLUT_CORE_PROFILE);
71 glutCreateWindow(argv[0]);
72
73 glewExperimental = GL_TRUE;
74 if (GLEW_OK != glewInit())
75 {
77 std::exit(EXIT_FAILURE);
78 }
79
80 initialize_04();
81 glutDisplayFunc(display_04);
82 glutMainLoop();
83
84 return 0;
85 }
86 catch (const std::exception& ke)
87 {
88 std::cerr << "Error happened because of " << ke.what();
89 return -1;
90 }
91 }
运行效果就是绘制了一个正方形的轮廓,如下图所示:
效果确实不怎么样^_^,但是可以阐述知识点和OpenGL API。可以看出,图中的正方形是旋转得到的。旋转矩阵通过下面这个我们自己写的接口CreateRotateMatrix获得(该矩阵的定义已在笔记(二)中做了介绍,在此不再赘述了),该函数的函数签名如下:
static Matrix4X4 CreateRotateMatrix(double dAngle, const Vector3D& rotateAxis);
dAngle ——旋转的角度
rotateAxis ——旋转轴向量
紧接着就是通过glUniformMatrix4fv的方式将上述旋转矩阵上传至服务(GPU)端。上传之前,就像我们上传文件到服务器一样,需要知道上传的位置,所以在调用此函数前,会先调用glGetUniformLocation函数获得Uniform变量的索引值(见程序:41行)。事实上,GLSL编译器在链接着色器程序时,会创建一个uniform变量列表,在设置unifrom前所获取的索引便是在该列表中的索引。glGetUniformLocation函数签名如下:
GLint glGetUniformLocation(GLuint program, const char* name);
program ——链接后的GLSL程序标识ID
name ——uniform在着色器程序代码中的变量名
返回值 ——uniform变量在uniform变量列表中的索引值
上述函数的返回值和输入参数program都比较简单,但name(变量名)的设置可以比较灵活——可以是单一的变量名称,也可以是结构体中成员变量(域)的名称(通过.操作符访问),或者是数组中的某一条目(通过[]索引访问),下面是几个较为复杂的实例:
着色器中的声明如下:
1 uniform struct
2 {
3 struct
4 {
5 float a;
6 float b[10];
7 } c[2];
8 vec2 d;
9 } e;
应用程序端获取索引值的函数调用为:
1 GLint loc1 = glGetUniformLoaction(program, "e.d"); // ok
2 GLint loc1 = glGetUniformLoaction(program, "e.c[0]"); // error
3 GLint loc1 = glGetUniformLoaction(program, "e.c[0].b"); // ok, 数组第一个元素的索引
4 GLint loc1 = glGetUniformLoaction(program, "e.c[0].b[2]"); // ok
确定好存放位置之后,就可以通过glUniformMatrix4fv接口向GPU端上传数据了,其函数签名为:
void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* values)
location ——上传数据在uniform列表中索引值
count ——上传的矩阵个数
transpose ——行主序还是列主序
value ——指向矩阵数据的指针
上传了数据之后,就可以在着色器程序中对unifrom变量进行访问了,例如,顶点着色器中访问旋转矩阵的代码如下:
#version 330 core
uniform mat4 mat_simple_transform;
layout(location = 0) in vec4 vPosition;
void main()
{
gl_Position = mat_simple_transform * vPosition;
}
实践提示:① 一定要保证着色器中的uniform变量类型一定要和应用程序上传的数据类型一致。例如在我们的着色器程序中,变换矩阵元素的类型是单精度浮点型,那上传的数据也必须是单精度浮点型的。我在写这段程序的时候,就遇到了一个错误:着色器中使用的是mat4,也就是矩阵元素单精度浮点型,但上传的数据时使用了double类型(双精度浮点型),而且这样的错误还很难发现——编译器并没有提示,只是图都不见了;② 另外GLSL支持的隐式类型转换更少,更要保持类型的一致性。例如:如果矩阵是mat4类型的,vPosition必须是vec4类型的,那么它们之间是不能运算的,会有编译错误。
4 变换矩阵的综合使用
这一节主要对笔记(二)定义的坐标变换矩阵的综合运用,给出几个基于目前所了解的知识点就能实现的绘制实例,也算是对前两篇读书笔记的温习吧!这里的例子,顶点数据就是上述例子中的数据——绘制的都是正方形轮廓,只是通过不同的display函数实现不同的展示效果。
4.1 缩放
将绘制函数修改为如下函数:
1 void display_04_scale()
2 {
3 // ----------------清空颜色缓存----------------
4 glClear(GL_COLOR_BUFFER_BIT);
5
6 for (int i = 1; i <= 10; i++)
7 {
8 // ----------------创建缩放矩阵----------------
9 Matrix4X4 mat = Matrix4X4::CreateScaleMatrix(i * 0.1);
10
11 // ----------------通过uniform上传至GPU----------------
12 glUniformMatrix4fv(location, 1, GL_FALSE, mat._m);
13
14 // ----------------绘制正方形图像----------------
15 glDrawArrays(GL_LINE_LOOP, 0, 4);
16 }
17 }
可以得到下面的显示效果:
4.2 平移
将绘制函数改为如下函数:
1 void display_04_translate()
2 {
3 // ----------------清空颜色缓存----------------
4 glClear(GL_COLOR_BUFFER_BIT);
5
6 for (int i =-4; i <= 4; i++)
7 {
8 // ----------------创建缩放矩阵----------------
9 Matrix4X4 mat = Matrix4X4::CreateTranslateMatrix(
10 Vector3D(i * 0.1, i * 0.1, 0.0));
11
12 // ----------------通过uniform上传至GPU----------------
13 glUniformMatrix4fv(location, 1, GL_TRUE, mat._m);
14
15 // ----------------绘制正方形图像----------------
16 glDrawArrays(GL_LINE_LOOP, 0, 4);
17 }
18 }
可以得到下述效果:
4.3 综合效果
下面代码演示的是各种不同坐标变换矩阵相乘得到的变换矩阵,对图形进行变换,然后进行绘制:
1 void display_04_combine()
2 {
3 // ----------------清空颜色缓存----------------
4 glClear(GL_COLOR_BUFFER_BIT);
5
6 // 上
7 {
8 Matrix4X4 mat = Matrix4X4::CreateTranslateMatrix(Vector3D(0.0, 0.5, 0.0));
9 mat = mat * Matrix4X4::CreateScaleMatrix(0.3);
10 mat = mat * Matrix4X4::CreateRotateMatrix(0, Vector3D(0.0, 0.0, 1.0));
11 glUniformMatrix4fv(location, 1, GL_TRUE, mat._m);
12 glDrawArrays(GL_LINE_LOOP, 0, 4);
13 }
14
15 // 右
16 {
17 Matrix4X4 mat = Matrix4X4::CreateTranslateMatrix(Vector3D(0.5, 0.0, 0.0));
18 mat = mat * Matrix4X4::CreateScaleMatrix(0.4);
19 mat = mat * Matrix4X4::CreateRotateMatrix(PI / 12, Vector3D(0.0, 0.0, 1.0));
20 glUniformMatrix4fv(location, 1, GL_TRUE, mat._m);
21 glDrawArrays(GL_LINE_LOOP, 0, 4);
22 }
23
24 // 下
25 {
26 Matrix4X4 mat = Matrix4X4::CreateTranslateMatrix(Vector3D(0.0, -0.5, 0.0));
27 mat = mat * Matrix4X4::CreateScaleMatrix(0.5);
28 mat = mat * Matrix4X4::CreateRotateMatrix(PI / 6, Vector3D(0.0, 0.0, 1.0));
29 glUniformMatrix4fv(location, 1, GL_TRUE, mat._m);
30 glDrawArrays(GL_LINE_LOOP, 0, 4);
31 }
32
33 // 左
34 {
35 Matrix4X4 mat = Matrix4X4::CreateTranslateMatrix(Vector3D(-0.5, 0, 0.0));
36 mat = mat * Matrix4X4::CreateScaleMatrix(0.6);
37 mat = mat * Matrix4X4::CreateRotateMatrix(PI / 4, Vector3D(0.0, 0.0, 1.0));
38 glUniformMatrix4fv(location, 1, GL_TRUE, mat._m);
39 glDrawArrays(GL_LINE_LOOP, 0, 4);
40 }
41 }
效果图:
5 总结
这一次读书笔记主要是对上次读书笔记的回顾与应用,同时讲述了GLSL着色器程序的编译以及如何通过uniform变量向着色器程序传递数据。这次读书笔记内容虽然较多,但相对前一次读书笔记来说,要简单一些。到目前为止,我们还是在二维领域摸索,但OpenGL主要是用于三维图形的绘制,接下来我们会将这些基础知识应用到三维图形的绘制。哎,明天又要上班了,又不能睡懒觉了,~~~~(>_<)~~~~