文章目录
LearnGL - 学习笔记目录
本人才疏学浅,如有什么错误,望不吝指出。
上一篇:LearnGL - 02 - DrawTriangle,学会了如何画一个最最基本的三角形。
这一篇:我将在上一篇简单的例子的基本上添加一些功能,便于学习 shader。
什么是 shader ?
shader 直译是:着色器。
shader 是用 GLSL 编写的(OpenGL Shading Language,的简写, 意思是 OpenGL 着色器语言 )
GLSL 是一种 CLike 语言,所以与 C语言非常类似。
在 OpenGL 中,我们用 GLSL 编写的 shader 编译好后,都会链接到一个 Program(程序)对象中,OpenGL 调用该程序对象中链接到的 shader 来处理对应阶段的逻辑。
Program(程序)对象的 Shader 对应有几个阶段:
- Vertex Shader(顶点着色器)
- Tessellation Control Shader(细分控制着色器)
- Tessellation Evaluation Shader(细分计算着色器)
- Geometry Shader(几何着色器)
- Fragment Shader(片段/片元着色器)
其中顶点、片元着色器是必须的,其他都是可选的。
之前的文章 LearnGL - 01.1 - OpenGL 概述 & 管线概述 中我也有列出对应 OpenGL 的简述渲染管线图解,这里再引用一下:
由上面图解所示,箭头流向也就是阶段执行的顺序。
每个阶段的输入,就是上个阶段的输出。
这也是目前着色器中唯一能在着色器之间单向通讯的规则。
另外,着色器的 编译、链接 在之前的文章也有简述 LearnGL - 02 - DrawTriangle - VBO,这里不在说明。
下面使用 shader 来给之前的三角形的每个顶点添加一些不同的颜色,首先是在应用程序层添加顶点的颜色数据。
给顶点添加颜色(attribute, varying)
添加顶点数据
我们原来的顶点数据是:
float vertices[] = {
// x, y, z
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
现在需要添加颜色,而在GLSL中颜色值最终的输出都是0.0~1.0范围的,都是float浮点型,所以我们在此基础上有添加了3个颜色分量,RGB
float vertices[] = {
// x, y, z r, g, b
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
};
着色器也添加对应的顶点属性(添加 VS:attribute,varying,FS:varying)
下面是原本的两着色器:顶点、片元着色器
前面有说着色器的输入,就是上一个阶段的输出。反过来讲:着色器的输出,就是下个阶段的输入。
但有一些另外,就是着色器阶段中的 头部 着色器,与 尾部 着色器。
- 头部 着色器就是我们的:顶点着色器 vertex shader,它的上个着色器阶段是不存在的,对它输入数据的就是应用程序层,在这里就是我们在 C++ 写的代码来设置 vertex shader 的 vertex attribute(顶点属性)的部分,就是应用层对顶点着色器的数据输入。
- 尾部 着色器就是我们的:片元/片段着色器 fragment shader,它的下个着色器阶段是不存在的,它的输出数据是可以指向 帧缓存目标写入数据的(输出数据)。
在这里我们使用的是 attribute
、varying
关键字。也可以使用 in
、out
关键字。
- 在 顶点着色器中
attribute
是专门给顶点着色器 定义 输入数据的变量用的。varying
是顶点着色器输出给片元着色器用的(或是存在下一个可选着色器阶段的话,会先传给下个阶段)。而 在顶点着色器的varying
变量有个特定,它在会在光栅化阶段对其他生成的片元中,对该varying
变量进行顶点之间的插值得来的。
- 在 片元着色器中
- 不使用
attribute
关键字定义变量,否则会报错。因为attribute
只能在顶点着色器中使用。 varying
是上个着色器阶段传入的片元。一般都会经过顶点之间的插值得到的。
- 不使用
在下面我们对原来的顶点、片元着色器添加了对应的输入、输出变量。
static const char* vertex_shader_text =
"#version 450 compatibility\n"
"attribute vec3 vPos;\n"
"void main() {\n"
" gl_Position = vec4(vPos, 1.0);\n"
"}\n";
static const char* fragment_shader_text =
"#version 450 compatibility\n"
"void main() {\n"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
下面因为需要填加颜色的顶点的颜色输出,需要增加3个float分量的attribute、还有varying属性:
static const char* vertex_shader_text =
"#version 450 compatibility\n"
"attribute vec3 vPos;\n"
"attribute vec3 vCol;\n"
"varying vec3 fCol;\n"
"void main() {\n"
" gl_Position = vec4(vPos, 1.0);\n"
" fCol = vCol;\n"
"}\n";
static const char* fragment_shader_text =
"#version 450 compatibility\n"
"varying vec3 fCol;\n"
"void main() {\n"
" gl_FragColor = vec4(fCol, 1.0);\n"
"}\n";
这里再次强调说明一下,顶点着色器添加了:attribute vec3 vPos;
varying vec3 fCol;
两个属性,前者是attribute的,用于顶点着色器输入的,后者varying用于顶点传递到片元着色器用的属性,外部不需要设置,我们只要在顶点的main函数里对varying赋值就好,这样片元着色器中的 varying 变量会得到顶点之间的插值数据,只要顶点着色器的varying的变量名称 与 片元着色器中的varying的变量名称对应上就可以传递了,否则会有着色器程序的链接错误。
如果我给片元着色器中的 varying vec3 fCol;
改为 varying vec3 fColAAA;
,后者多加了 AAA 三个字符的变量名。
运行的话就会得到着色器程序链接的错误提示(还记得我们上一篇有讲得如何输出错误把?)
修改了变量名后的片元着色器
static const char* fragment_shader_text =
"#version 450 compatibility\n"
"varying vec3 fColAAA;\n"
"void main() {\n"
" gl_FragColor = vec4(fColAAA, 1.0);\n"
"}\n";
运行后的报错:
- Program Linking 的错误
- fColAAA 有错误相关的变量名
- previous 前一个shader,即fColAAA的前一个shader,在这里就是vertex shader顶点着色器
- not write 就是说前面的vertex shader没有对fColAAA写入数据
在应用程序设置顶点颜色的属性(设置:VS:attribute)
GLint vcol_location;
...
vpos_location = glGetAttribLocation(program, "vPos"); // 获取 顶点着色器中的顶点 attribute 属性的 location
vcol_location = glGetAttribLocation(program, "vCol"); // 获取 顶点着色器中的顶点 attribute 属性的 location
glVertexAttribPointer(vpos_location, 3, GL_FLOAT, GL_FALSE, // 设置 顶点属性 vPos 格式
sizeof(float) * 6, (void*)0);
glEnableVertexAttribArray(vpos_location); // 启用 顶点缓存 location 位置的属性
glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE, // 设置 顶点属性 vCol 格式
sizeof(float) * 6, (void*)(sizeof(float) * 3));
glEnableVertexAttribArray(vcol_location); // 启用 顶点缓存 location 位置的属性
注意 glVertexAttribPointer 函数调用时:
- 第一个参数 设置对应的 location 索引值
vpos_locaiton
的不变,vcol_location
的也是通过glGetAttribLocation
来获取即可。 - 第二个参数 还是填入
3
即可,因为颜色也是RGB三个分量。 - 第三个参数 和 第四个参数 都不变
- 第五个参数 原始是
sizeof(float) * 3
的,现在因为每个顶点属性都添加了一个三个分量的RGB,所以6 个 float 的字节大小。改为:sizeof(float) * 6
就可以了。 - 第六个参数 主要是设置
vcol_location
的需要该为(void*)(sizeof(float)*3)
,因为从偏移3个float的字节数开始,因为前面3个float的字节数据都是 vPos 用的。
这时 顶点属性(Vertex Attribute) 有两个,对应到着色器中就是:一个是:vPos
,另一个是 vCol
。
另外 顶点属性的数量也是有限的,可以通过以下 API 查看你的 OpenGL 环境支持多少个:
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum number of vertex attributes supported : " << nrAttributes << std::endl;
如下图,表示两次设置 glVertexAttribPointer
时,每次设置的属性使用对应的显卡内存(显存)。
绘制效果
让三角型旋转起来(使用:uniform)
说到旋转,它也是一种线性变换,也可以用矩阵来描述,这里不再对矩阵过多的描述。
关于矩阵:
- 推荐阅读《Unity 入门精要》,里面讲得很不错。或是搜索相关的 3D 矩阵 相关的。
- 变换 - 排版超级好
- 可汗学院
使用现成数学库:
- github glfw/deps/linmath.h
- openglredbook/examples/blob/master/include/vmath.h
- 变换里头推荐使用的是:Git:GLM
- GLM 是 OpenGL Mathematics 的缩写。
GLM 下实现一个 Camera 相机矩阵可以这么弄:
#include <glm/vec3.hpp> // glm::vec3
#include <glm/vec4.hpp> // glm::vec4
#include <glm/mat4x4.hpp> // glm::mat4
#include <glm/ext/matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale
#include <glm/ext/matrix_clip_space.hpp> // glm::perspective
#include <glm/ext/scalar_constants.hpp> // glm::pi
glm::mat4 camera(float Translate, glm::vec2 const& Rotate)
{
// fov, aspect near far
glm::mat4 Projection = glm::perspective(glm::pi<float>() * 0.25f, 4.0f / 3.0f, 0.1f, 100.f);
glm::mat4 View = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -Translate));
View = glm::rotate(View, Rotate.y, glm::vec3(-1.0f, 0.0f, 0.0f));
View = glm::rotate(View, Rotate.x, glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 Model = glm::scale(glm::mat4(1.0f), glm::vec3(0.5f));
return Projection * View * Model;
}
但是 OpenGL 库并没有封装 应用程序层 的数学库(我是没发现有,但是 GLSL 是有的,现在终于知道为何没发现矩阵相关的API了,原来是因为之前旋转 glad 服务选项时,旋转的 profile 为 core 的话,会将兼容版本的API都会删除,而我用的是4.5版本的,在4.0 之后就没有 matrix state 矩阵状态管理的接口了,所以如果你想用回这些API,那么 glad 的 profile 记得选择:compatibility)。
然后我看了 GLFW 中的例子都是使用第三方的源码。
就在一个文件里 : linmath.h
(linmath==linear algebra mathematic(-lib) 线性代数数学(库)),这个头文件里包含了对 vec2~4
,mat4x4
,quat
的类型和 函数 的声明及定义。
linmath.h 头文件内容点击这里查看: github glfw/deps/linmath.h
vec2~4
就是包含2 到 4 个float的分量的向量类型定义:typedef float vec2[2];
typedef float vec3[3];
typedef float vec4[4];
的声明。vec 就是 vector 的缩写。mat4x4
就是包含了 4 个4分量的vec4 的矩阵类型定义:typdef vec4 mat4x4[4];
。mat 就是 matrix 的缩写。quat
就是包含了4个float分量的四元数类型定义:typedef float quat[4];
。quat 就是 quaternion 的缩写。
下面我们就使用 mat4x4
来制作 旋转 的效果。
在顶点着色器添加 uniform 的矩阵
前面有说,在 GLSL 有封装这些数学库了。直接用 mat4
类型。
然后在原来的顶点着色器添加,并应该 uniform
变化矩阵后:
static const char* vertex_shader_text =
"#version 450 compatibility\n"
"uniform mat4 rMat;\n" // 添加了这一行
"attribute vec3 vPos;\n"
"attribute vec3 vCol;\n"
"varying vec3 fCol;\n"
"void main() {\n"
" gl_Position = rMat * vec4(vPos, 1.0);\n" // 应用 rMat 的矩阵变换
" fCol = vCol;\n"
"}\n";
为何使用 uniform
修饰符的呢?
首先:uniform
直译就是:统一的,一致的,不变的。adj。
因为这个在单次绘制中,对于整个图元来说都是不会修改的。uniform
是对整个 着色器程序 的 所有着色器 都是 共享使用 的。(之前的文章有讲到)
"uniform mat4 rMat;\n" // 添加了这一行
还记得之前的文章有说如何在应用层设置吗?我之前只是提到了一下。
之前 attribute
属性变量是通过 getBindAttribLocation
来获取 attribute 的索引值的。
同样的 uniform
也是可以获取 uniform
修饰分类的索引值(通过 glGetUniformLocation API 获取 uniform location)。
所有这些索引值,都是在 着色器程序链接时,给每个变量 确定他的 location,如果没有明文指定location,链接器自动根据所有其他变量的使用情况来这个变量分配一个location。
先获取 uniform 变量的 location
通过 glGetUniformLocation API 获取 uniform location
这里要说明的是,location 是按 4个分量对齐的。
那么意味着不论是 uniform 还是 attribute 的location值与类型都有关系:
- GLSL 定义:
layout(location = 0) uniform float intensity;
或是uniform float intensity;
(不明文指定location的,编译器会自行设置一个) 就算定义了只有一个float分量的intensity 变量,也会占用一个坑位(索引位),这个坑位占了4分量的坑位。 - 你也可以定义:
uniform vec2 pos;
这样这个坑位上就使用了前两个分量。
着色器中给输入输出不单只是上面的简单使用方式。
还可以使用数据块的输入、输出。
GLSL 440 版本还对数据块中成员定义 layout(…) 来定义 location, component。
如:
in Lighting {
layout(location=0, component=3) vec3 col; // 灯光颜色:col 在0索引值的坑位,占3个分量
layout(location=0, component=1) float intensity; // 灯光强度:intensity 也在0索引值的坑位,占1个分量
};
这样上面的col、intensity都在一个坑位,节省GPU资源。用起来也很清晰。
那么,重点来了:GLSL中 mat4 是 相当于由 4个 vec4,所以一个 mat4 ,占四个 location 的索引坑位:
举个例子:
// jave.lin : vertex shader 的代码
#version 450 compatibility
uniform mat4 rMat; // 这里声明一个 uniform mat4
uniform vec4 property1; // 这里声明一个 uniform vec4
uniform float property2;// 这里声明一个 uniform float
uniform vec2 property3;// 这里声明一个 uniform vec2
在应用程序层调用 glGetUniformLocation 获取他们的 location索引值:
GLint rmat_location = glGetUniformLocation(program, "rMat"); // 获取 uniform rmat_location == 0,但占用了0,1,2,3
GLint pro1_location = glGetUniformLocation(program, "property1"); // 获取 uniform property1 == 4,因为前面的 mat4 占了4个坑
GLint pro2_location = glGetUniformLocation(program, "property2"); // 获取 uniform property2 == 5,float只有1个分量,但也占用1个坑
GLint pro3_location = glGetUniformLocation(program, "property3"); // 获取 uniform property3 == 6
描述写在了注释里,可能不太清晰,我还是弄到正文吧:
rMat
的 location == 0,但占用了0,1,2,3property1
的 location == 4,因为前面的mat4
占了4个坑property2
的 location == 5,float
只有1个分量,但也占用property3
的 location == 6
前面的 Lighting
数据,如果在: OpenGL 4.4 或是说 GLSL 440 版本之前。
上面的用法只能这么写:
layout(location=0) in vec4 Lighting; // 然后在使用时取颜色:Lighting.rgb来取颜色,Lighting.a来取强度。明显第一种 440 版本的可能性更高。
OK,了解相关内容后,那么获取 uniform mat4 MVP;
的内容吧。
这里我们使用的是前面那种简单的方式:
// c++ 应用程序层代码
GLint mvp_location
...
rmat_location = glGetUniformLocation(program, "rMat"); // 获取 着色器程序的 uniform 变量的 location
这样就拿到 rmat_location
值了。
先 include
之前说的 linmath.h
头文件(linmath.h
文件我们在之前的 01 CreateWindow 那篇有将到这个是 glfw3.3.2/deps 下的依赖文件之一,我们已将其复制到 Dependencies/Include 下了,所以可以直接 #include
就好):
#include "linmath.h"
OK,导入 linmath.h
后,应用程序层就可以使用之前说的:mat4x4
。
然后是处理旋转,而重点是 旋转起来 ,就是让我们肉眼可见的动画。
而 CG 动画 或是 纸质动画 的本质都是每一渲染帧显示不同的内容呈现出来的动画效果的,只要渲染帧数量足够的多,渲染帧之间切换的频率足够的块,动画就越是流畅。早期的 走马灯 就是这个原理。
另外,早期的动画,都是以 24 帧为标准,现在早就超过这个标准了,因为 24 帧是相对那些移动幅度比较小的动画而言都还好。
这些就再说说一个单位术语:FPS,你也许在很多地方都看到过。FPS 就是 Frame Per Second 的缩写,就是:每秒多少帧的单位。
如果我们做的一些电脑全屏动画,就需要更高的帧率了,否则某些动画大幅度移动过程中会看到跳帧,不连贯的画面。所以我们一般全屏游戏,一般都是 60 FPS 的更新画面帧的频率。
现在有限IMAX 3D影院里有些电影,特别是科幻片,一般都有写着:60 帧,或是更高的:120 帧,的说明。其实也就是 60 FPS 或是 120 FPS。
而现在的 VR 要制作得好一些画质的话,都需要有 8K 分辨率画面,因为人的眼球可识别的差不多有 16K 分辨率画面吧(每个像素就按现在普通 2K 屏手机的像素那么大)。而现在的手机硬件很难做到 8K或是16K高清模型,超高帧率的渲染。所以在未来几百年、甚至在几千年的脑电波控制技术来临之前,这个 VR 的用户交互方式还是不会淘汰的。顶多就 VR 硬件使用便捷上、硬件性能上会有所提升。
而越是大的的分辨率,需要对于 3D 渲染管线来说,片元着色器的压力就越大。电影院里的超级高清蓝光都是超大的分辨率的,所以目前很多一些电影,特别是纯 CG(Computer Graphics) 电影,因为渲染压力,他们都要配置一些高端的渲染农场(就是一些快速、分布式渲染用的多个机器的渲染架构)来离线渲染或是实时渲染。
那么要有动画的话,我们需要重复执行绘制命令,每次都绘制不同的参数已达到动画效果,所以我们需要在 loop 循环里去更新旋转的矩阵,每一帧都修改 旋转矩阵 的值,再设置到 着色器程序中的 uniform mat4 rMat;
变量中去即可。
再根据 location 设置 uniform 变量
通过 glUniform* 函数设置,下面是用到了:glUniformMatrix4fv。
我们的 loop 循环就是 while
语句里的内容:
mat4x4 rMat; // 声明定义一个 mat4x4 用的旋转矩阵
while (!glfwWindowShouldClose(window)) { // 检测是否需要关闭窗体
glfwGetFramebufferSize(window, &width, &height); // 获取窗口大小
glViewport(0, 0, width, height); // 设置Viewport
glClearColor(0.1f, 0.2f, 0.1f, 0.f); // 设置清理颜色缓存时,填充颜色值
glClear(GL_COLOR_BUFFER_BIT); // 清理颜色缓存
mat4x4_identity(rMat); // 给矩阵单位化,消除之前的所有变换
mat4x4_rotate_Z(rMat, rMat, (float)glfwGetTime()); // 沿着 z 轴旋转,旋转量为当前 glfw 启用到现在的时间点(秒)
glUniformMatrix4fv(rmat_location, 1, GL_FALSE, (const GLfloat*)rMat); // 设置, 着色器中 uniform mat4 rMat; 的矩阵数据
glUseProgram(program); // 使用此着色器程序
glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角图元,从0号顶点,开始绘制3个顶点
glfwSwapBuffers(window); // swap buffer, from backbuffer to front buffer
glfwPollEvents(); // 处理其他的系统消息
}
留意着几行就可以了
mat4x4 rMat; // 声明定义一个 mat4x4 用的旋转矩阵
...
mat4x4_identity(rMat); // 给矩阵单位化,消除之前的所有变换
mat4x4_rotate_Z(rMat, rMat, (float)glfwGetTime()); // 沿着 z 轴旋转,旋转量为当前 glfw 启用到现在的时间点(秒)
glUniformMatrix4fv(rmat_location, 1, GL_FALSE, (const GLfloat*)rMat); // 设置, 着色器中 uniform mat4 rMat; 的矩阵数据
这里 linmath.h
的 API就不说,注释有简单的说明。
但是 glUniformMatrix4fv(rmat_location, 1, GL_FALSE, (const GLfloat*)rMat);
这一句还是要说明一下的:
前面我说给出的链接:glUniform* ,注意有个 *
号。
意思glUniform 知识个前缀,后面 *
还有多种名称组合出来的函数名:
(为何要搞这么多函数,因为 OpenGL 规范库都是由 C 标准 API 定义的,而 C 语言函数签名只有:函数名,不像 C++ 可以通过函数名,namespace,class name,参数类型,来作为函数签名,所以可以用参数类型来重载,C 语言不能通过 参数 来重载就只能多写几个函数名。)
有:
glUniform{1234}{fdi ui}(GLint location, TYPE value);
{1234} 是指定分量数量的,{fdi ui} 指定单个 f: float, d:double, i:int, ui:unsiged int 类型的 uniform 变量赋值。glUniform{1234}{fdi ui}v(GLint location, GLsizei count, const TYPE* values);
与 上面的函数区别在于函数别有个 v 结尾的字符,代表:vector,就是同类型数组,也叫 向量数组。除此之外,还多了个GLsizei count
参数,会载入 count 个数据的集合(根据glUniform*()
的调用方式,读入1~4个值),并写入location
位置的uniform
变量。如果location
是数组的起始索引值,那么数组之后的连续count
个元素都会被载入。glUniformMatrix{234}{fd}v(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
{234} 指定数据是以:2x2,3x3,4x4 的格式 其中transpose
参数如果为GL_TRUE
,则矩阵数据已行序读入。如果为GL_FALSE
,则以列序。value
参数组成NxN的矩阵数据的数组指针。glUniformMatrix{2x3,2x4,3x2,3x4,4x2,4x3}{fd}v(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
与上面相比,就是矩阵的维度可以指定不同的组合 {2x3,2x4,3x2,3x4,4x2,4x3}。其他都一样。
举些列子:
- glUniform1i 设置单个
int
例子
// 着色器
#version 450 compatibility
...
uniform int index;
// c++ 应用层
GLint index_data = -1;
GLint loc = glGetUniformLocation(program, "index");
glUniform1i(loc, index_data );
- glUniform1fv 设置
float
数组例子
// 着色器
#version 450 compatibility
...
uniform float buff1[16];
// c++ 应用层
GLfloat buff1_data[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
GLint loc = glGetUniformLocation(program, "buff1");
glUniform1fv(loc, buff1_data);
- glUniform1f 设置
float
数组某个元素例子
// 着色器
#version 450 compatibility
...
uniform float buff1[16];
// c++ 应用层
GLint loc = glGetUniformLocation(program, "buff1[4]"); // 从0开始,索引为4的元素,就是从1开始数的话,就是第5个元素
glUniform1f(loc, 8); // 将着色器 uniform 的 buff1[4] = 8;
- glUniformMatrix4v 社会组单个
mat4
矩阵例子
// 着色器
#version 450 compatibility
...
uniform mat4 rot_mat;
// c++ 应用层
mat4x4 rot_mat_data = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
GLint loc = glGetUniformLocation(program, "rot_mat");
glUniformMatrix4v(loc, 1, GL_FALSE, (GLFloat*)rot_mat_data);
- glUniformMatrix3x3v 设置单个
mat3
例子
// 着色器
#version 450 compatibility
...
uniform mat3 rot_mat;
// c++ 应用层
mat3x3 rot_mat_data = {1,2,3,4,5,6,7,8,9};
GLint loc = glGetUniformLocation(program, "rot_mat");
glUniformMatrix3x3v(loc, 1, GL_FALSE, (GLFloat*)rot_mat_data);
- glUniformMatrix4v 设置
mat4
矩阵数组例子
// 着色器
#version 450 compatibility
...
uniform mat4 rot_mat_arr[4];
// c++ 应用层
mat4x4 rot_mat_arr_data[4] = {
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
};
GLint loc = glGetUniformLocation(program, "rot_mat_arr");
glUniformMatrix4v(loc, 4, GL_FALSE, (GLFloat*)rot_mat_arr_data);
运行效果
换y轴来旋转
将 mat4x4_rotate_Z
换成 mat4x4_rotate_Y
诶,这与我们一般用的3D渲染系统不太一样呢,一般默认会有背面剔除的呢。
难不成默认是没有开启的吗?
查看面向剔除
可以用 glIsEnabled API 来查看 OpenGL 的一些开关值,如下,查看一下 GL_CULL_FACE 的符号常量值:
GLboolean cf = glIsEnabled(GL_CULL_FACE);
std::cout << "cull face enabled : " << (cf ? "true" : "false") << std::endl;
输出是:cull face enabled : false
还真没开启,那么我们可以试试开启看看效果:
设置面向剔除
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
GLboolean cf = glIsEnabled(GL_CULL_FACE);
std::cout << "cull face enabled : " << (cf ? "true" : "false") << std::endl;
输出是:cull face enabled : true,使用 glEnable 开启了剔除面向了,而且也使用了另一个 glCullFace API 设置了剔除面向是正面还是背面:这里剔除的是 GL_BACK
背面。
再看看运行效果:
可以看到 剔除背面 开启后,当三角面旋转到背面朝向镜头时,就看不到了。
注意
在着色器中,uniform、attribute 声明的变量,如果都没有在对应的着色器中有逻辑代码的引用,那么编译器会因为优化而删除这个变量的声明。
而 GLSL 编译器的优化开关可以通过:#pragma
指令来设置,这个指令只要在函数外部放置就可以了。
- 开启 代码编译优化:
#pragma optimize(on)
- 关闭 代码编译优化:
#pragma optimize(off)
默认是开启优化的。
完整源码
// jave.lin
#include"glad/glad.h"
#include"GLFW/glfw3.h"
//#include"linmath.h"
// 把linmath.h 放在 iostream 之前include会有错误,所以放到iostream 后include就好了
// 而这个错误正式 xkeycheck.h 文件内 #error 提示的,所以可以使用 #define _XKEYCHECK_H 这个头文件的引用标记宏
// 就可以避免对 xkeycheck.h 头文件的 include 了。
#include<iostream>
#include"linmath.h"
#define PRINT_VERSION // 打印版本相关信息
#ifdef PRINT_VERSION
#define PROFILE_NAME_CORE "core"
#define PROFILE_NAME_COMPAT "compat"
static const char* get_api_name(int api) {
if (api == GLFW_OPENGL_API)
return "OpenGL";
else if (api == GLFW_OPENGL_ES_API)
return "OpenGL ES";
return "Unknown API";
}
static const char* get_profile_name_gl(GLint mask) {
if (mask & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT)
return PROFILE_NAME_COMPAT;
if (mask & GL_CONTEXT_CORE_PROFILE_BIT)
return PROFILE_NAME_CORE;
return "unknown";
}
// 打印各种版本信息
static void print_infos(GLFWwindow* window) {
//
// ====== GLFW Library的版本 ======
//
int glfw_major, glfw_minor, glfw_revision;
glfwGetVersion(&glfw_major, &glfw_minor, &glfw_revision);
// 头文件声明版本
printf("GLFW header version: %u.%u.%u\n",
GLFW_VERSION_MAJOR,
GLFW_VERSION_MINOR,
GLFW_VERSION_REVISION);
// 库版本
printf("GLFW library version: %u.%u.%u\n", glfw_major, glfw_minor, glfw_revision);
// 库版本的字符串描述
printf("GLFW library version string: \"%s\"\n", glfwGetVersionString());
//
// ====== client, context, profile 的版本 ======
//
int ch, client, major, minor, revision, profile;
client = glfwGetWindowAttrib(window, GLFW_CLIENT_API);
major = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR);
minor = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MINOR);
revision = glfwGetWindowAttrib(window, GLFW_CONTEXT_REVISION);
profile = glfwGetWindowAttrib(window, GLFW_OPENGL_PROFILE);
printf("%s context version string: \"%s\"\n",
get_api_name(client),
glGetString(GL_VERSION));
printf("%s context version parsed by GLFW: %u.%u.%u\n",
get_api_name(client),
major, minor, revision);
GLint mask;
glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &mask);
printf("%s profile mask (0x%08x): %s\n",
get_api_name(client),
mask,
get_profile_name_gl(mask));
//
// ====== render, vendor 的信息 ======
//
printf("%s context renderer string: \"%s\"\n",
get_api_name(client),
glGetString(GL_RENDERER));
printf("%s context vendor string: \"%s\"\n",
get_api_name(client),
glGetString(GL_VENDOR));
printf("%s context shading language version: \"%s\"\n",
get_api_name(client),
glGetString(GL_SHADING_LANGUAGE_VERSION));
}
#endif // PRINT_VERSION
static const char* vertex_shader_text =
"#version 450 compatibility\n"
"uniform mat4 rMat;\n"
"attribute vec3 vPos;\n"
"attribute vec3 vCol;\n"
"varying vec3 fCol;\n"
"void main() {\n"
" gl_Position = rMat * vec4(vPos, 1.0);\n"
" fCol = vCol;\n"
"}\n";
static const char* fragment_shader_text =
"#version 450 compatibility\n"
"varying vec3 fCol;\n"
"void main() {\n"
" gl_FragColor = vec4(fCol, 1.0);\n"
"}\n";
float vertices[] = {
// x, y, z r, g, b
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
};
static void error_callback(int error, const char* description) {
fprintf(stderr, "ErrorCode : %d(0x%08x), Error: %s\n", error, error, description);
}
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { // 当键盘按键ESCAPE按下时,设置该window为:需要关闭
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
int main() {
glfwSetErrorCallback(error_callback); // 安装glfw内部错误时的回调
if (!glfwInit()) { // 初始化glfw
std::cout << "glfwInit FAILURE" << std::endl; // 初始化失败
exit(EXIT_FAILURE);
}
// 设置最低的openGL 版本,major:主版本号,minor:次版本号
// openGl 太低版本的话是不支持CORE Profile模式的
// 会报错:ErrorCode: 65540(0x00010004), Error : Context profiles are only defined for OpenGL version 3.2 and above
//glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
//glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
// 根据上面的错误提示,至少使用3.2才行,这里我们使用4.5
//glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
//glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
// core profile 下运行有问题,不显示任何内容,但不会报错。
// 着色器编译、着色器程序链接都没有错误日志信息。
// 很有可能是因为我参考的学习网站使用的API相对比较老,使用的是3.3的。
//glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 所以这里我们不设置 major, minor的版本,默认使用本计算机能用的最高版本
// 使用 compatibility profile 就有内容出现了。
int width = 800;
int height = 600;
// 使用glfw创建窗体
GLFWwindow* window = glfwCreateWindow(width, height, "jave.lin - Learning OpenGL - 02_01 DrawTriangle_Extensions", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl; // 构建窗体失败
glfwTerminate();
exit(EXIT_FAILURE);
}
glfwMakeContextCurrent(window);
glfwSetKeyCallback(window, key_callback); // 安装glfw内部键盘按键的回调
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { // 装载OpenGL的C函数库
std::cout << "Failed to initialize OpenGL context" << std::endl; // 装载报错
glfwTerminate();
exit(EXIT_FAILURE);
}
#ifdef PRINT_VERSION
// 打印版本信息
print_infos(window);
#endif
GLint rmat_location, vpos_location, vcol_location;
GLuint vertex_buffer, vertex_shader, fragment_shader, program;
GLint success, infoLogLen;
glGenBuffers(1, &vertex_buffer); // 创建 VBO
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); // 绑定 VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 设置 VBO 数据
vertex_shader = glCreateShader(GL_VERTEX_SHADER); // 创建 顶点着色器
glShaderSource(vertex_shader, 1, &vertex_shader_text, NULL); // 设置 顶点着色器源码
glCompileShader(vertex_shader); // 编译 顶点着色器
glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success); // 获取着色器编译状态
if (!success) { // 如果编译不成功
glGetShaderiv(vertex_shader, GL_INFO_LOG_LENGTH, &infoLogLen);
GLchar* infoLog = (GLchar*)malloc(infoLogLen); // 如果编译失败,则将编译日志储存到:infoLog 中
glGetShaderInfoLog(vertex_shader, infoLogLen, NULL, infoLog);
std::cout << "Vertex Shader Compiling Error Status: " // 输出编译错误日志信息
<< success << ", Infomation Log : " << infoLog << std::endl;
free(infoLog);
exit(EXIT_FAILURE);
}
fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); // 创建 片元着色器
glShaderSource(fragment_shader, 1, &fragment_shader_text, NULL); // 设置 片元着色器源码
glCompileShader(fragment_shader); // 编译 片元着色器
glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success); // 获取着色器编译状态
if (!success) { // 如果编译不成功
glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &infoLogLen);
GLchar* infoLog = (GLchar*)malloc(infoLogLen); // 如果编译失败,则将编译日志储存到:infoLog 中
glGetShaderInfoLog(fragment_shader, infoLogLen, NULL, infoLog);
std::cout << "Fragment Shader Compiling Error Status: " // 输出编译错误日志信息
<< success << ", Infomation Log : " << infoLog << std::endl;
free(infoLog);
exit(EXIT_FAILURE);
}
program = glCreateProgram(); // 创建着色器程序
glAttachShader(program, vertex_shader); // 附加 顶点着色器
glAttachShader(program, fragment_shader); // 附加 片元着色器
glLinkProgram(program); // 链接着色器程序
glGetProgramiv(program, GL_LINK_STATUS, &success); // 如果链接不成功
if (!success) {
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLen);
GLchar* infoLog = (GLchar*)malloc(infoLogLen); // 如果链接失败,则将链接日志储存到:infoLog 中
glGetProgramInfoLog(program, infoLogLen, NULL, infoLog);
std::cout << "Program Linking Error Status: " // 输出链接错误日志信息
<< success << ", Infomation Log : " << infoLog << std::endl;
free(infoLog);
exit(EXIT_FAILURE);
}
glDeleteShader(vertex_shader); // 链接成功后,就可以删掉vertex 子程序了
glDeleteShader(fragment_shader); // 链接成功后,就可以删掉fragment 子程序了
rmat_location = glGetUniformLocation(program, "rMat"); // 获取 着色器程序的 uniform 变量的 location
vpos_location = glGetAttribLocation(program, "vPos"); // 获取 顶点着色器中的顶点 attribute 属性的 location
vcol_location = glGetAttribLocation(program, "vCol"); // 获取 顶点着色器中的顶点 attribute 属性的 location
glVertexAttribPointer(vpos_location, 3, GL_FLOAT, GL_FALSE, // 设置 顶点属性 vPos 格式
sizeof(float) * 6, (void*)0);
glEnableVertexAttribArray(vpos_location); // 启用 顶点缓存 location 位置的属性
glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE, // 设置 顶点属性 vCol 格式
sizeof(float) * 6, (void*)(sizeof(float) * 3));
glEnableVertexAttribArray(vcol_location); // 启用 顶点缓存 location 位置的属性
mat4x4 rMat; // 声明定义一个 mat4x4 用的旋转矩阵
glEnable(GL_CULL_FACE); // 开启面向剔除
glCullFace(GL_BACK); // 设置剔除背面
GLboolean cf = glIsEnabled(GL_CULL_FACE); // 查看是否启用面向剔除
std::cout << "cull face enabled : " << (cf ? "true" : "false") << std::endl;
while (!glfwWindowShouldClose(window)) { // 检测是否需要关闭窗体
glfwGetFramebufferSize(window, &width, &height); // 获取窗口大小
glViewport(0, 0, width, height); // 设置Viewport
glClearColor(0.1f, 0.2f, 0.1f, 0.f); // 设置清理颜色缓存时,填充颜色值
glClear(GL_COLOR_BUFFER_BIT); // 清理颜色缓存
mat4x4_identity(rMat); // 给矩阵单位化,消除之前的所有变换
mat4x4_rotate_Y(rMat, rMat, (float)glfwGetTime()); // 沿着 y 轴旋转,旋转量为当前 glfw 启用到现在的时间点(秒)
glUniformMatrix4fv(rmat_location, 1, GL_FALSE, (const GLfloat*)rMat); // 设置, 着色器中 uniform mat4 rMat; 的矩阵数据
glUseProgram(program); // 使用此着色器程序
glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角图元,从0号顶点,开始绘制3个顶点
glfwSwapBuffers(window); // swap buffer, from backbuffer to front buffer
glfwPollEvents(); // 处理其他的系统消息
}
glfwDestroyWindow(window); // 销毁之前创建的window对象
glfwTerminate(); // 清理glfw之前申请的资源
return 0;
} // int main() {
References
- 变换 - 排版超级好