一、图形渲染管线
在OpenGL中,所有事物都在3D空间中,而我们的屏幕是2D的,这导致OpenGL大部分工作都是在进行3D转2D。而这个过程是由OpenGL的图形渲染管线(Graphic Pipeline)管理的。图形渲染管线分以下几个阶段:
顶点数据:3D空间中顶点的坐标值
顶点着色器:实现顶点坐标系转换
形状(图元)装配:将顶点组装成指定图元形状
几何着色器:可添加新的顶点构造新的图元
光栅化:生成片段(指渲染一个像素所需所有数据)
片段着色器:计算顶点的输出颜色
测试与混合:进行深度,模板,混合等测试
二、着色器语言
为图形计算量身定制的语言 ( OpenGL Shading Language,GLSL),它和C语言很像,将在第四节详细讲解。下面是着色器的使用流程。
…
先了解一些概念
顶点着色器:描述如何绘制顶点的文件
片段着色器:描述如何绘制顶点颜色的文件
PS:通常我们将着色器写在独立的文件里,然后在主程序里动态加载和编译。
//版本号说明
#version 330 core
//layout (location = 0):是标记position位置
//in关键字:输入变量
//vec3: 3分量向量
layout (location = 0) in vec3 position;
//程序入口
void main()
{
//gl_Position: 预定义变量(不可修改),输出顶点
//vec4: 4分量向量(第4个分量w不是用来表示空间的,咱玩的是3D不是4D)
//w分量也叫做齐次坐标,简单来说是用来位移用的[矩阵运算]
gl_Position = vec4(position.x, position.y, position.z, 1.0);
}
//版本号说明
#version 330 core
//out关键字:输出变量
out vec4 color;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
现在我们写了最简单的顶点着色器和片段着色器,那么我们将如何使用它们呢。
---------------------- 创建着色器 -----------------
//创建一个顶点着色器
GLuint vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//加载和编译顶点着色器
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
//创建一个片段着色器
GLuint fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//加载和编译片段着色器
glShaderSource(fragmentShader, 1, &fragmentShaderSource, null);
glCompileShader(fragmentShader);
----------------- glShaderSource解释 -----------------
//第一参数:着色器
//第二参数:指定了传递的源码字符串数量,这里只有一个。
//第三参数:是顶点着色器真正的源码(指上边写的GLSL)(char *类型)
//第四参数:我们先设置为NULL。
--------------------- 获取编译信息 --------------------
GLint success;
GLchar infoLog[512];
//检测着色器编译是否成功
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success)
{
//获取错误信息
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
//do someting...
}
------------------------ 链接过程 --------------------
//创建着色器程序
GLuint shaderProgram;
shaderProgram = glCreateProgram();
//链接上边写的着色器
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
--------------------- 获取链接信息 --------------------
GLint success;
GLchar infoLog[512];
//检测着色器链接是否成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success)
{
//获取错误信息
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
//do someting...
}
//激活着色器程序
glUseProgram(shaderProgram);
//删除着色器
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
来,我们这里复习一下思路
- 创建着色器源码
- 加载,编译和链接着色器源码
- 激活着色器程序和内存释放(删除着色器)
三、数据输入
我们已经在上边做好渲染工作,是时候输入我们要画的东西了。
了解一些概念先
顶点缓冲对象(Vertex Buffer Objects, VBO):管理顶点数据的对象,在GPU中保存大量顶点
顶点数组对象(Vertex Array Object, VAO):保存VBO的配置过程(可多个)
索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO):用于减少顶点数据量
-------------------- 准备数据 -----------------
//顶点坐标数据 这里指的是物体坐标 值限制在[-1,1]
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
//创建顶点缓冲对象
GLuint VBO;
glGenBuffers(1, &VBO);
//绑定缓冲对象
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//复制数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//解析数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
//使能顶点属性 参数和glVertexAttribPointer第一个参数相同
glEnableVertexAttribArray(0);
----------------- glBufferData解释 -----------------
//第一参数: 缓冲对象类型
//第二参数: 数据大小
//第三参数: 数据
//第四参数: 管理数据的方式
// GL_STATIC_DRAW :数据不会或几乎不会改变。
// GL_DYNAMIC_DRAW:数据会被改变很多。
// GL_STREAM_DRAW :数据每次绘制时都会改变。
--------- ---- glVertexAttribPointer解释 -----------------
//第一参数: 要配置的顶点属性。就是在顶点着色器源码中layout(location = 0)的那个值
//第二参数: 顶点属性大小 这里顶点属性是一个vec3 它由3个值组成,所以大小是3
//第三参数: 顶点数据类型
//第四参数: 是否归一化 就是把顶点数据映射到[0,1](有符号是[-1,1])
//第五参数: 步长 两个顶点属性之间的距离
//第六参数: 数据位置的偏移量
VAO的使用非常简单,这里套用上边VBO的代码
问:为啥要用VAO?
答:如果不用VAO,那么VBO的绑定到解绑的过程全部都要写在绘制图形的过程中,这样好麻烦呀。使用VAO可以记录这个过程,那么后面写代码就轻松啦
-------------------- 准备数据 -----------------
//顶点坐标数据 这里指的是物体坐标 值限制在[-1,1]
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
//创建顶点数组对象
GLuint VAO;
glGenVertexArrays(1, &VAO);
//创建顶点缓冲对象
GLuint VBO;
glGenBuffers(1, &VBO);
//绑定顶点数组缓冲
glBindVertexArray(VAO);
//绑定缓冲对象
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//复制数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//解析数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
//使能顶点属性 参数和glVertexAttribPointer第一个参数相同
glEnableVertexAttribArray(0);
//解绑顶点数组对象
glBindVertexArray(0);
-------------------- 绘制过程 -----------------
//使用着色器程序
glUseProgram(shaderProgram);
//绑定VAO
glBindVertexArray(VAO);
//画个三角形
glDrawArrays(GL_TRIANGLES, 0, 3);
//解绑VAO
glBindVertexArray(0);
-------------------- glDrawArrays解释 -----------------
//第一参数:绘制类型
//第二参数:指定顶点数组的起始索引
//第三参数:需要画几个顶点
举个例子:当我们要画得不是一个三角形而是一个矩形时,用两个三角形组成矩形(OpenGL主要处理三角形),那么顶点数据是这样的:
GLfloat vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
这显然有点不对劲,一个矩形只需要4个顶点,然而却用了6个顶点,显然多了50%的开销。那么我们使用另一种方法来表示顶点,像这样子
GLfloat vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
GLuint indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
这样看起来顶点数据就没有多余的开销了,那么我们怎样使用这样的顶点数据呢?
//创建索引缓冲对象
GLuint EBO;
glGenBuffers(1, &EBO);
//绑定顶点数组对象
glBindVertexArray(VAO);
//绑定顶点缓冲对象 和 复制数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//绑定索引缓冲对象 和 复制数据
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//解析数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
//使能
glEnableVertexAttribArray(0);
//解绑顶点数组对象
glBindVertexArray(0);
那么新的绘制图形会变成这样子
-------------------- 绘制过程 -----------------
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
//绘制矩形
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
-------------------- glDrawElements解释 -----------------
//第一参数:绘制类型
//第二参数:需要画几个顶点
//第三参数:索引类型
//第死参数:指定EBO的偏移量或这新的索引数组
//线模式
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//默认模式
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
到这里就是简单着色器使用的所有内容啦~
四、GLSL详解
#version version_number
//顶点着色器的输入变量(也叫顶点属性),我们能声明的in 是有限的(16个),由硬件决定,有些会更多
//在主程序中,我们可以这样获取它
// GLint nrAttributes;
// glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
// std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
int main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
类型 | 含义 |
---|---|
int float double uint bool | 基本类型 |
vecn | 包含n个float分量的默认向量 |
bvecn | 包含n个bool分量的向量 |
ivecn | 包含n个int分量的向量 |
uvecn | 包含n个unsigned int分量的向量 |
dvecn | 包含n个double分量的向量 |
Matrix | 矩阵 |
通常使用vec就够用了,我们可以这样子玩向量。
//xyzw 分别是向量的1,2,3,4分量
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
vec2 vect = vec2(0.5f, 0.7f);
vec4 result = vec4(vect, 0.0f, 0.0f);
vec4 otherResult = vec4(result.xyz, 1.0f);
#version 330 core
out vec4 color;
//这是个全局变量
uniform vec4 ourColor;
void main()
{
color = ourColor;
}
在程序中我们该如何给它赋值呢?
//根据当前时间来设置颜色的G通道
GLfloat timeValue = glfwGetTime();
GLfloat greenValue = (sin(timeValue) / 2) + 0.5;
//查询 ourColor 变量
GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
//选择着色器程序
glUseProgram(shaderProgram);
//赋值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
OpenGL核心是个C库,不支持重载,所以像glUniform 函数通过加不同后缀来实现不同输入参数
后缀 | 含义 |
---|---|
f | 函数需要一个float作为它的值 |
i | 函数需要一个int作为它的值 |
ui | 函数需要一个unsigned int作为它的值 |
3f | 函数需要3个float作为它的值 |
fv | 函数需要一个float向量/数组作为它的值 |
这也是着色器间数据传输的例子【篇幅很长,只列出关键部分】,首先顶点着色器和片段着色器是这样子的
//顶点着色器
#version 330 core
// 位置变量的属性位置值为 0
layout (location = 0) in vec3 position;
// 颜色变量的属性位置值为 1
layout (location = 1) in vec3 color;
// 向片段着色器输出一个颜色
out vec3 ourColor;
void main()
{
gl_Position = vec4(position, 1.0);
ourColor = color; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
//片段着色器
#version 330 core
in vec3 ourColor;
out vec4 color;
void main()
{
color = vec4(ourColor, 1.0f);
}
然后主程序是这样子
//顶点数据
GLfloat vertices[] = {
// 位置 // 颜色
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 // 顶部
};
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat)));
glEnableVertexAttribArray(1);
-------------------- glVertexAttribPointer解释 -----------------
//前面几个参数已经很熟悉了,说一下最后一个参数
//由顶点数据可以看出,颜色数据在第4位,那么就应该跳过3位来表明颜色数据的起始位置。
从前边可以看出编写,编译,管理着色器是真他喵麻烦,所以让我们来把他封装成类吧。
https://github.com/AutoCatFuuuu/OpenGL
[ShaderHelper.cpp] & [ShaderHelper.h]
五、写在最后
本文章只是写给自己看的,看着别人的文章,根据自己的理解,以使用理解代码为主,做下记录,仅此而已。想要教程的话
https://learnopengl-cn.github.io
https://learnopengl-cn.readthedocs.io/zh/latest/
欢迎相互学习讨论:QQ:673315140