顶点着色器
首先我们需要定义一些顶点数据。OpenGL不是简单地把所有的3D坐标转换成屏幕上的2D像素。它只会把x,y,z在-1.0到1.0范围时才会处理。然后在标准化设备坐标范围内的坐标才会最终呈现在屏幕上。
float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
由于我们需要绘制的是一个2D的三角形,所以我们把z值都设为0。
标准化设备坐标(Normalized Device Coordinates, NDC)
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):
与通常的屏幕坐标不同,y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。最终你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。
你的标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。
我们通过顶点缓冲对象管理这个内存,它会在GPU内存中存储大量顶点。使用这些缓冲对象的好处就是可以一次性发送大批顶点数据到显卡上,CPU发送数据到显卡相对较慢,所需需要尽可能一次性发送更多的数据。当数据在显卡的内存中,顶点着色器几乎能立即访问这些顶点,速度是非常快的。
顶点缓冲对象和OpenGL中的其他对象一样,这个缓冲对象有一个独一无二的ID,我们可以通过glGenBuffers函数和一个缓冲ID生成一个VBO对象:
unsigned int VBO; glGenBuffers(1, &VBO);
在OpenGL中有很多种缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同类型的缓冲对象,可以通过glGenBuffers函数和一个缓冲ID生成一个VBO对象。
glBindBuffer(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,这样就能确保显卡把数据放在能够高速写入的内存部分。
顶点着色器
我们需要用着色器语言GLSL编写顶点作色器。下面是最简单的一个顶点着色器源代码:
#version 330 core
layout(location =0 ) in vec3 aPos;
void main()
{
gl_Position=vec4(aPos.x,aPos.y,aPos.z,1.0);
}
向量(Vector)
向量在图形编程中我们会经常使用到,向量能准确的描述位置和方向。在GLSL中一个向量最多有四个分量,分别可以通过vec.x,vec.y,vec.z和vec.w,vec.w这个分量不是用作表达空间中的位置的,而是用在所谓的透视除法上。
为了设置顶点着色器的输出,我们必须把位置数据复制给预定义的gl_Position变量,它是vec4类型的。在main函数中,我们将gl_Position设置的值会成为这个顶点着色器的输出。由于我们输入的分量只有三个,所以必须转换为4分量的,可以通过把ve3的数据作为ve4构造器的参数,同时把vec.w分量设置为1.0f。
编译着色器
顶点着色器的代码我们需要动态编译,它存在一个c的字符串中。
首先需要创建一个着色器对象,顶点着色器的类型为Unsigned int,然后通过glCreateShader创建这个作色器。
unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER);
因为我们正在创建的是顶点着色器,所以我们传递的参数是GL_VERTEX_SHADER。然后将作色器源码附加到作色器对象上,编译。
glShaderSource(vertexShader,1,&vertexShaderSource,NULL); glCompileShader(vertexShader);
void
glShaderSource
(
GLuint shader, GLsizei count, const GLchar **string, const GLint *length)
;
GLuint shader :指定要替换源代码的作色器对象句柄
GLsizei :指定源码数字串的数量
GLchar **string:指定一个包含作色器代码的字符串指针数组。
GLint *length:指定字符串长度数组
检测编译是否成功:
int success; char 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; }
片段着色器
片段作色器的主要中所用计算像素的最后输出颜色。我们也用一个vec4来表示向量的输入颜色分别为:红色、绿色、蓝色和Alpha分量,通常我们会把它叫做RGBA。
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }
片段着色器的创建和编译方法和顶点作色器一样,只是在创建片段作色器对象的过程中,需要把GL_VERTEX_SHADER换成GL_FRAGMENT_SHADER
unsigned int fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);
着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候**这个着色器程序。已**着色器程序的着色器将在我们发送渲染调用的时候被使用。
glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用。现在我们需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们:
unsigned int shaderProgram; shaderProgram=glCreateProgram(); glAttachShader(shaderProgram,vertexShader); glAttachShader(shaderProgram,fragmentShader);
接下来我们可以通过glGetShaderiv和glGetShaderInfoLogl来检测链接着色器程序是否失败:
void glGetShaderiv(GLuint shader,Glenum pname,Glint *params)
GLuint shader:指定的shader对象
glenum pname:状态参数,通常有一下几种GL_SHADER_TYPE
, GL_DELETE_STATUS
, GL_COMPILE_STATUS
, GL_INFO_LOG_LENGTH
, GL_SHADER_SOURCE_LENGTH
.
Glint *params:编译状态查询
void glGetShaderInfoLog(Gluint shader,GLsizei maxLength,GLsizei *length,GLchar *infoLog)
LGsizei maxLength:指定用于存储返回的信息日志的字符缓冲区的大小。
Glsizei* length:返回在infoLog中返回的字符串的长度(不包括空终止符)。
Glchar* infoLog: 指定用于返回信息日志的字符数组。
然后通过glUseProgram来**这个对象,在使用完只有我们需要通过glDeleteShader来删除Shader程序。
链接顶点属性
我们应该告诉OpenGL如何解释我们传进来的顶点数据。
- 位置数据被储存为32位(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
- 数据中第一个值在缓冲开始的位置。
glVertexAttribPointer(0,3,GL_FLOAT,GL_FLSE,3 * sizeof(float),(void*)0); glenableVertexAttribArray(0);
glVertexAttribPointer这个函数的参数比较多
- 第一个参数就是我们要配置的顶点属性,这个和我们在顶点着色器中使用的layout(location =0) 定义了position的顶点属性位置是对应的。
- 第二个参数是顶点属性的大小,因为是vec3,它的分量为3所以我们填3
- 第三个是数据类型,这里是GL_FLOAT
- 第四个参数表示是否标准化,如果是GL_TRUE,则所有顶点数据都会被转换成0到1之间。所以我们选择GL_FALSE
- 第五个参数是步长,由于下一个顶点数据在3*sizeof(float)之后,所以我们这里填写3*sizeof(float)
- 第六个参数是偏移量,在这里我们的偏移量是0,所以我们传入(void*)0
// 0. 复制顶点数组到缓冲*OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 1. 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 2. 当我们渲染一个物体时要使用着色器程序 glUseProgram(shaderProgram); // 3. 绘制物体 someOpenGLFunctionThatDrawsOurTriangle();
顶点数组对象
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
一个顶点数组对象会储存以下这些内容:
- glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- 通过glVertexAttribPointer设置的顶点属性配置。
- 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
下面就是通过创建顶点数组对象,然后通过GLBindVertexArray绑定VAO,然后绘制物体。
unsigned int VAO; glGenVertexArrays(1, &VAO); // 1. 绑定VAO glBindVertexArray(VAO); // 2. 把顶点数组复制到缓冲*OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 3. 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);
最后通过glDrawArrays函数来绘制三角形:
glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3);