欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

1、OpenGL | 坐标如何转化为有颜色的像素

程序员文章站 2022-07-14 23:25:37
...

OpenGL是一个图形接口函数库,他是用来跟GPU驱动打交道的。

1、OpenGL | 坐标如何转化为有颜色的像素
在OpenGL中,所有的图形都是3D的,所有的2D图形都是从3D转过来的,转化的方法是通过一个叫图形管道的东西,你输入一个3D坐标,就立马给你转换成2D 像素。

转换过程分两部:
1、3D坐标转2D
2、2D坐标转成有颜色的像素

这个转换并渲染的过程,要用到一个叫shaders(着色器)的东西,实际上他不是个东西,只是个小程序,你用GL shading language写成的,给GPU去执行的程序罢了。
有些shaders是我们可以自主编写的,如下图蓝色部分:
1、OpenGL | 坐标如何转化为有颜色的像素
如果再细分的话:
有很多步,比如
第一个,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,坐标如下:
1、OpenGL | 坐标如何转化为有颜色的像素
最终,你的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 缓冲区中的数据是这样排列的:
1、OpenGL | 坐标如何转化为有颜色的像素
下面就用函数告诉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
*/