LearnGL - 02.1 - DrawTriangle_Extension - VBO/Shader


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,它的下个着色器阶段是不存在的,它的输出数据是可以指向 帧缓存目标写入数据的(输出数据)。

在这里我们使用的是 attributevarying 关键字。也可以使用 inout 关键字。

  • 在 顶点着色器中
    • 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)

说到旋转,它也是一种线性变换,也可以用矩阵来描述,这里不再对矩阵过多的描述。

关于矩阵:

使用现成数学库:

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~4mat4x4quat 的类型和 函数 的声明及定义。

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,3
  • property1 的 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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值