目录
图形渲染管线介绍
1.图形渲染管线主要被划分成两个主要部分,第一部分是把物体的3D坐标转换成为2D坐标,第二部分是把2D坐标转变为有实际颜色的像素。一般步骤为:顶点着色器->图元装配->几何着色器->光栅化->片段着色器->Alpha测试与混合。
2.图形渲染管线高度专门化,所以大多数显卡可以并行执行,而在GPU上为每一个渲染管线阶段运行各自的小程序,这些小程序的名字就叫做着色器。而且着色器还可以用自己写的着色器(GLSL)代替默认的。
(一)顶点缓冲区
1.顶点缓冲区的概念与辨析 :基本就是去掉vertex,它只是一个内存缓冲区,一个内存字节数组,从字面上讲就是一块用来存字节的内存。
辨析:但是顶点缓冲区又和C++中像字符数组的内存缓冲区不太一样,它是OpenGL中的内存缓冲区,这意味着它实际上在显卡显存(Video RAM)上。
2.概念理解:所以这里的基本思路是我要定义一些数据来表示三角形,我要把它放入显卡VRAM中,然后还需要发出DrawCall绘制指令。实际上我们还需要告诉显卡如何读取和解释这些数据,以及如何把它放在我们的屏幕上,一旦我们发出DrawCall绘制指令,就得明确着色器的内存布局,怎么摆出来以及显卡需要怎么做,需要对显卡编程,就是着色器。着色器是一堆我们可以编写的在显卡上以一种非常特殊的方式运行的代码。
3.运行原理:“对显卡输送数据--->向显卡解释数据--->绘制目标步骤”。而OpenGL像一台状态机,我们要处理的一系列的状态而并非对象。这就是OpenGL渲染的流程。
4.代码实践:
声明:旧版OpenGL与新版OpenGL有实现上的差异。对于新版需要用到VBO和VAO绑定。
另外在生成缓冲区对象之前,需要检查glad是否可以获取地址,否则会无法生成缓冲区对象。
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//旧版处理方式:这种写法在glad版本的OpenGL中已经过时了,但是在glew.h下可以跑。
//(glad.h是glew.h进化版)
unsigned int buffer;
glGenBuffer(1,&buffer);
glBindBuffer(GL_VERTEX_BUFFER,buffer);
glBufferData(GL_ARRAY_BUFFER,6*sizeof(float),positions,GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(float)*2,0);
glBindBuffer(GL_ARRAY_BUFFER,0);
创建顶点缓冲区:
unsigned int buffer;
glGenBuffers(1,&buffer);
//第一个参数: 指定需要生成几个已声明的缓冲区
//第二个参数: 指定返回整数的内存地址
//效果: 生成缓冲区的id,表征上看是一个整数,但是代指的是实际对象。
//当你想用这个对象时你就可以使用这个数字调用。
但是,我们要渲染三角形,就需要说明是用那个缓冲区来渲染三角形,即传递这个整数。
glBindBuffer(GL_ARRAY_BUFFER,buffer);
下一步是指定数据。一个简单的方式是在声明数据的时候直接把顶点数据填充进去
float positions[6]={
-0.5f,-0.5f,
0.0f ,0.5f,
0.5f,-0.5f
};
glBufferData(GL_ARRAY_BUFFER,6*sizeof(float),positions,GL_STATIC_DRAW);
//然后我们一般需要创建索引缓冲区,没有索引缓冲区就可以调用glDrawArrays()来绘制指定图元
glDrawArrays(GL_TRIANGLE,0,3);
这种选择类似于PS,选中图层,选中笔刷,而只影响该图层。OpenGL也一样,在使用它之前需要选择或绑定 必备的东西,这就是它的运行原理,它是上下文相关的状态机。
最后记得调用glEnableVertexAttribArray(),启用glVertexAttribPointer();
glEnableVertexAttribArray(0);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(float)*2,0);
//效果:这两段代码告诉OpenGL缓冲区布局是什么,理论上有一个着色器就可以在屏幕上看到三角形了。
(二)VAO/VBO/IBO
1.新版本顶点缓冲区引入的几个概念
顶点数组对象 Vertex Array Object ,VAO
顶点缓冲对象 Vertex Buffer Object ,VBO 【就相当于旧版的顶点缓冲区】
元素缓冲对象 Element Buffer Object ,EBO/或索引缓冲对象 Index Buffer Object,IBO
阅读反思:
在LearnOpenGl的教程中,提到OpenGL的核心模式要求我们使用VAO。这句话的意思是必须。仔细阅读,甚至一个语气都可能影响到你对使用条件的把握。
另外注意精确理解术语。比如当提到“对象"时,我们就可以参照C++里面的对象与属性的概念去理解。
定性理解。顶点缓冲对象。顶点就是待输入的数据,对象就是一个状态的集成。这里的关键字就是”缓冲“,那么意思就是说,这是一块内存。这里指对象的内存,而并非内存的对象。
2.新旧版本剖析
//旧版处理方式:这种写法在glad版本的OpenGL中已经过时了。但是在glew.h下可以跑。(glad.h是glew.h进化版)
//0.生成指定数量的缓冲区对象
unsigned int buffer;
glGenBuffer(1,&buffer);
//1.复制对顶点数组到缓冲区中供OpenGL使用
glBindBuffer(GL_VERTEX_BUFFER,buffer);
glBufferData(GL_ARRAY_BUFFER,6*sizeof(float),positions,GL_STATIC_DRAW);
//2.设置顶点属性指针
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(float)*2,0);
glEnableVertexAttribArray(0);
//3.但我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
//4.绘制物体
glUseProgram(shaderProgram);
//解绑:结束对象的生命周期
glBindVertexArray(0);
分析:通过旧版本对顶点数组的处理办法,我们可以观察到当我们绘制一个物体时必须要重复这一过程,但是如果有五个以上的顶点属性,上百个物体。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快变成一个麻烦事。这就是面向过程编程的劣势。
3.总结
//新版本处理方式
// 初始化代码(只运行一次 (除非你的物体频繁改变))
// 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);
[...]
// 绘制代码(渲染循环中)
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
(1) VAO的设计思想 类似于面向对象的类的概念。我们用一个顶点数组对象去集成任意多个属性。当配置顶点属性指针时,只需要将那些调用执行一次,之后绘制物体时只需绑定相应的VA0就行了。
(2) VBO 但是想使用VAO,我们还需绑定与配置对应的VBO来说明这是那个对象的内存。VBO就像我们调用函数,会在栈上占用一块虚拟空间,(内存)会随着对象的生命周期结束后而释放(可以理解为失效)所以VBO与VAO拥有生命周期。
(3) VAO + VBO= 一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。
4.配置顶点属性指针
//设置顶点属性指针,设置位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//设置颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
........我们还可以接下来配置纹理等属性
注释:
1.glVertexAttribPointer函数是使用来配置顶点属性的。
参数含义分别为,1顶点位置的起始位置,2几个一组,3数据类型,4是否需要标准化,5步长(以字节为单位),偏移量指针(字节为单位))
2.glEnableVertexAttribArray()用来激活对应属性的顶点着色器
(三)着色器
1.概念:着色器是一个运行在显卡上的程序代码。代码形式:以文本或字符串形式编写的。
编译方式:发送到GPU上编译链接运行。
CPU与GPU的配合:GPU处理图形的速度快的多,所以利用显卡的能力在屏幕上绘制图形。
而CPU也有擅长的部分,我们可以将结果数据发送给显卡同时仍然在CPU上处理。
2.类型:对于大多数图形编程:重点放在两种着色器,顶点着色器和片段着色器。
(1)顶点着色器:他会被渲染的每个顶点调用,主要是告诉显卡我们计划这个顶点在屏幕空间的什么位置。而窗口基本上就是由像素组成的,指定的顶点就需要实际的像素填充,勾勒出了图形的轮廓。
(2)片段着色器:顶点着色器结束运行,进入片段着色器管道。片段着色器就是对需要填充的每个像素调用一次,主要决定像素是什么颜色的。相当于在勾勒的轮廓里上色。
对比:顶点着色器和片段着色器性质上是相似的,但是使用片段着色器的代价合并价值更高。
因为顶点着色器是一次性批量发送数据,但是是片段着色器是为每个像素运行,后者运行成本更大。并且,顶点着色处理输入信息,而片段着色器需要合成信息,计算的信息量不同。然而有些东西还必须按像素计算,比如光源。受光源,环境,纹理,提供表面的材质等因素影响,每个像素的颜色值都不同。而在这一些输入结束后,在片段着色器中的决定仅仅是单个像素的颜色。
3.代码实践
主要完成的任务有三个,创建着色器绑定至程序,编译着色器源码为程序,调用着色器。
如何管理shader文件?首先将不同功能的代码文件分类以便于我们管理。因此我们将shader的源码单独放在.shader文件中,其中vertex shader和fragment shader文件可以分成两个,也可以写成一个。但是如果以后比如一个顶点着色器要搭配多个片段着色器,还是会依照功能分成多个文件。
另外,shader独立成文件后,需要以读取文件的方式建立链接。
//shader 源码
#shader vertex
#version 330 core
layout(location = 0) in vec4 position;
void main()
{
gl_Position = position;
};
#shader fragment
#version 330 core
layout(location = 0) out vec4 color;
void main()
{
color = vec4(1.0, 0.0, 0.0, 1.0);
};
然后我们需要用parse逐行解析shader,解析到#shader就去判断是什么类型的shader,然后去填入string stream中。
struct ShaderProgramSource
{
std::string VertexSource;
std::string FragmentSource;
};
static ShaderProgramSource ParseShader(const std::string& filepath)
{
std::ifstream stream(filepath); //从硬盘到内存
enum class ShaderType
{
NONE=-1,VERTEX=0,FRAGMENT=1
};
std::stringstream ss[2];
ShaderType type=ShaderType::NONE;
std::string line;
while (getline(stream, line))
{
if (line.find("#shader") != std::string::npos)
{
if (line.find("vertex") != std::string::npos)
type = ShaderType::VERTEX;
else if (line.find("fragment") != std::string::npos)
type = ShaderType::FRAGMENT;
}
else
{
ss[(int)type] << line << "\n";
}
}
return { ss[0].str(),ss[1].str() };
}
4.以上我们完成了着色器的生成,编译,读取。接下来实现着色器程序的调用。 然后我们需要创建一个缓冲区,生成指定个数的缓冲区对象;绑定缓冲区 ;把内存中的数据拷贝到GPU的显存中;激活着色器;按照属性指定着色器的内存布局;解析shader源码;创建shader程序;激活程序。
完整代码
#include"glad/glad.h"
#include"GLFW/glfw3.h"
#include<iostream>
#include<fstream>
#include<sstream>
struct ShaderProgramSource
{
std::string VertexSource;
std::string FragmentSource;
};
static ShaderProgramSource ParseShader(const std::string& filepath)
{
std::ifstream stream(filepath); //从硬盘到内存
enum class ShaderType
{
NONE=-1,VERTEX=0,FRAGMENT=1
};
std::stringstream ss[2];
ShaderType type=ShaderType::NONE;
std::string line;
while (getline(stream, line))
{
if (line.find("#shader") != std::string::npos)
{
if (line.find("vertex") != std::string::npos)
type = ShaderType::VERTEX;
else if (line.find("fragment") != std::string::npos)
type = ShaderType::FRAGMENT;
}
else
{
ss[(int)type] << line << "\n";
}
}
return { ss[0].str(),ss[1].str() };
}
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);
glCompileShader(id);
//to do error handing
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));//C函数,在栈上分配
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;
}
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);
glValidateProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
int main(void)
{
GLFWwindow* window;
glfwInit();
if (!glfwInit()) return -1;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
window = glfwCreateWindow(480, 480, "hello", NULL, NULL);
if (!window)
{
std::cout << "error" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
float positions[6] = {
-0.5f,-0.5f,
0.0f, 0.5f,
0.5f, -0.5f
};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, 6* sizeof(float) , positions, GL_STATIC_DRAW);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,8,(const void*)0);
glEnableVertexAttribArray(0);
ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");
unsigned int shader = CreateShader(source.VertexSource,source.FragmentSource);
glUseProgram(shader);
while (!glfwWindowShouldClose(window))
{
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shader);
glfwTerminate();
return 0;
}
(四)索引缓冲区
(1)背景
举一反三,回顾绘制一个三角形的过程,我们联想如何画一个四边形呢?一个角度上我们可以认为所有的多边形都是由三角形组成的,那么我们可以以同样的方法在不同的位置绘制若干三角形以此来拼接出我们的目标多边形。但是一旦模型复杂起来,就会有许多重复的顶点,这种办法就完全不现实。
另一个角度,按照“环节”去考虑,我们应该在顶点着色器管道就勾勒出轮廓,然后在片段着色器管道里上色。显然第一个角度不符合OpenGL的设计理念。
而顶点缓冲区的目的就是不提供冗余或重复的顶点位置。
(2)概念:
删除任何重复的顶点,在顶点缓冲区得到了完全唯一的顶点,之后创建一个索引以便多次绘制顶点,然后我们用IBO绑定代码把索引缓冲区发送给显卡;最终我们使用glDrawElement()绘制图形。
(3)代码实现:
unsigned int IBO;
glGenBuffers(1, &IBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indicesGL_STATIC_DRAW);
//rendering
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,nullptr);
绘制矩形完整代码 :
//main.cpp
#include"glad/glad.h"
#include"GLFW/glfw3.h"
#include<iostream>
#include<fstream>
#include<sstream>
struct ShaderProgramSource
{
std::string VertexSource;
std::string FragmentSource;
};
static ShaderProgramSource ParseShader(const std::string& filepath)
{
std::ifstream stream(filepath); //从硬盘到内存
enum class ShaderType
{
NONE=-1,VERTEX=0,FRAGMENT=1
};
std::stringstream ss[2];
ShaderType type=ShaderType::NONE;
std::string line;
while (getline(stream, line))
{
if (line.find("#shader") != std::string::npos)
{
if (line.find("vertex") != std::string::npos)
type = ShaderType::VERTEX;
else if (line.find("fragment") != std::string::npos)
type = ShaderType::FRAGMENT;
}
else
{
ss[(int)type] << line << "\n";
}
}
return { ss[0].str(),ss[1].str() };
}
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);
glCompileShader(id);
//to do error handing
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));//C函数,在栈上分配
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;
}
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);
glValidateProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
int main(void)
{
GLFWwindow* window;
glfwInit();
if (!glfwInit()) return -1;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
window = glfwCreateWindow(480, 480, "hello", NULL, NULL);
if (!window)
{
std::cout << "error" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//顶点属性
float positions[] = {
-0.5f,-0.5f,
0.5, -0.5f,
0.5f, 0.5f,
-0.5f,0.5f
};
unsigned int indices[] =
{
0,1,2,
2,3,0
};
unsigned int VAO,VBO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, 6 * 2 * sizeof(float), positions, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,8,(const void*)0);
unsigned int IBO;
glGenBuffers(1, &IBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);
ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");
unsigned int shader = CreateShader(source.VertexSource,source.FragmentSource);
glUseProgram(shader);
while (!glfwWindowShouldClose(window))
{
glClear(GL_COLOR_BUFFER_BIT);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//线框模式
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteProgram(shader);
glfwTerminate();
return 0;
}