在刚学 OpenGL 的时候千万不要沉迷细节,后面你会慢慢懂,又或者说:放弃
一、最简单的着色器
前面讲过,如果我们打算做渲染的话必需要配置顶点和片段着色器,因此这里不得不先提前了解一下着色器
最简单的顶点着色器:
#version 330 core
layout (location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
}
这段很像 C 语言的代码即是 GLSL(OpenGL Shading Language)着色器语言,即开发人员写的短小的自定义程序,它们是在图形卡的 GPU 上执行的,代替了固定的渲染管线的一部分
- #version 330 core:版本号:对应着 3.3 版本
- In 关键字:声明所有的输入顶点属性,目前我们只需要关心位置数据
- layout (location = 0):设定输入变量的位置属性为 0
你会发现预定义的 gl_Position 变量是 vec4 类型的,但其实第 4 维并不表示位置,我们先默认设为 1.0,并且暂时不关心这个
最简单的片段着色器:
#version 330 core
out vec4 color;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
相对于顶点着色器,片段着色器貌似更加简洁一点
- out 限定符:该着色器会将color所对应的数值输出,这也是片段所对应的颜色值
二、着色器编译与链接
为了能够让OpenGL使用对应的着色器,我们必须在运行时动态编译它
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// Check for compile time errors
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;
}
- vertexShader = glCreateShader(GL_VERTEX_SHADER):创建着色器,其中参数 GL_VERTEX_SHADER 即顶点着色器
- glShaderSource(vertexShader, 1, &vertexShaderSource, NULL):第一个参数为编译的着色器对象,第二个参数指定了需要传递的源码字符串的数量,第三个参数就是上面的着色器源码了,第四个参数先不管,目前只需要设置为 null 即可
下半部分的代码,主要是用于检测是否编译成功,如果编译失败获取异常
片段着色器和期类似,具体可以参考完整代码
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本,如果想要使用刚才编译的着色器,我们必须把它们链接为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序,已激活的着色器程序将在我们发送渲染调用的时候被使用
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入,当输出和输入不匹配的时候,会得到一个连接错误
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// Check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
着色器链接代码如上,下半部分的代码,仍然是用于检测是否链接成功,如果失败获取异常
- glCreateProgram:创建一个程序,并返回新创建程序对象的ID引用
- glLinkProgram:将之前编译的着色器附加到程序对象上后,用此方法链接
- glDeleteShader:别忘了用完删除
三、VAO 与 VBO
前面已经介绍过了 glVertexAttribPointer() 这个函数,现在我们可以以现在的理解再来看一次
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0):
- 第一个参数指定我们要配置的顶点属性,现在我们考虑了着色器,还记得我们在顶点着色器中使用 layout(location = 0) 定义了 position 顶点属性的位置值(Location)吗?它把顶点属性的位置值设置为了 0 。因为我们希望把数据传递到这一个顶点属性中,所以这里我们也传入 0
- 第二三四个参数就不用多说了,前面有讲
- 第五个参数:步长(Stride),也就是我们相邻两个顶点属性的间隔(这个属性第二次出现的地方到整个数组第一次出现的地方之间有多少字节),如下图,我们绘制三角形,自然是需要 3 个顶点,并且每个顶点都是个 vec3 ,由 3 个浮点数组成,又因为我们数据是紧挨着的,所以我们相邻两个顶点属性的间隔自然是 3 * sizeof(GLfloat),既然是紧挨着的,我们也可以通过传 0 来让 OpenGL 决定具体步长是多少,一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔
- 最后一个参数:偏移位置,也就是第一个数据所在的位置。这里是0
那上图中的数据是存储在哪里的呢?正是 VBO
程序中可以有多个 VBO,获取则是通过在调用 glVetexAttribPointer 时绑定到 GL_ARRAY_BUFFER 的 VBO 决定的,由于在调用 glVetexAttribPointer 之前绑定的是先前定义的 VBO 对象,顶点属性 0 现在会链接到它的顶点数据
每当我们绘制一个物体的时候都必须重复这一过程(复制顶点数组到缓冲中供OpenGL使用 → 设置顶点属性指针 → 确定使用着色器程序 → 绘制物体),假设属性特别的多,又或者说是物体特别的多,这样就显得特别的麻烦,这个时候就需要VAO了
VAO(Vertex Array Object,顶点数组对象),看这个名字应该就可以明白了,它可以用于存储顶点属性调用,这样的话你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了,不但如此,不同顶点数据和属性配置之间切换也变得变得非常简单,绑定不同的VAO即可
一个顶点数组对象会储存以下这些内容:
- glEnableVertexAttribArray 和 glDisableVertexAttribArray 的调用。
- 通过 glVertexAttribPointer 设置的顶点属性配置。
- 通过 glVertexAttribPointer 调用进行的顶点缓冲对象与顶点属性链接。
glGenVertexArrays(1, &VAO):绑定 VAO,传 0 解绑
四、实例代码
到这里,终于掌握了绘制三角形的所有知识点,前戏比较长,现在可以进入正题了!
#pragma comment(lib,"glew32.lib")
#include<iostream>
#include<opengl/glew.h>
#define GLEW_STATIC
#include<GLFW/glfw3.h>
#include<opengl/freeglut.h>
// Function prototypes
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
// Window dimensions
const GLuint WIDTH = 800, HEIGHT = 600;
// Shaders
const GLchar* vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
const GLchar* fragmentShaderSource =
"#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
// The MAIN function, from here we start the application and run the game loop
int main()
{
// Init GLFW
glfwInit();
// Set all the required options for GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
// Create a GLFWwindow object that we can use for GLFW's functions
GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
glfwMakeContextCurrent(window);
// Set the required callback functions
glfwSetKeyCallback(window, key_callback);
// Set this to true so GLEW knows to use a modern approach to retrieving function pointers and extensions
glewExperimental = GL_TRUE;
// Initialize GLEW to setup the OpenGL Function pointers
glewInit();
// Define the viewport dimensions
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width , height);
// Build and compile our shader program
// Vertex shader
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// Check for compile time errors
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
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// Check for compile time errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// Link shaders
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// Check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// Set up vertex data (and buffer(s)) and attribute pointers
GLfloat vertices[] =
{
-0.5f, -0.5f, 0.0f, // Left
0.5f, -0.5f, 0.0f, // Right
0.0f, 0.5f, 0.0f // Top
};
GLuint VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// Bind the Vertex Array Object first, then bind and set vertex buffer(s) and attribute pointer(s).
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0); // Note that this is allowed, the call to glVertexAttribPointer registered VBO as the currently bound vertex buffer object so afterwards we can safely unbind
glBindVertexArray(0); // Unbind VAO (it's always a good thing to unbind any buffer/array to prevent strange bugs)
// Game loop
while (!glfwWindowShouldClose(window))
{
// Check if any events have been activiated (key pressed, mouse moved etc.) and call corresponding response functions
glfwPollEvents();
// Render
// Clear the colorbuffer
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// Draw our first triangle
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
// Swap the screen buffers
glfwSwapBuffers(window);
}
// Properly de-allocate all resources once they've outlived their purpose
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// Terminate GLFW, clearing any resources allocated by GLFW.
glfwTerminate();
return 0;
}
// Is called whenever a key is pressed/released via GLFW
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
}
效果如下: