1、OpenGL | 坐标如何转化为有颜色的像素
OpenGL是一个图形接口函数库,他是用来跟GPU驱动打交道的。
在OpenGL中,所有的图形都是3D的,所有的2D图形都是从3D转过来的,转化的方法是通过一个叫图形管道的东西,你输入一个3D坐标,就立马给你转换成2D 像素。
转换过程分两部:
1、3D坐标转2D
2、2D坐标转成有颜色的像素
这个转换并渲染的过程,要用到一个叫shaders(着色器)的东西,实际上他不是个东西,只是个小程序,你用GL shading language写成的,给GPU去执行的程序罢了。
有些shaders是我们可以自主编写的,如下图蓝色部分:
如果再细分的话:
有很多步,比如
第一个,Vertex shader,接收一个叫Vertex Data的数组,来存储3D坐标、颜色、渲染方式等,主要工作是把3D坐标 转为 “特别” 的3D坐标,并对vertex值做个 “基础” 改变。如上图,原语装配阶段将顶点着色器中构成原语 的所有顶点 (如果GL_POINTS被选择,则为顶点) 作为输入,并将给定的原语形状中的 所有点组装起来;在这个例子中是一个三角形。
第二个,Shape shader,我们无法操控,直接送到几何 shader
第三个,几何 Shader,接受一个顶点集合作为输入,它形成一个原语,并且有能力通过发射新的顶点来形成新的(或其他)原语来生成其他形状。在本例中,它从给定的形状生成第二个三角形。
第四个,镶嵌 shader,把给定的原语细分成许多更小的原语。例如,这允许你创建更流畅的环境,通过创建更多的三角形,与player的距离越小。
第五个,光栅化 shader,无法参与。镶嵌着色器的输出被传递到光栅化阶段,将产生的原语 映射到最终屏幕上对应的像素,从而产生片段着色器使用的片段。在片段着色器运行之前,剪切被执行。剪切会丢弃视图之外的任何片段,从而提高性能
其中,片段 (fragment)是OpenGL渲染单个像素所需的所有数据
第六个,片段 shader,主要目的是计算像素的最终颜色,这通常是所有高级OpenGL效果出现的阶段。通常片段着色器包含有关3D场景的数据,它可以用来计算最终的像素颜色(像光,阴影,光的颜色等等)。
第七个, alpha测试和混合,无法控制。最终的对象会通过alpha测试和混合阶段的阶段。这个阶段检查片段的相应的depth(和stencil)值(稍后会讲到),并用这些值来检查产生的片段是在其他对象的前面还是后面,是否该被丢弃。还会检查alpha值(alpha值定义了一个对象的透明度)并相应地混合这些对象。因此,即使一个像素输出的颜色是在fragment shader中计算出来的,当渲染多个三角形时,最终的像素颜色仍然可能是完全不同的。
在实际应用中,由于GPU上没有vertex 和 fragment 的shader,我们自定义vertex shader 和 fragment shader,其他选择默认就行了。
例子:
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
存储了三角形的三个顶点的3D坐标,把每个vertex的坐标称为标准化的设备坐标,NDC,坐标如下:
最终,你的NDC坐标会转化为屏幕坐标。使用glViewport提供的数据,通过viewport转换,您的NDC坐标将被转换为屏幕空间坐标,然后转换为片段 作为你的片段着色器的输入。
具体的vertex shader 编写思路,把定义好的vertex数组,送到图形管道第一阶段——vertex shader中,原理是,在GPU上开辟内存存储vertex 数据,配置OpenGL如何去操作这个内存、如何把数据送到显卡。
于是,这个vertex shader 就把我们写到内存的vertex们 逐个处理。
在编程上,我们通过vertex buffer objects(vertex缓冲区对象)管理GPU内存,好处是一次送一堆,因为从CPU把数据送到显卡是相对慢的,你只要把数据放在GPU内存中,shader就执行很快(不用等)。
创建vertex缓冲区代码:
GLuint VBO;
glGenBuffers(1, &VBO);
/*
1是这个缓冲区对象个数,&VBO用来存储缓冲对象名称的数组
*/
OpenGL有很多类型的缓冲区对象,比如,vertex缓冲区对象的缓冲区类型是GL_ARRAY_BUFFER,
glBindBuffer(GL_ARRAY_BUFFER, VBO);OpenGL允许我们一次绑定到多个缓冲区,只要它们有不同的缓冲区类型。此时,绑定到一个目标上了,我们(在GL_ARRAY_BUFFER目标上)进行的任何缓冲区调用都将用于配置当前绑定的缓冲区,即VBO。
然后我们可以调用glBufferData函数,将之前定义的顶点数据复制到缓冲区的内存中:
专门用于将用户定义的数据复制到当前绑定的缓冲区中的函数
glBufferData(GL_ARRAY_BUFFER, //我们数据进入的缓冲区类型
sizeof(vertices),//字节计算
vertices, //自己的数据
GL_STATIC_DRAW//告诉显卡我们想怎样管理数据
/*
GL_STATIC_DRAW: 数据不会改变
• GL_DYNAMIC_DRAW: 数据会改变很多
• GL_STREAM_DRAW: 数据在每次draw 完成时,都会改变
本次测试用例是一个三角型,
三角形数据在每次渲染调用时都不改变,
因此选用图上参数。
*/
);
整理一下,现在把vertex 的数据存到了显卡上,数据由vertex 缓冲区对象VBO 托管,接下来再是创建vertex 和 fragment 的shader了。
现在的OpenGL 非要你写个vertex 和 fragment shader ,才让你去渲染。
OK,接下来用GLSL语言写shaders:
一共分为3部分:
1、指定OpenGL版本,330=3.3
2、读取输入的vertex
3、输出处理的vertex(有个预定义的gl_Position ,vec4,在Main函数的最后段,会自动把vertex shader 输出的数据存到vec4)
总的来说,输入ver3(3个参数),输出vec4(4个参数)
#version 330 core
//用于vertex数据的输入,in是关键字,声明要输入vertex属性了
//属性是position ,位置,
//创建了一个vec3来存坐标
layout (location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
//最后一个参数叫透视分割position.w。
}
- [ a] 注意:在真实的应用程序中,输入数据通常还没有在规范化的设备坐标中,所以我们首先必须将输入数据转换为OpenGL可见区域中的坐标
使用shader:
为了让OpenGL去使用这个shader,必须把他加到程序源码的动态运行库里面,于是就有一下函数提供接口,
GLuint vertexShader;//shader 对象
vertexShader = glCreateShader(GL_VERTEX_SHADER);
接下来,将着色器的源代码附加到着色器对象并编译着色器
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//第二个参数是多少字符串,第三个是shader源码
glCompileShader(vertexShader);
/*
检查编译是否成功
GLint success;
GLchar infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" <<
infoLog << std::endl;
}
*/
Fragment Shader:
用来计算像素输出颜色。
计算机图形上的颜色用4个参数代表,Red、 Green、 Blue、Alpha(opacity)component(阿尔法_不透明度组件);
OpenGL和GLSL中,RGBA值都是在0.0~0.1f
#version 330 core
//定义输出值
out vec4 color;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
编译fragment shader
GLuint fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
2个shader 都完成编译后,现在就是要把他们两个的着色器对象链接起来,到一个shader program中,用来渲染。
制作Shader program:
同理,又需要shader program对象,他是所有shader的结合体。
把shader链接到shader program中,然后 在渲染对象时 **shader program 。搞笑的是,这个程序也是用shader**的。
而且,是在每次发出渲染调用时,就使用这个**shader。
Luint shaderProgram;
shaderProgram = glCreateProgram();
这个shader程序会输出结果到下一个shader,如果输出的结果与下个shader 不匹配,报错。
然后,把编译好的shaders们整合到 对象中,再链接他们:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
/*
如果链接不成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}
*/
链接之后,我们就用glUseProgram(),**program 对象
glUseProgram(shaderProgram);
在glUseProgram之后的每一次着色器与渲染调用,会使用这个程序对象(因此,着色器)。
- 不要忘记删除资源:
- glDeleteShader(vertexShader);
- glDeleteShader(fragmentShader);
到这里为止,我们输入vertex数据到了GPU,写了shader告诉GPU如何处理vertex数据,但是,我们并没有告诉OpenGL如何interpret内存中的vertex数据,如何把vertex数据与 vertex shader的属性连接起来。
现在就告诉OpenGL,必须手动指定 输入数据的哪一部分要放到 顶点着色器 的哪个顶点属性中。这意味着我们必须在渲染之前指定OpenGL应该如何解释顶点数据
vertex 缓冲区中的数据是这样排列的:
下面就用函数告诉OpenGL,如何用glVertexAttribPointer(),解释vertex 数据(或属性):
glVertexAttribPointer(0,
3,//指定顶点属性的大小。顶点属性是一个vec3,所以它由3个值组成
GL_FLOAT, //指定数据类型为GL_FLOAT
GL_FALSE, //是否数据被规范化。如果设置为GL_TRUE,那么值不在0~1之间(对于有符号的数据,则为-1~1)的所有数据都将映射到这些值。我们将其保留在GL_FALSE。
3 * sizeof(GLfloat),//stride,步幅__它告诉我们连续的顶点属性集之间的空间
(GLvoid*)0);//位置数据在缓冲区中开始位置的偏移量
glEnableVertexAttribArray(0);
/*
第一个参数指定我们要配置的顶点属性,
布局的顶点着色器中指定了位置顶点属性的位置(location = 0),
这将顶点属性的位置设置为0,
因为我们想要传递数据给这个顶点属性,所以我们传递了0
*/