顶点数据:
顶点数据是一系列顶点的集合。
一个顶点(Vertex)是一个3d坐标的数据的集合。
而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据。
(但是简单起见,我们还是假定每个顶点只有一个3D位置和一些颜色值组成的。)
图元:
为了让openGL知道我们的坐标和颜色值构成的到底是什么,openGL需要你去指定这些数据所表示的渲染类型。
我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出这些提示的叫做图元(Primitive)
任何一个绘制指令的调用都将把图元传递给openGL。这是其中的几个:
GL_POINTS 、GL_TRIANGLES、 GL_LINE_STRIP。
图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。
顶点着色器主要的目的是把3D坐标转换为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。
图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),
并所有的点装配成指定图元的形状;
图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,
它可以通过产生新顶点构造出新的(或是其他的)图元来生成其他形状。
几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,
生成供片段着色器(Fragment Shader)使用的片段(Fragment) 。在片段着色器运行之前会执行裁切(Clipping),
裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
OpenGL 中的一个片段是OpenGL渲染一个像素所需的所有数据。
片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。
通常片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。
这个阶段检测片段的对应的深度(和模版(Stencil))值,用它们来判断这个像素是其他物体的前面还是后面,
决定是否应该丢弃。这个阶段也会检查alpha值 并对物体进行混合(Blend)。
然而,对于大多数场合,我们只需要配置定点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。
渲染一个2D的三角形,我们将它顶点的z坐标设置为0.0,这样子的话三角形每一点的深度(Depth) 都是一样的,从而使它看上去像是2D的。
通常深度可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节省资源。
标准化设备坐标(Normalized Device Coordinates,NDC)
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,
标准化坐标是一个x、y和z值在-1.0到1.0的一小段空间。
标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport 函数提供的数据,进行视口变换(Viewport Transform)完成的。
所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。
定义这样的顶点数据以后,我们会把它输入发送给图形渲染管线的第一个处理阶段:顶点着色器。
它会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。
我们通过顶点缓冲对象(Vertex Buffer Objects,VBO)管理这个内存。
顶点缓冲对象是我们在OpenGL中第一个出现的OpenGL对象。
这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers 函数和一个缓冲ID生成一个VBO对象:
GLuint VBO; glGenBuffers(1, &VBO);
OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是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是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。
1个参数是目标缓冲的类型;
2个参数是指定传输数据大小(以字节为单位);
3个参数是我们希望发送的实际数据;
4个参数指定了我们希望显卡如何管理给定的数据。它有3种形式:
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW:数据每次绘制时都会改变。
向量(Vector)它简明的表达了任意空间中的位置和方向,并且它有非常有用的数学属性。
在GLSL中一个向量有最多4个分量,每个分量都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。
vec.w分量而是用在所谓透视划分(Perspective Division)上。
在真实的程序里输入数据通常都不是标准化设备坐标,顶点着色器一般会先把它们转换至OpenGL的可视区域内。
顶点着色器(VertexShader)
const GLchar* vertexShaderSource = "#version 330 core \ layout(location = 0) in vec3 position; \ void main() \ { \ gl_Position = vec4(position.x,position.y,position.z,1.0); \ } \ ";
编译着色器
编写之后的顶点着色器源码,为了能让OpenGL使用它,我们必须在运行时动态编译它的源码。
首先创建一个着色器对象,注意还是用ID来引用的。所以我们存储这个顶点着色器为GLuint,
然后用glCreateShader 创建这个着色器:
GLuint vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
我们把需要创建的着色器类型以参数形式提供给glCreateShader。
由于我们正创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER。
下一步我们把这个顶点着色器源码附加到着色器对象上,然后编译它:
glShaderSource(vertexShader,1,&vertexShaderSource,NULL); glCompileShader(vertexShader);
glShaderSource函数参数:
1个参数要编译的着色器对象;
2个参数指定传递的源码字符串数量,上面这里只有一个;
3个参数是顶点着色器真正的源码;
4个参数我们先设置为NULL。
如果想检测glCompileShader是否成功可以用glGetShaderiv 检查是否编译成功。如果失败可以用glGetShaderInfoLog获取错误信息。
片段着色器(Fragment Shader)
该着色器全是关于计算你的像素最后的颜色输出。
const GLchar* fragmentShaderSource = "#version 330 core \ out vec4 color; \ void main() \ { \ color = vec4(1.0f,0.5f,0.2f,1.0f); \ } \ ";
在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。
每个分量的强度设置在0.0到1.0之间。
编译片段着色器的过程与顶点着色器类似,只不过我们使用GL_FRAGMENT_SHADER常量作为着色器类型:
GLuint fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader,1,&fragmentShaderSource,null);
glCompileShader(fragmentShader);
两个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中。
------------------------------
https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/