Learn OpenGL CN
Learn OpenGL EN
本来是懒得写笔记了,毕竟原教程已经很详细了,步骤也很清晰
一口气做完前四章后(Hello Triangle、Shaders、Texture、Transform),感觉还是需要一个总结,老了记性不大好(爬
另一方面原教程的代码基本都挤在 main() 里,看着属实难受
OpenGL 简介
OpenGL 是什么
- 本身是一套规范,而不是API
- 规定函数的功能、结果
- 函数的具体实现由库的开发者决定(通常是显卡厂商)
- 包含了一系列可以操作图形、图像的函数
- 本质上是状态机
- 使用 State-Changing 函数设置 Context 状态,使用 State-Using 函数执行
- 内核是 C 库
- 对象、绑定对象
- 对象、绑定对象
OpenGL 中经常用到的流程
// 1. 创建对象
unsigned int objectId = 0;
glGenObject(1, &objectId);
// 2. 绑定对象至上下文 context
glBindObject(GL_WINDOW_TARGET, objectId);
// 3. 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 4. 将上下文对象设回默认
glBindObject(GL_WINDOW_TARGET, 0);
OpenGL 渲染模式
立即渲染模式(Immediate mode,固定渲染管线):抽象了很多细节,自由度低
核心渲染模式(Core-profile)
OpenGL 的扩展(Extension)
可以使用扩展是 OpenGL 的一大特性
当一个显卡公司提出一个新特性或者渲染上的大优化,通常会以扩展的方式在驱动中实现
开发者不必等待一个新的 OpenGL 规范面世,就可以使用这些新的渲染特性了,只需要简单地检查一下显卡是否支持此扩展
通常,当一个扩展非常流行或者非常有用的时候,它将最终成为未来的OpenGL规范的一部分
if(GL_ARB_extension_name)
{
// 使用硬件支持的全新的现代特性
}
else
{
// 不支持此扩展: 用旧的方式去做
}
渲染流水线
记忆恢复术·一图流(之前的笔记印象还是比较深的,就不再整理了,只做一些补充)
- 图元 Primitive:想要绘制成的样子,如 GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP
- 像素 Pixel 和片元 Fragment 的区别:片元是潜在的像素(当片元通过了各种测试被渲染到屏幕,它就成了像素)
常用对象
- 顶点数组对象:Vertex Array Object,VAO
- 顶点缓冲对象:Vertex Buffer Object,VBO
- 元素缓冲对象(索引缓冲对象):Element Buffer Object,EBO(Index Buffer Object,IBO)
GPU 上的 Buffer 操作
新手村常用 Buffer:GL_ARRAY_BUFFER
、GL_ELEMENT_BUFFER
// 1. 生成 Buffer,并有一个独一无二的 ID
unsigned int VBO;
glGenBuffers(1, &VBO);
// 2. Bind 类型,可同时绑定多个不同类型的 Buffer
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 3. 填充数据,将数据复制到当前与 GL_ARRAY_BUFFER 绑定的内存空间中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
GL_STATIC_DRAW
:数据不会或几乎不会改变,如静态图形GL_DYNAMIC_DRAW
:数据会被改变很多。GL_STREAM_DRAW
:数据每次绘制时都会改变。
还有个 glBufferSubData()
,可以往 Buffer 里补充数据,需要计算好 Offset
整体流程
int main()
{
// 1. 初始化 GLFW 窗口
GLFWwindow* window = GLFWWindowSetup();
// 2. 初始化 GLAD
// GLAD: 管理 OpenGL 的指针,必须在使用 OpenGL 前初始化
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 3. 设置视口尺寸
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
// 4. 读取、编译 Shader,设置 Shader Program
Shader shader("VertexShader.glsl","FragmentShader.glsl");
// 5. 准备顶点属性 VAO、纹理单元 Texture Unit
unsigned int VAO, VBO;
PrepareVertexAttriPtr(&VAO, &VBO);
unsigned int texture0, texture1;
PrepareTexture("Resource/TestTexture.jpg", &texture0);
PrepareTexture("Resource/sakura.jpg", &texture1);
// 6. 渲染循环
while (!glfwWindowShouldClose(window))
{
// 6.1 检测关闭窗口的输入
ProcessInput(window);
// 6.2 真正的渲染循环部分
RenderLoop(shader, VAO, texture0, texture1);
// 6.3 交换 Buffer, 监听事件
glfwSwapBuffers(window);
glfwPollEvents();
}
// 7. 释放
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shader.ID);
glfwTerminate();
return 0;
}
链接顶点属性
渲出来的东西奇奇怪怪,八成是这儿整叉劈了
void PrepareVertexAttriPtr(unsigned int* outVAO, unsigned int* outVBO) {
float vertices[] = {
0.5f, 0.5f, 0.0f, 1.0, 0.0, 0.0, 1.0, 1.0, // Top right
0.5f, -0.5f, 0.0f, 0.0, 1.0, 0.0, 1.0, 0.0,// Bot right
-0.5f, -0.5f, 0.0f, 0.0, 0.0, 1.0, 0.0, 0.0,// Bot left
-0.5f, 0.5f, 0.0f, 0.0, 1.0, 1.0, 0.0, 1.0 // Top left
};
GLint indices[] = {
0, 1, 3,
1, 2, 3
};
// 渲染模式 - States-setting
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 线框模式
//glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 填充模式(默认)
// 1. 绑定 VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
*outVAO = VAO;
// 2. 把 VAO 的数据复制到 VBO
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
*outVBO = VBO;
// 3. 把顶点索引复制到 EBO - EBO 被放在了 VAO 的末尾
GLuint EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设置顶点属性指针
// Position
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
// Color
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
// UV
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
// 5. 解绑
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
glVertexAttribPointer()
- 参数 1 指定要配置的顶点属性(跟 Shader 中的
layout (location = x)
对应) - 参数 2 指定顶点属性的大小(Vector 1,2,3,4?)
- 参数 3 指定数据的类型,这里是
GL_FLOAT
(GLSL中vec*都是由浮点数值组成的)。 - 参数 4 是否 Normalize(对于有符号型 signed 数据是 -1 ~1)
- 参数 5 步长 Stride,下一次轮到该数据中间间隔了的长度,头到头,看图更直观
- 参数 6 是偏移量 Offset,第一次出现时离 0 的距离,看图更直观。因为类型是 void*,要做个强制类型转换(虽然不懂为什么要整成这个类型)
读取纹理
void PrepareTexture(const char* texturePath, unsigned int* textureOut) {
// 1. 绑定纹理
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
*textureOut = texture;
// 2. 设置 wrap & filter
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 3. 加载纹理
int width, height, nrChannels;
// OpenGL - bottomY, texture - topY
stbi_set_flip_vertically_on_load(true);
unsigned char* data = stbi_load(texturePath, &width, &height, &nrChannels, 0);
// 4. 生成纹理单元和 MipMap
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
// 5. 释放
stbi_image_free(data);
}
纹理这里有两个需要设置的属性:Wrap Mode 和 Filter,引擎里见多了就不细写了
除了代码里的 GL_LINEAR
这种统一设置,还有类似 GL_LINEAR_MIPMAP_NEAREST
这样对 MipMap 区别对待的设置:使用 Nearest 的多级渐远纹理级别,并使用 Linear 进行采样
glTexImage2D()
- 参数 1 指定了纹理目标(Target)。除此之外还有
GL_TEXTURE_1D
和GL_TEXTURE_3D
- 参数 2 为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别
- 参数 3 告诉OpenGL我们希望把纹理储存为何种格式。还可以有
GL_RGBA
- 参数 4、5 设置最终的纹理的宽度和高度
- 参数 6 直接设 0(历史遗留问题)
- 参数 7、8 定义源图的格式和数据类型:使用 RGB 值加载,储存为 char(byte) 数组
- 参数 9 是图像数据
shader Program 设置
shader Program 由多个 Shader 合并链接(至少由一个 Vertex Shader 和 Fragment Shader 链接)
创建 Shader Program Object 的流程
// 1. 读取 Shader 文件
// iostream, fstream, sstream
// ...
// 2. 编译 Shader
unsigned int vertexShaderID, fragmentShaderID;
vertexShaderID= glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShaderID, 1, &vShaderCode, NULL);
glCompileShader(vertexShaderID);
checkCompileErrors(vertexShaderID, "VERTEX");
fragmentShaderID= glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShaderID, 1, &vShaderCode, NULL);
glCompileShader(fragmentShaderID);
checkCompileErrors(fragmentShaderID, "FRAGMENT");
// 3. 创建 Program
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
// 4. 链接 Shader 到 Program
glAttachShader(shaderProgramID, vertexShaderID);
glAttachShader(shaderProgramID, fragmentShaderID);
glLinkProgram(shaderProgramID);
checkCompileErrors(shaderProgramID, "PROGRAM");
// 5. 删除 Shader,链接完成,卸磨杀驴
glDeleteShader(vertexShaderID);
glDeleteShader(fragmentShaderID);
需要使用这个 Program 渲染时(包括设置 Uniform 参数时),要先调用 glUseProgram()
函数
渲染循环
向量和矩阵运算相关的不写了,回去看了看之前的笔记还是挺全的
void RenderLoop(Shader shader, unsigned int VAO, unsigned int texture0, unsigned int texture1 = NULL) {
// --清空屏幕--
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // State-setting
glClear(GL_COLOR_BUFFER_BIT); // State-using
// --属性更新--
// 1. Texture
// 提前绑定好纹理单元,并进行采样
shader.use();
shader.setInt("texture0", 0); //glUniform1i(glGetUniformLocation(shader.ID, "texture0"), 0);
shader.setInt("texture1", 1);
// 采样多个纹理时,需要手动 Active 后再绑定
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture0);
if (texture1) {
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture1);
}
// 2. Transform
// 新版本的 glm 矩阵需要手动初始化为单位矩阵
glm::mat4 transform = glm::mat4(1.0f);
// **** 实际变换顺序与代码相反:scale-rotate-translate
transform = glm::translate(transform, glm::vec3(0.1, -0.5 * sin((float)glfwGetTime()), 0.0));
transform = glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0, 0.0, 1.0));
transform = glm::scale(transform, glm::vec3(1.0, 1.0, 1.0));
// 将数据通过 Uniform 传递给 Shader,需要确保变量名称相同
// 实际调用的 OpenGL 函数见 Shader 类,或注释
shader.use(); // glUseProgram(shader.ID);
shader.setFloat("brightness", 1.5); // glUniform1f(glGetUniformLocation(shader.ID, "brightness"), 2.0);
unsigned int transformLoc = glGetUniformLocation(shader.ID, "transform");
// **** GL_TRUE 这里对矩阵做了个转置
glUniformMatrix4fv(transformLoc, 1, GL_TRUE, glm::value_ptr(transform));
glBindVertexArray(VAO);
// --绘制--
//glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}
传递纹理用的是 Uniform int,因为实际上直接传递的不是纹理数据,而是指向存放纹理数据的 ID
glfwGetTime()
:获取时间,玩具++;
这里有个问题,原文说 OpenGL 是列主序,glm 也是列主序,所以在传递变换矩阵的时候不需要做转置
但是我自己跑起来是需要转置的,竟然也没查到它们到底分别是什么主序,浪迹网络多年的搜索技能竟然败得这么彻底,多少有点离谱…
向 Shader 传值
向 Shader 传递顶点属性之外的其他数据时,需要借助全局变量 Uniform
Vertex Shader
// 使用 3.3 版本 OpenGL,Core 渲染模式
#version 330 core
// 顶点属性
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec3 vColor;
layout (location = 2) in vec2 vTexcoord;
// 需要确保和 Fragment Shader 的输入属性名称相同
out vec4 vertexColor;
out vec2 texcoord;
// 接收的全局变量
uniform mat4 transform;
void main()
{
gl_Position = vec4(vPosition, 1.0) * transform;
vertexColor = vec4(vColor, 1.0);
texcoord = vTexcoord;
}
Fragment Shader
#version 330 core
// 需要确保和 Vertex Shader 的输出属性名称相同
in vec4 vertexColor;
in vec2 texcoord;
out vec4 fragColor;
// 接收的全局变量
uniform float brightness;
uniform sampler2D texture0;
uniform sampler2D texture1;
void main()
{
//fragColor = texture(texture0, texcoord);
//fragColor *= vertexColor;
fragColor = mix(texture(texture0, texcoord), texture(texture1, texcoord), 0.5) * brightness;
}
mix()
,同 Lerp()
,实用小玩具
传递 Uniform
// 1. 根据名称获取 Uniform 的位置
GLint paramLoc = glGetUniformLocation(ID, "paramName");
// 2. 给该位置赋值
glUniform1i(paramLoc, value);
// 懒人二合一
glUniform1i(glGetUniformLocation(ID, "paramName"), value);
Shader 类
// 防止多次定义起冲突
#ifndef SHADER_H
#define SHADER_H
#include <glad/glad.h>;
#include <string>
class Shader
{
public:
unsigned int ID;
Shader(const char* vertexPath, const char* fragmentPath);
void use();
void setBool(const std::string& name, bool value) const;
void setInt(const std::string& name, int value) const;
void setFloat(const std::string& name, float value) const;
private:
void checkCompileErrors(unsigned int shader, std::string type);
};
#endif
#include "Shader.h";
#include <glad/glad.h>;
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
Shader::Shader(const char* vertexPath, const char* fragmentPath)
{
// 1. 读取 Shader 文件
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
vShaderFile.close();
fShaderFile.close();
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure& e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ: " << e.what() << std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
// 2. 编译 shader
unsigned int vertex, fragment;
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// 3. 删除 shader
glDeleteShader(vertex);
glDeleteShader(fragment);
}
// 激活 program
void Shader::use()
{
glUseProgram(ID);
}
// 设置 Uniform 更方便
void Shader::setBool(const std::string& name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void Shader::setInt(const std::string& name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void Shader::setFloat(const std::string& name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
// 检查编译和链接状态
void Shader::checkCompileErrors(unsigned int shader, std::string type)
{
int success;
char infoLog[1024];
if (type != "PROGRAM")
{
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n --- " << std::endl;
}
}
else
{
glGetProgramiv(shader, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n --- " << std::endl;
}
}
}