顶点属性
什么是顶点属性?
顶点属性(Vertex Attribute)在计算机图形学中,是指用来描述一个顶点特征的数据。这些数据可以是位置、颜色、法线、纹理坐标等等。简单来说,就是用来定义一个顶点在三维空间中的属性,以及它在渲染过程中应该如何被处理
在之前的内容中已经创建了顶点缓冲区,也就是用来存放CPU传过来的顶点数据的缓冲区,有了数据,还需要告诉OpenGL如何解释这些数据,就需要用方法 glVertexAttribPointer
如果顶点只有位置属性,那么:
float position[6] = {
-0.5f, 0.5f,
-0.5f, -0.5f,
0.5f, 0.5f
};
设置属性:
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GL_FLOAT) * 2, (const void*)0);
-
第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用
layout(location = 0)定义了position顶点属性的位置值(Location)吗?layout就是这里的顶点属性的标号,对应position中的顶点,从前到后为位置,颜色,纹理,所以分别为0,1,2 -
第二个参数指定顶点属性的大小。顶点属性是一个
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。我们会在后面详细解释这个参数。
如果有其他属性:
float vertices[] = {
//位置 //颜色 //纹理
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,1.0f, // 右上角
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,0.0f, // 右下角
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,0.0f, // 左下角
-0.5f, 0.5f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f,1.0f // 左上角
};
设置顶点属性:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8, (const void*)0); //positon
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8, (const void*)(3 * sizeof(float))); //color
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 8, (const void*)(6 * sizeof(float))); //color
着色器
现在准备数据的事情都准备的差不多了,总结一下之前做了什么,有一个代表顶点位置的数组,创建了一个VBO用来存放CPU传过来的数据,并初始化VBO的数据为顶点位置的数据,然后设置顶点属性,告诉OpenGL以何种方式来读取数据,现在可以来编写着色器了。
要把着色器代码变为着色器程序,类似于C++的程序的执行过程,同样需要编译链接,所以先来写一个方法:
static unsigned int createShader(const std::string& vertexShader, const std::string& fragmentShader)
{
//创建一个着色器程序
unsigned int program = glCreateProgram();
//编译着色器代码
unsigned int vs = compileShader(GL_VERTEX_SHADER, vertexShader);
unsigned int fs = compileShader(GL_FRAGMENT_SHADER, fragmentShader);
//将得到的着色器代码文件附加到程序上
glAttachShader(program, vs);
glAttachShader(program, fs);
//链接程序
glLinkProgram(program);
//删除中间文件
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
先创建一个程序,然后编译着色器代码,编译后得到的vs和fs可以看作两个文件,将两个文件附加在program上后,链接程序,程序成功链接之后,就获得了一个可以执行的程序,之前的中间文件就不再需要可以直接删除。
编译代码的部分重复较多,再抽象出一个函数:
//编译着色器代码
static unsigned int compileShader(unsigned int type, const std::string& source)
{
//创建着色器
unsigned int id = glCreateShader(type);
//指定着色器源码(将源代码加载到着色器对象中)
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr);
//id:指定要加载源代码的着色器对象的ID。1:表示只有一个字符串。
//&src:指向包含着色器源代码的字符串。nullptr:表示字符串以空字符结尾。
//编译着色器代码
glCompileShader(id);
//错误处理
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (result == GL_FALSE)
{
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
char* message = (char*)alloca(length*sizeof(char));
//char* message = new char[length];
glGetShaderInfoLog(id, length, &length, message);
std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << " shader! " << std::endl;
std::cout << message << std::endl;
glDeleteShader(id);
return 0;
}
return id;
}
着色器代码的编译链接已经完成,现在可以着手着色器代码本身,着色器代码(GLSL)。GLSL需要记住之前提到的,着色器接收一个输入得到一个输出给下一个阶段,所以通过in接收上一阶段的数据处理,之后将结果通过out输出到下一阶段,下一阶段可以通过in来获取这个输出作为本阶段的输入
//编写着色器源码
std::string vertexShader =
R"(#version 330 core
layout(location = 0) in vec4 aPos;
void main()
{
gl_Position = aPos;
}
)";
std::string fragmentShader =
R"(#version 330 core
layout(location = 0) out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
)";
语法细节:(待补充)
准备工作完成,最后开始使用代码
//编译着色器,通过编译链接vs和fs的源码,得到最终的着色器程序
unsigned int shader = createShader(vertexShader,fragmentShader);
glUseProgram(shader);
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
...
}
glDeleteProgram(shader); //回收资源
不出意外的得到了一个大红色的三角形。