- VAO,VBO,EBO/IBO 几个重要的数据对象
- 顶点变换处理
- 顶点缓存创建、绑定、设置数据
- 着色器
- 顶点着色器
- 片元着色器
- 使用着色器
- 取得编写的 GLSL 着色器脚本
- 顶点着色器
- 片元着色器
- 创建着色器程序
- 创建着色器子程序,设置好子程序类型,设置好对应脚本
- 编译着色器子程序
- 编译失败诊断日志
- 将子程序附加到着色器程序
- 将着色器附加到程序
- 将程序链接
- 链接失败诊断日志
- 设置当前管线使用的程序
- 设置顶点属性格式
- 开始绘制
- 视口大小
- 清理缓存
- 绘制
- 绘制效果
- 完整代码
- References
LearnGL - 学习笔记目录
本人才疏学浅,如果什么错误,望不吝指出。
VAO,VBO,EBO/IBO 几个重要的数据对象在OpenGL中,我们可以看到很多相关的博客,或是开源项目中都可以看到,VAO,VBO,EBO/IBO,到底是什么鬼。
这几个玩意都是一些英文的缩写:
- VAO : Vertex Array Object,顶点数组对象
- VBO : Vertex Buffer Object, 顶点缓存对象
- EBO/IBO : Element/Index Buffer Object, 索引缓存对象
这篇只是用 VBO,以后学习使用到 VAO、EBO/IBO 再说吧:
- VBO 出了本篇,还有下一篇:
- LearnGL - 02.1 - DrawTriangle_Extension - VBO/Shader
- EBO/IBO 的文章:
- LearnGL - 03 - DrawQuad - VBO/EBO
- VAO 相关:
- LearnGL - 04 - VAO 探究
- LearnGL - 04.1 - DrawDoubleQuad - UsingVAO
在学习 使用 OpenGL 绘制内容前,最好线了解基本的 OpenGL 渲染管线,References 文章 你好,三角形 也有讲到。要想更详细讲解,大家可以搜索了解:图形渲染管线 或是 光栅化图形渲染管线。或是查看之前的一篇 LearnGL - 01.1 - OpenGL 概述 & 管线概述
在现代的GPU设计中,必须要有一个VS(Vertex Shader,顶点着色器)和一个FS(Fragment Shader,片段着色器,也叫片元着色器),所以在我们绘制一个三角形时,我们需要编写最简单的VS和FS。
绘制三角形之前,我们需要有三角形的三个点的坐标
float vertices[] = {
// x,y,z
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
渲染管线可以简单的分为三大阶段:
- 应用程序阶段 : 主要负责几何、光栅化阶段需要的渲染数据,顶点、索引、着色器、还有一些绘制状态配置。
- 几何阶段 : 主要对应用程序阶段输入的顶点变换处理。
- 光栅化阶段 : 主要对几何阶段处理后的点作插值生成片段后的处理。
其中几何阶段中,比较重要的是,顶点的变换过程。
顶点变换处理我们输入的都是3D坐标,而最终显示的是在2D的平面屏幕上的。
所以需要将:3D坐标变换为2D屏幕坐标:OpenGL Transformation 几何变换的顺序概要(MVP,NDC,Window坐标变换过程)
变换顺序:Object Space -> World Space -> View Space -> Projection Space -> NDC Space -> Screen Space
其中 Projection Space -> NDC Space -> Screen Space 底层硬件会处理。
了解的基本的变换过程后,为了简单起见,我们尝试上面三个坐标点都当做NDC坐标点来使用,后续在对 Object Space -> World Space -> View Space 做一些介绍。
上面三个坐标点的z值为0,在OpenGL的NDC中,x,y,z在[-1~1]之间都是可视的NDC范围内的坐标,超出NDC Space的坐标点都当做是看不见的。 (Drirect的x,y都是[-1~1],但是z是[0~1]之间的,这点与OpenGL的NDC是不一样的)
所以上面三个点是可以看到的。
基本上在NDC下这三点组成的三角形如下:
使用顶点缓存对象,需要使用 glGenBuffers 来创建缓存对象。如下:
GLuint vertex_buffer;
glGenBuffers(1, &vertex_buffer);
生成缓存的vertex_buffer
就相当于在显卡的内存(显存)中生成了一个空指针。这也 vertex_buffer 也就是我们的之前说的 VBO。
这是 vertex_buffer
都是默认的数据,直到调用:glBindBuffer 后才是我们自己想要的数据,而绑定的缓存类型要设置好,这里我们绑定到:GL_ARRAY_BUFFER
的类型缓存。
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
绑定后,后续对GL_ARRAY_BUFFER 类型的缓存对象都会对 vertex_buffer
数据操作。
下面可以是用 glBufferData 来对上面我们绑定的 GL_ARRAY_BUFFER
的缓存对象设置(将 CPU 内存 数据复制到 GPU 显存)数据,即:对GPU显存中的 vertex_buffer
指针设置数据。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData 的 uage 参数我们使用的是 GL_STATIC_DRAW
,因为缓存数据几乎不改变,这样性能大大提升。
现在有顶点数据了,也创建了顶点缓存对象了,也设置了顶顶啊缓存对象的数据。
那么就可以开始配置好 至少需要的两个着色器:顶点着色器 和 片段着色器 就可以开始绘制了。
着色器想要详细的了解 GLSL 4.5 版本的 可以查看 OpenGL Shading Language Specification 4.5
OpenGL 管线中的各个着色器的指定的阶段、位置。
在 OpenGL 着色器的使用可以简单的分为几个步骤:
- 取得编写的 GLSL 着色器脚本
- 创建着色器程序
- 创建着色器子程序,设置好子程序类型,设置好对应脚本
- 编译着色器子程序
- 将着色器附加到程序
- 将程序链接
- 设置当前管线使用的程序
着色器语言,GLSL 也就是OpenGL Shading Language,是一门 CLike 语言(与C语言很类似的)。
GLSL是在OpenGL 2.0 发布的(也就说之前的版本是没有的,只能用固定管线)。
OpenGL 兼容模式(compatibility profile)还是可以使用固定功能的管线(fixed-function pipeline)的,在OpenGL 核心模式(core profile)是没有固定功能管线的。 这里使用的是核心模式的。 其实后面我改用 compatibility profile 了,因为使用的API比较久的话,对应 core profile 下的话,会有过时API导致无法正常绘制。因为我参考的学习资料是3.3的API资料。
顶点着色器下面我们列出一个最简单的顶点着色器:
#version 450 core
layout (location = 0) in vec3 vPos;
void main() {
gl_Position = vec4(vPos, 1.0);
}
第一行中的 #version 450 core
,就是指定使用的版本,指定这个Shader 是运行在 450 版本的 GLSL,这个 450 其实与我们的 OpenGL 4.5 是对应的,450 == 4.5 * 100
。
core
对应我们之前说的OpenGL 核心模式。(后面我改用 compatibility profile 了)
但一般我们会按照这个着色器使用的API特性来决定它的 version 的,想我们上面这个这么简单的着色器,使用的都是非常基础的特性,可以用 110 版本就可以了。这样它就可以兼容在比较低版本的 OpenGL 或是GLSL比较低的版本中运行。但这里图方便,我直接写了个 450 版本的。
第二行 layout (location = 0) in vec3 pos;
,分几个部分来讲解吧。
- layout(location = 0) : 是布局限定符(layout qualifier),location是指定这类shader变量类型属性所处的索引值。它可以用于
uniform
、attribute
。布局限定符不是必须的,对于attribute
的话,如果布局限定符不手动指定location索引值的话,GLSL 编译时会自动分配对应的索引值,可以使用对应的 API来获取。对于uniform
的话,是在着色器程序链接时确定的,后面的正文有讲到。 - in : 表示这时着色器的输入数据,在这个顶点着色器中会将应用程序设置的数据复制到该变量上,也可以查看下面的GLSL 的类型修饰符。
- vec3 : 包含3个分量的浮点型向量类型,可以通过(x,y,z或是r,g,b来访问,更详细的分量访问可以查看下表GLSL 向量访问分量符,也可以去搜索了解:swizzle)
- vPos: 这是变量的名称,这里用"v"开头的用意是指:在顶点着色器中的变量。(后面我们将片元着色器fragment shader时,是以"f"开头作,当然这不是必须的,都是个人喜欢的命名方式,怎么样可以让代码可读性高一些就好。)
GLSL 的类型修饰符
类型修饰符描述const将一个变量定义为只读形式。如果它初始化时用的是一个编译时常量,那么它本身也会成为编译时常量in设置这个变量为着色器阶段的输入变量out设置这个变量为着色器阶段的输出变量uniform设置这个变量为用于应用程序传递给着色器的数据,它对于给定的图元而言是一个常量buffer设置应用程序共享的一块可读写的内存。这块内存也作为着色器中的存储缓存(storage buffer)使用shared设置变量是本地工作组(local work group)中共享的。它只能用于计算着色器中上面对 uniform 特别讲解一下:uniform是对该所有着色器阶段都是共享使用的,它必须定义为全局变量。
GLSL 向量访问分量符
分量访问符符号描述(x,y,y,z,w)与位置相关的分量(r,g,b,a)与颜色相关的分量(s,t,p,q)与纹理坐标相关的分量第三行 void main() {
就是顶点着色器的入口函数,不需要参数,也不需要返回值。
第四行 gl_Position = vec4(vPos, 1.0);
中 gl_Position
是顶点着色器的内置输出变量,这个变量的值会传递到下一个阶段的着色器作为输入数据使用。这里我们将应用程序阶段设置的ndc space下的顶点数据直接作为gl_Position
来输出,只不过,输出时,我们不全了 vec4
的分量,最后一个分量为1(作为齐次坐标)。可以这句代码也可以写成:gl_Position = vec4(vPos.x, vPos.y, vPos.z, 1.0);
,这是swizzle语法的特点,就这一句的话,可以还很多种写法。
然后我们将这段 顶点着色器 的源代码设置到我们的一个变量中。
你也可以放在一个文件中,如:xxx.vert
文件中,但这里为了方便DEMO内聚的可读性,我就先放到一个变量中。
如下:
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";
可以看到这段 片元着色器 的脚本比 顶点着色器 的还要简短。
第一行 和顶点着色器一样,都是脚本使用的GLSL 版本450,这点基本GLSL中所有着色器都是需要的。 第二行 片元着色器的main函数,也基本是着色器需要的。 第三行 对GLSL内置输出变量为红色值:gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
,这里的vec4(1.0, 0.0, 0.0, 1.0); 的 vec4 类型向量的分量可以解读为RGBA,R=1.0,G和B都是=0.0,A是1.0,所以可以理解为就是一个不透明的红色。
顶点着色器 和 片元着色器 的脚本都有了,那么就可以应用程序代码中使用它们,还记得之前将的使用步骤吧:(那么下面再列一次)
- 取得编写的 GLSL 着色器脚本
- 创建着色器程序
- 创建着色器子程序,设置好子程序类型,设置好对应脚本
- 编译着色器子程序
- 将着色器附加到程序
- 将程序链接
- 设置当前管线使用的程序
再之前的两个 shader 脚本 复制过来,如下:
顶点着色器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";
创建着色器程序
用到 glCreateShader API
unsigned int vertex_shader;
vertex_shader = glCreateShader(GL_VERTEX_SHADER);
这样就创建了一个 顶点着色器 对象,ID为:vertex_shader
。
有了ID,需要给这个着色器指定源码
创建着色器子程序,设置好子程序类型,设置好对应脚本用到 glShaderSource API
glShaderSource(vertex_shader, 1, &vertexShaderSource, NULL);
编译着色器子程序
用到 glCompileShader API
glCompileShader(vertex_shader);
编译失败诊断日志
也许编译不一定会成功,这时想要查看是什么原因导致编译失败,可以通过 glGetShaderiv,glGetShaderInfoLog 组合使用来查看编译日志。
// 获取编译状态值储存到:success 变量
GLint success, infoLogLen;
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
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?