OpenGL入门之渲染管线pipeline,着色器Shader
Posted 木大白易
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenGL入门之渲染管线pipeline,着色器Shader相关的知识,希望对你有一定的参考价值。
图像渲染管线
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。
它们是由着色器(Shader)程序控制,且运行在GPU上!
一个经典的示图:
- Vertex Data:顶点数据,数组的形式传递3个3D坐标来表示一个三角形,这个数组就是顶点数据。而顶点(Vertex)就是一个3D坐标的数据的集合。每个顶点又包含多个顶点属性(Vertex Attribute),比如position,color,normal(法线)等。
- 顶点着色器(Vertex Shader):把3D坐标转为另一种3D坐标
- 图元装配(Primitive Assembly):将顶点着色器输出的所有顶点作为输入,装配成指定图元的形状。图元就是告诉OpenGL怎么绘制这些点,比如GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
- 几何着色器(Geometry Shader):把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
- 光栅化阶段(Rasterization Stage):会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
- 片段着色器(Fragment Shader):计算一个像素的最终颜色。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
- Alpha测试和混合(Blending)阶段:检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。
顶点输入
开始绘制图形之前,我们需要先给OpenGL输入一些顶点数据。OpenGL仅当3D坐标在3个轴(x、y和z)上 -1.0到1.0 的范围内时才处理它。所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates),此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。
绘制一个三角形:
float vertices[] = //三个顶点,z轴都设置为0,表示相同的深度depth
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
;
OpenGL的坐标系为右手坐标系,即坐标x轴向右为正,y轴向上为正,z轴屏幕朝外为正,原点为为图像中心!
顶点缓冲对象VBO
定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。通过 顶点缓冲对象(Vertex Buffer Objects, VBO) 管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批cpu的数据到显卡上,而不是每个顶点发送一次。
unsigned int VBO;
glGenBuffers(1, &VBO); //生成一个vbo对象
//顶点数据的缓存类型是GL_ARRAY_BUFFER
glBindBuffer(GL_ARRAY_BUFFER, VBO); //绑定
//将顶点数据复制到缓冲内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
着色器Shader
着色器是由着色器语言GLSL(OpenGL Shading Language)编写的!它非常类似于cpp!
顶点着色器
一个最基本的顶点着色器:
#version 330 core //定义版本 并指定core-profile
layout (location = 0) in vec3 aPos; //指定location为顶点的第0个属性
void main()
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); //w设置默认为1
这里vec4的最后一个参数w,是用于透视除法(Perspective Division)!
为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position
变量,它是vec4类型!
如何编译着色器
const char *vertexShaderSource = "#version 330 core\\n"
"layout (location = 0) in vec3 aPos;\\n"
"void main()\\n"
"\\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\\n"
"\\0";
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);//创建着色器
//第二个参数:传递的源码字符串数量,这里只有一个
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); //设置源码
glCompileShader(vertexShader); //编译
片段着色器
片段着色器所做的是计算像素最后的颜色输出。
一个最基本的片段着色器:
#version 330 core
out vec4 FragColor;
void main()
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); //默认为rgba,取决于帧缓冲
同样的可以像顶点着色器那样编译:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
着色器程序Shader Program
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。
unsigned int shaderProgram;
shaderProgram = glCreateProgram(); //创建program
glAttachShader(shaderProgram, vertexShader); //attach
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); //link
glUseProgram(shaderProgram);//使用
//link完成之后就可以删除了
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
//绘制
glDrawArrays(GL_TRIANGLES, 0, 3);
链接顶点属性
现在我们将顶点数据传到GPU中了,但是OpenGL还不知道怎么使用这些数据,我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
顶点缓冲数据会被解析为下面这样子:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);//启用
这里配合上面的图,重点关注一下glVertexAttribPointer
函数:
- 第一个参数:对应的是要配置顶点的哪一个属性,在shader中写了
layout (location = 0)
,所以这里写0。 - 第二个参数:顶点属性的大小,这里是一个vec3,有3个值组成
- 第三个参数:数据类型,这里是GL_FLOAT
- 第四个参数:是否对数据进行归一化处理,这里已经是标准化后的数据了,不需要
- 第五个参数:步长(Stride),表示连续的顶点属性组之间的间隔,单位byte。
- 第六个参数:表示位置数据在缓冲中起始位置偏移量(Offset)指针。
顶点数组对象VAO
OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。
顶点数组对象(Vertex Array Object, VAO) 可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。
unsigned int VAO;
glGenVertexArrays(1, &VAO); //创建VAO
glBindVertexArray(VAO);//绑定
//绘制代码
这里mac中的OpenGL可能找不到这个函数,所以需要一个三方库来寻找具体的函数指针!
下载GLAD,去官网:https://glad.dav1d.de/
将语言(Language)设置为C/C++,在API选项中,选择3.3以上的OpenGL(gl)版本。之后将模式(Profile)设置为Core,并且保证选中了生成加载器(Generate a loader)选项。现在可以先(暂时)忽略扩展(Extensions)中的内容。都选择完之后,点击生成(Generate)按钮来生成库文件。
将下载好的头文件和glad.c添加到工程中即可!
至此,一个完整的三角形就可以被绘制出来了!
完整的代码如下:
/**********************Shader begin******************/
const char *vertexShaderSource = "#version 330 core\\n"
"layout (location = 0) in vec3 aPos;\\n"
"void main()\\n"
"\\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\\n"
"\\0";
const char *fragmentShaderSource = "#version 330 core\\n"
"out vec4 color;\\n"
"void main()\\n"
"\\n"
" color = vec4(1.0f, 0.5f, 0.2f, 1.0);\\n"
"\\0";
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);//创建着色器
//第二个参数:传递的源码字符串数量,这里只有一个
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); //设置源码
glCompileShader(vertexShader); //编译
int success;
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
int length;
glGetShaderiv(vertexShader, GL_INFO_LOG_LENGTH, &length);
char* infoLog = (char *)malloc(length*sizeof(char *));
glGetShaderInfoLog(vertexShader, sizeof(infoLog), NULL, infoLog);
cout<<"compile vertex failed:"<<infoLog<<endl;
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);//创建着色器
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
unsigned int shaderProgram;
shaderProgram = glCreateProgram(); //创建program
glAttachShader(shaderProgram, vertexShader); //attach
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); //link
//link完成之后就可以删除了
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
/**********************Shader end******************/
float vertices[] = //三个顶点,z轴都设置为0,表示相同的深度depth
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
;
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
unsigned int VBO;
glGenBuffers(1, &VBO); //生成一个vbo对象
//顶点数据的缓存类型是GL_ARRAY_BUFFER
glBindBuffer(GL_ARRAY_BUFFER, VBO); //绑定
//将顶点数据复制到缓冲内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);//启用
//解绑
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
//渲染
while (!glfwWindowShouldClose(window))
...
glUseProgram(shaderProgram);//使用
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);//绘制三角形
...
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
...
元素缓冲对象EBO
元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)!
举个例子,如果我们要绘制一个矩形,它由两个三角形组成,按照前边的例子,顶点数据需要6个,而实际矩形只需要4个顶点即可!EBO就可以解决这个问题!
float 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 // 左上角
;
unsigned int indices[] =
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
;
unsigned int EBO;
glGenBuffers(1, &EBO);//创建EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);//绑定
//将索引数据复制到缓冲内存中
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
最终绘制的时候,需要做一些修改:
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
- 第一个参数:指定绘制模式
- 第二个参数:绘制顶点的个数,这里是6个
- 第三个参数:索引的类型
- 第四个参数:当使用EBO的时候,传入数据的offset,这里为0
推荐阅读
https://www.cnblogs.com/xiangqi/p/14608073.html
以上是关于OpenGL入门之渲染管线pipeline,着色器Shader的主要内容,如果未能解决你的问题,请参考以下文章