你好,三角形
图形渲染管线
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线管理的。
图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的,并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
有些着色器允许开发者自己配置,这就允许我们用自己写的着色器来替换默认的。这样我们就可以更细致地控制图形渲染管线中的特定部分了,而且因为它们运行在GPU上,所以它们可以给我们节约宝贵的CPU时间。OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的。
OpenGL中的图形渲染管线由顶点着色器、图元装配、集合着色器、光栅化、片段着色器、测试与混合组成。
顶点输入
我们在开始绘制图形之前需要给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,输入的顶点都是标准化( x , y , z ∈ [ − 1.0 , 1.0 ] x,y,z\in[-1.0,1.0] x,y,z∈[−1.0,1.0])的3D坐标。在这个范围之外的坐标不会被显示。
标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。
定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。
我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。
顶点着色器
顶点着色器(Vertex Shader)是几个可编程着色器中的一个。如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。
我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,这样我们就可以在程序中使用它了。
编译着色器
我们已经写了一个顶点着色器源码(储存在一个C的字符串或文件中),但是为了能够让OpenGL使用它,我们必须在运行时动态编译它的源码。
片段着色器
片段着色器(Fragment Shader)是第二个也是最后一个我们打算创建的用于渲染三角形的着色器。片段着色器所做的是计算像素最后的颜色输出。
在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。
同样地,片段着色器也需要在编译后使用。
着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序对象的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。也就是说,链接的顺序其实是规定好的。
链接顶点属性
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
因此,我们必须告诉OpenGL如何解析顶点数据,从而确定每一个顶点属性。
顶点数组对象
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。
索引缓冲对象
索引缓冲对象也是一个缓冲,它专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。
渲染三角形的源码
渲染代码
#include "../env/glad.h"
#include "../env/glfw3.h"
#include <iostream>
#include <fstream>
#define WIDTH 800
#define HEIGHT 600
GLFWwindow *initialize(int width, int height);
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);
std::string getShaderSource(const std::string &filename);
int main() {
GLFWwindow *window = initialize(WIDTH, HEIGHT);
int success;
char info[512];
const char *charPtr;
const char **charPtrPtr;
// 以下是编译着色器
// 创建一个着色器对象
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
// 读取顶点着色器GLSL代码
std::string vertexShaderSource = getShaderSource("2-vertice.glsl");
if(vertexShaderSource.empty()) return -1;
charPtr = vertexShaderSource.c_str();
charPtrPtr = &charPtr;
// 把这个着色器源码附加到着色器对象上,然后编译
glShaderSource(vertexShader, 1, charPtrPtr, nullptr);
glCompileShader(vertexShader);
// 获取顶点着色器编译信息
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success) {
glGetShaderInfoLog(vertexShader, 512, nullptr, info);
std::cout << "Complie vertex shader error: " << info << std::endl;
return -1;
}
// 编译片段着色器
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
std::string fragmentShaderSource = getShaderSource("2-fragment.glsl");
charPtr= fragmentShaderSource.c_str();
charPtrPtr = &charPtr;
glShaderSource(fragmentShader, 1, charPtrPtr, nullptr);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if(!success) {
glGetShaderInfoLog(fragmentShader, 512, nullptr, info);
std::cout << "Complie fragment shader error: " << info << std::endl;
return -1;
}
// 创建着色器程序对象
GLuint shaderProgram;
shaderProgram = glCreateProgram();
// 链接着色器到程序对象
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// 生成顶点数组对象
// 当配置顶点属性指针时,你只需要将那些调用执行一次,
// 之后再绘制物体的时候只需要绑定相应的VAO就行了。
GLuint myVAO;
glGenVertexArrays(1, &myVAO);
glBindVertexArray(myVAO);
// 三角形的三个顶点
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
// 生成顶点缓冲对象
unsigned int myVBO;
glGenBuffers(1, &myVBO);
// 将顶点缓冲对象绑定到特定的顶点缓冲上
// 一旦缓冲类型被绑定对象后,我们使用的任何缓冲调用都会用来配置之前前绑定的对象
glBindBuffer(GL_ARRAY_BUFFER, myVBO);
// 把定义的顶点数据复制到缓冲的内存中
// 第4个参数指定了显卡如何管理这部分数据
// GL_STATIC_DRAW :数据不会或几乎不会改变。
// GL_DYNAMIC_DRAW:数据会被改变很多。
// GL_STREAM_DRAW :数据每次绘制时都会改变。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 告诉OpenGL如何解析顶点数据
// 参数1:在GLSL中定义的位置
// 参数2:顶点的维度
// 参数3:顶点的数据类型
// 参数4:是否希望标准化
// 参数5:相邻两个顶点的间隔
// 参数6:起始顶点在缓冲的起始位置
// 每个顶点属性从一个VBO管理的内存中获得它的数据,
// 而具体是从哪个VBO(程序中可以有多个VBO)获取
// 则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);
// 启用顶点属性,默认禁用
// 参数是顶点的在GLSL定义的位置
glEnableVertexAttribArray(0);
while(!glfwWindowShouldClose(window)) {
glUseProgram(shaderProgram);
glBindVertexArray(myVAO);
// 使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。
// 参数1:图元类型
// 参数2:顶点数组起始索引
// 参数3:绘制的顶点数
glDrawArrays(GL_TRIANGLES, 0, 3);
processInput(window);
glfwSwapBuffers(window);
glfwPollEvents();
glClearColor(0.2,0.3,0.3,1.0);
glClear(GL_COLOR_BUFFER_BIT);
}
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
GLFWwindow *initialize(int width, int height) {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, "2", nullptr, nullptr);
if(!window) {
exit(-1);
}
glfwMakeContextCurrent(window);
if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
exit(-1);
}
glViewport(0,0,width,height);
glfwSetWindowSizeCallback(window, framebuffer_size_callback);
return window;
}
std::string getShaderSource(const std::string &filename) {
std::ifstream in;
in.open(filename, std::ios::in);
if(!in.is_open()) {
return "";
}
std::string ans, line;
while(!in.eof()) {
getline(in, line);
ans += line;
ans.push_back('\n');
}
return ans;
}
顶点着色器“2-vertices.glsl”
// 每个着色器都起始于一个版本声明。
// core表明使用核心模式
#version 330 core
// in声明输入顶点的属性
// layout (location = 0)设定了输入变量的位置值
layout (location = 0) in vec3 aPos;
void main()
{
// 为了设置顶点着色器的输出,
// 我们必须把位置数据赋值给预定义的gl_Position变量
// gl_position是vec4类型的
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
片段着色器“2-fragment.glsl”
#version 330 core
// out关键字声明输出变量
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
渲染矩形的源码
#include "../env/glad.h"
#include "../env/glfw3.h"
#include <iostream>
#include <fstream>
#define WIDTH 800
#define HEIGHT 600
GLFWwindow *initialize(int width, int height);
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);
std::string getShaderSource(const std::string &filename);
int main() {
GLFWwindow *window = initialize(WIDTH, HEIGHT);
int success;
char info[512];
const char *charPtr;
const char **charPtrPtr;
// 以下是编译着色器
// 创建一个着色器对象
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
// 读取顶点着色器GLSL代码
std::string vertexShaderSource = getShaderSource("2-vertice.glsl");
if(vertexShaderSource.empty()) return -1;
charPtr = vertexShaderSource.c_str();
charPtrPtr = &charPtr;
// 把这个着色器源码附加到着色器对象上,然后编译
glShaderSource(vertexShader, 1, charPtrPtr, nullptr);
glCompileShader(vertexShader);
// 获取顶点着色器编译信息
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success) {
glGetShaderInfoLog(vertexShader, 512, nullptr, info);
std::cout << "Complie vertex shader error: " << info << std::endl;
return -1;
}
// 编译片段着色器
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
std::string fragmentShaderSource = getShaderSource("2-fragment.glsl");
charPtr= fragmentShaderSource.c_str();
charPtrPtr = &charPtr;
glShaderSource(fragmentShader, 1, charPtrPtr, nullptr);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if(!success) {
glGetShaderInfoLog(fragmentShader, 512, nullptr, info);
std::cout << "Complie fragment shader error: " << info << std::endl;
return -1;
}
// 创建着色器程序对象
GLuint shaderProgram;
shaderProgram = glCreateProgram();
// 链接着色器到程序对象
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// 生成顶点数组对象
// 当配置顶点属性指针时,你只需要将那些调用执行一次,
// 之后再绘制物体的时候只需要绑定相应的VAO就行了。
GLuint myVAO;
glGenVertexArrays(1, &myVAO);
glBindVertexArray(myVAO);
// 矩形的4个顶点
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
-0.5f, 0.5f, 0.0f,
0.5f, 0.5f, 0.0f
};
unsigned int indices[]= {
0, 1, 2,
1, 2, 3
};
// 生成顶点缓冲对象
unsigned int myVBO;
glGenBuffers(1, &myVBO);
// 将顶点缓冲对象绑定到特定的顶点缓冲上
// 一旦缓冲类型被绑定对象后,我们使用的任何缓冲调用都会用来配置之前前绑定的对象
glBindBuffer(GL_ARRAY_BUFFER, myVBO);
// 把定义的顶点数据复制到缓冲的内存中
// 第4个参数指定了显卡如何管理这部分数据
// GL_STATIC_DRAW :数据不会或几乎不会改变。
// GL_DYNAMIC_DRAW:数据会被改变很多。
// GL_STREAM_DRAW :数据每次绘制时都会改变。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 告诉OpenGL如何解析顶点数据
// 参数1:在GLSL中定义的位置
// 参数2:顶点的维度
// 参数3:顶点的数据类型
// 参数4:是否希望标准化
// 参数5:相邻两个顶点的间隔
// 参数6:起始顶点在缓冲的起始位置
// 每个顶点属性从一个VBO管理的内存中获得它的数据,
// 而具体是从哪个VBO(程序中可以有多个VBO)获取
// 则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);
// 启用顶点属性,默认禁用
// 参数是顶点的在GLSL定义的位置
glEnableVertexAttribArray(0);
// 生成索引缓冲对象
unsigned int myEBO;
glGenBuffers(1, &myEBO);
// 把索引复制到索引缓冲中
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, myEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 线框模式绘制
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
while(!glfwWindowShouldClose(window)) {
glUseProgram(shaderProgram);
glBindVertexArray(myVAO);
// VAO自动绑定了EBO
// 这里用glDrawElements代替了glDrawArrays
// 参数1:绘制模式
// 参数2:绘制的定点数
// 参数3:索引类型
// 参数4:EBO中的偏移量
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
processInput(window);
glfwSwapBuffers(window);
glfwPollEvents();
glClearColor(0.2,0.3,0.3,1.0);
glClear(GL_COLOR_BUFFER_BIT);
}
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
GLFWwindow *initialize(int width, int height) {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, "2", nullptr, nullptr);
if(!window) {
exit(-1);
}
glfwMakeContextCurrent(window);
if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
exit(-1);
}
glViewport(0,0,width,height);
glfwSetWindowSizeCallback(window, framebuffer_size_callback);
return window;
}
std::string getShaderSource(const std::string &filename) {
std::ifstream in;
in.open(filename, std::ios::in);
if(!in.is_open()) {
return "";
}
std::string ans, line;
while(!in.eof()) {
getline(in, line);
ans += line;
ans.push_back('\n');
}
return ans;
}