你好——三角形

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34536551/article/details/83018684

● 图形渲染管线((图形输送管道) ) 可以被划分为两个主要部分: 第一部 分把你的3D坐标转换为2D坐标, 第二部分是: 把2D坐标转变为实际的有颜色的 像素。

● 注意: 2D坐标和2D像素也是不同的, 2D坐标精确表示一个点在2D空间中 的位置 而2D像素是这个点的近似值, 2D像素受到你的屏幕/窗口分辨率的 限制。

● 图形渲染管线接受一组3D坐标, 然后把它们转变为你屏幕上的有色2D像素输出。 图形渲染管线可以被划分为几个阶段, 每个阶段将会把前一个阶段的输出作为输入。

● 着色器程序:是在GPU上为每一个(渲染管线)阶段运行各自的小程序, 从而在图形渲染管线中快速处理你的数据。

OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的。

● 图形渲染管线包含很多部分, 每个部分都将在转换顶点数据到最终像素这一过程中处理各自特定的阶段。

● 首先, 我们以数组的形式传递3个3D坐标作为图形渲染管线的输入, 用来表 
示一个三角形, 这个数组叫做顶点数据(Vertex Data);

顶点数据是一系列顶点的集合。 一个顶点(Vertex)是一个3D坐标的数据的集合。 而顶点数据是用顶点属性(Vertex Attribute)表示的, 它可以包含任何我们想用的数据。

● 我们需要给OpenGL 提示, 表示希望这些顶点数据表示的是什么渲染类型, 这些提示叫做图元(基本图形),任何一个绘制指令的调用都将把图元传递给OpenGL。 这是其中的几个:GL_POINTS、 GL_TRIANGLES、 GL_LINE_STRIP。

● 在现代OpenGL中, 我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器) 。


●  图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把
3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。


● 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),
并所有的点装配成指定图元的形状。

● 几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。

● 几何着色器的输出会被传入光栅化阶段这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段。在片段着色器运行之前会执行裁切。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

片段着色器的主要目的是计算一个像素的最终颜色,通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
 

●  在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

●  注意:对于大多数场合,我们只需要配置顶点和片段着色器就行了(因为GPU中没有默认的顶点/片段着色器)。几何着色器是可选的,通常使用它默认的着色器就行了。
 


顶点输入


●   注意: OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴(x、 y和z) 上都为-1.0到1.0的范围内时才处理3D坐标。 所有在这个范围内的坐标叫做标准化设备坐标。 会最终显示在你的屏幕上(所有出了这个范围的都不会显示) 。

注意:通常深度可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节省资源。
 

●注意: 一旦你的顶点坐标已经在顶点着色器中处理过, 它们就应该是标准化设备坐标了, 标准化设备坐标是一个x、 y和z值在-1.0到1.0的一小段空间。 任何落在范围外的坐标都会被丢弃/裁剪, 不会显示在你的屏幕上。

●注意: 你的标准化设备坐标接着会变换为屏幕空间坐标,这是使用你通过glViewport函数提供的数据, 进行视口变换完成的。 所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。

● 定义这样的顶点数据以后, 我们会把它作为输入发送给图形渲染管线的第一个处理阶段: 顶点着色器。 它会在GPU上创建内存用于储存我们的顶点数据, 还要配置OpenGL如何解释这些内存, 并且指定其如何发送给显卡。 顶点着色器接着会处理我们在内存中指定数量的顶点。

我们通过顶点缓冲对象管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。


● 我们可以使用 glGenBuffers 函数和一个缓冲ID生成一个VBO对象:

unsigned int VBO;
glGenBuffers(1, &VBO);

顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上:
 

glBindBuffer(GL_ARRAY_BUFFER, VBO);

● 从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBufferData 是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:

       GL_STATIC_DRAW :数据不会或几乎不会改变。

       GL_DYNAMIC_DRAW:数据会被改变很多。

      GL_STREAM_DRAW :数据每次绘制时都会改变。
 

注意: 每次渲染调用时都保持原样,就是说一个缓冲中的数据不变,那么使用类型最好是GL_STATIC_DRAW。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。
 

现在我们已经把顶点数据储存在显卡的内存中,用VBO这个顶点缓冲对象管理。


顶点着色器


一个非常基础的GLSL顶点着色器的源代码:

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{

gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);

}


●  每个着色器都起始于一个版本声明。OpenGL 3.3以及和更高版本中,GLSL版本号和OpenGL的版本是匹配的,第一行表示GLSL的版本号是3.3, 然后我们会使用核心模式。
 

●  使用in关键字,在顶点着色器中声明所有的输入顶点属性

●  由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量aPos。我们同样也通过layout (location = 0)设定了输入变量的位置值。
 

●  GLSL有一个向量数据类型,它包含1到4个float分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w获取。注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。
 

●  为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数的最后,我们将gl_Position设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f。

● 当前这个顶点着色器可能是我们能想到的最简单的顶点着色器了,因为我们对输入数据什么都没有处理就把它传到着色器的输出了。在真实的程序里输入数据通常都不是标准化设备坐标,所以我们首先必须先把它们转换至OpenGL的可视区域内。


编译着色器


● 我们已经写了一个顶点着色器源码(储存在一个C的字符串中),但是为了能够让OpenGL使用它,我们必须在运行时动态编译它的源码。


● 我们首先要做的是创建一个着色器对象,注意还是用ID来引用的。所以我们储存这个顶点着色器为 unsigned int,然后用glCreateShader创建这个着色器:

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);


我们把需要创建的着色器类型以参数形式提供给glCreateShader。由于我们正在创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER。


● 下一步我们把这个着色器源码附加到着色器对象上,然后编译它:

 

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

glShaderSource函数把要编译的着色器对象作为第一个参数。第二个参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
 

你可能会希望检测在调用glCompileShader后编译是否成功了,如果没成功的话,你还会希望知道错误是什么,这样你才能修复它们。检测编译时错误可以通过以下代码来实现:

int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);


首先我们定义一个整型变量来表示是否成功编译,还定义了一个储存错误消息(如果有的话)的容器。然后我们用glGetShaderiv检查是否编译成功。如果编译失败,我们会用glGetShaderInfoLog获取错误消息,然后打印它。
 

if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
} 

如果编译的时候没有检测到任何错误,顶点着色器就被编译成功了。
 


片段着色器


● 片段着色器所做的是计算像素最后的颜色输出。
 

在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。这三种颜色分量的不同调配可以生成超过1600万种不同的颜色。

#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用out关键字声明输出变量,这里我们命名为FragColor。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。
 

●  我们使用GL_FRAGMENT_SHADER常量作为着色器类型:

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

两个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中。
 

猜你喜欢

转载自blog.csdn.net/qq_34536551/article/details/83018684