前言
当看到下面一段顶点着色器代码,相信很多写过着色器的小伙伴都觉得非常简单:不就是定义了着色器的布局中的顶点位置吗:顶点(x,y),颜色(r/g/b),偏移量(offset),并把位置信息赋值给内置变量gl_Position
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;
out vec3 fColor;
void main()
{
fColor = aColor;
gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
}
但是您想过没有,为什么简单的几行代码,GPU是如何把顶点缓冲区的数据显示到我们屏幕上的呢?这里我也不赘述openGL渲染管线的流程了,就单单从代码层面剖析一下:
layout (location = 0) in vec2 aPos;
对应的是:顶点数组,表示一个2个彩色的三角形,这2个彩色的三角形组成了一个矩形
float quadVertices[] = {
// positions // colors
-0.05f, 0.05f, 1.0f, 0.0f, 0.0f,
0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
-0.05f, -0.05f, 0.0f, 0.0f, 1.0f,
-0.05f, 0.05f, 1.0f, 0.0f, 0.0f,
0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
0.05f, 0.05f, 0.0f, 1.0f, 1.0f
};
position:
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
color:
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));
offset:这里创建的offset偏移量表示,需要创建多个相同的顶点数组,也就是多个矩形
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO); // this attribute comes from a different vertex buffer
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
这就是上面顶点着色器布局器的实际意义 。如果是100个彩色矩形呢,如果使用绘制函数glDrawArrays,我们就得调用100次glDrawArrays函数。这样的程序几乎没法写,计算机性能估计也要急剧下降。
优化
顶点着色器:
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;
out vec3 fColor;
void main()
{
fColor = aColor;
gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
}
运行效果
概念
一、一些重要的
GL_APICALL void GL_APIENTRY glDrawArrays (GLenum mode, GLint first, GLsizei count)
顶点法绘制:使用顶点数组方式绘制图形。第一个参数是绘制方式,第二个是顶点数组的起点,第三个是从顶点数组读取的个数。
void glDrawElements( GLenum mode, GLsizei count, GLenum type, const GLvoid *indices)
索引法绘制:绘制模式,绘制的顶点数,索引的数据格式,索引的数据buffer
二、Texture相关
void glGenTextures( GLsizei n,GLuint * textures)
n:生成纹理的数量
textures:存储纹理索引的首指针
GL_APICALL void GL_APIENTRY glActiveTexture (GLenum texture)
激活当前活动纹理单元,texture一般为GL_TEXTURE0,GL_TEXTURE1等
void glBindTexture(GLenum target, GLuint texture )
target: GL_TEXTURE_2D
texture:texture的索引
void glAttachShader(GLuint program, GLuint shader);
将一个着色器对象绑定到一个程序对象
GLint glGetUniformLocation(GLuint shaderID, const GLchar* varName);
返回一个有符号的整数,代表在shaderID指定的着色器中由varName命名的变量的位置。在一个着色器进行编译和连接之后,我们必须在着色器中寻找uniform的位置
void glUniform1f(GLint location, GLfloat v0);
设置标量和向量的统一值,1代表1个参数,f代表float,以此类推。
void glUniform1fv(GLint location, GLuint count, GLfloat* v);
接受一个指针,count值代表每个含有x个分量的数组中有多少个元素。使用范例:
GLfloat vColor[4]={1.0f, 1.0f, 1.0f, 1.0f};
glUniform4fv(iColorLocation, 1, vColor);
如果vColor是多维数组的话,count就是多维。
glUniformMatrix2fv(GLint location, GLuint count, GLboolean transpose, const GLfloat *m);
设置统一矩阵
void glReadPixels(GLint x, GLint y, GLSizei width, GLSizei height, GLenum format, GLenum type, const void *pixels);
x,y值指定为矩形左下角的窗口坐标
void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLunum format, GLenum type, void* data);
载入纹理
void glTexSubImage2D(GLenum target, GLint level, GLint xOffset, GLint yOffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *data);
在时间敏感的场合比如游戏或模拟应用中,重复加载新纹理可能会成为性能瓶颈。如果我们不再需要某个已加载的纹理,它可以被全部替换,也可以被替换掉一部分。替换一个纹理图像常常比直接使用glTexImage重新加载一个新纹理快得多。
GLboolean glIsTexture(GLint texture);
判断纹理是否有效
void glVertexAttributePointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* pointer);
指定了渲染时索引值为index的顶点属性数组的数据格式和位置,示例:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
- 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用
layout(location = 0)
定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0
。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
。 - 第二个参数指定顶点属性的大小。顶点属性是一个
vec3
,它由3个值组成,所以大小是3。 - 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中
vec*
都是由浮点数值组成的)。 - 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个
float
之后,我们把步长设置为3 * sizeof(float)
。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。 - 最后一个参数的类型是
void*
,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。
void glEnableVertexAttribArray(GLuint index)
出于性能考虑,所有VertexShader中的属性(Attribute)变量都是关闭的,意味着数据在Shader中是不可见的,哪怕数据已经上传至GPU,由glEnableVertexAttribArray启用后,才可以在VertexShader中访问顶点属性。glVertexAttribPointer只是建立了CPU和GPU之间的逻辑连接,从而数据上传至GPU,但数据是否可用取决于有没有调用glEnableVertexAttribArray.
几个重要的名词
- 顶点数组对象:Vertex Array Object,VAO
- 顶点缓冲对象:Vertex Buffer Object,VBO
- 索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO
通常深度可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节省资源。
核心模式(Core)与立即渲染模式(Immediate mode,也就是固定渲染管线)
标准化设备坐标(Normalized Device Coordinates, NDC)
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。与通常的屏幕坐标不同,y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。
在真实的程序里输入数据通常都不是标准化设备坐标,所以我们首先必须先把它们转换至OpenGL的可视区域内。
引用