10错误处理

如何自动化地捕获 OpenGL 的报错。由于 OpenGL 是一个状态机且基于 C 语言 API,它不会像现代 C++ 那样抛出异常,所以需要特殊的调试技巧。

传统方案:glGetError#

  • 现状:OpenGL 的函数执行失败时,屏幕通常只是黑屏,没有任何提示。
  • 原始手段:调用 glGetError()。它会返回一个错误代码(如 1280 代表 GL_INVALID_ENUM)。
  • 痛点:这个函数只能一次返回一个错误。如果产生了多个错误,你得在一个 while 循环里不断调用它才能清空错误队列。

进阶方案:封装 GLClearErrorGLLogCall#

  • 思路:为了不让主代码被 glGetError 淹没,Cherno 进行了宏封装。
  • GLCheckError():在执行 OpenGL 指令调用,用来清空之前的旧错误。
  • GLLogCall():在指令执行调用,检查是否有新错误产生,并打印出文件名行号具体函数名

神奇的宏:ASSERT# 运算符#

  • 自动化:定义一个 GLCall(x) 宏。

    #define GLCall(x) GLClearError();\
        x;\
        ASSERT(GLLogCall(#x, __FILE__, __LINE__))
  • 技巧

    • #x:将代码内容转为字符串。
    • __FILE____LINE__:C++ 内置宏,自动获取报错位置。
    • ASSERT:如果报错,直接让程序停在出问题的那一行,而不是跑飞。

现代方案:glDebugMessageCallback#

视频没讲到这个

  • 新特性:在 OpenGL 4.3+ 版本中引入。
  • 优势:这是“被动式”报错。你只需在初始化时设置一个回调函数,一旦 OpenGL 出错,驱动程序会自动跳进你的函数。
  • 局限性:虽然更现代,但某些旧显卡驱动支持不佳。Cherno 推荐在学习阶段使用 GLCall 宏,因为它兼容性最强且更直观。

实战演示:故意写错代码#

  • 验证:Cherno 故意在 glDrawElements 里传了一个错误的枚举值。
  • 结果:控制台瞬间精准报出了:
    1. 哪个函数错了(glDrawElements)。
    2. 在哪个文件哪一行。
    3. 错误代码是什么。

💡 博客总结(金句/重点)#

知识点关键点
为什么 OpenGL 不报错?它是 C 风格 API,不提供异常机制,需手动查询状态。
glGetError 特点错误码是累积的,必须循环读取直到返回 GL_NO_ERROR
GLCall 宏的作用它可以包裹任何 gl 函数,实现一站式“清空-执行-检查”。
调试效率配合 __debugbreak()(Windows 特有)可以让调试器直接定位到崩掉的那行源码。

在发布版本(Release)中通常要关掉这些报错检查。因为频繁地通过总线询问 GPU 状态会严重拖慢帧率(FPS)。

09顶点缓冲区

总述#

核心痛点:重复的顶点数据#

  • 现状分析:绘制一个矩形需要两个三角形(6 个顶点)。
  • 资源浪费:矩形只有 4 个角,使用 glDrawArrays 必须重复定义其中 2 个顶点(坐标、纹理、颜色等完全一致)。
  • 影响:随着模型复杂化,这种冗余会成倍消耗显存(VRAM)和带宽。

解决方案:Index Buffer 原理#

  • 定义:索引缓冲区(IBO/EBO)存储的是指向顶点数组的整数索引
  • 逻辑
    1. Vertex Buffer:只存储 4 个唯一的顶点坐标。
    2. Index Buffer:存储顺序(如 0, 1, 2, 2, 3, 0),告诉 GPU 如何连接这些点。
  • 优势:一个浮点数顶点属性通常 12-32 字节,而一个索引(unsigned intshort)仅需 4 或 2 字节。

编码实践:创建与绑定 IBO#

  • 创建对象glGenBuffers(1, &ibo);

  • 绑定目标:必须指定为 GL_ELEMENT_ARRAY_BUFFER

  • 填充数据

    unsigned int indices[] = { 0, 1, 2, 2, 3, 0 };
    
    
    //索引缓冲区:index buffer object
    unsigned int ibo;
    // 生成一个缓冲区 ID
    glGenBuffers(1, &ibo);
    // 绑定该 ID 到元素数组缓冲区 (Element Array Buffer)插槽,
    //也称索引缓冲区对象
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
    // 将顶点数据从 CPU 内存拷贝到 GPU 显存,设为静态读取模式
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 *   sizeof(unsigned int),
    	indices, GL_STATIC_DRAW);
  • 注意:索引必须是正整数,且建议使用 unsigned int 以保证最大兼容性。

08处理着色器

主要是添加了struct ShaderProgramSourcestatic ShaderProgramSource ParseShader(const std::string& filepath)

#ifdef LY_EP08
#include <iostream>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <fstream>
#include <sstream>
#include <string>

struct ShaderProgramSource
{
	std::string VertexSource;
	std::string FragmentSource;
};

static ShaderProgramSource ParseShader(const std::string& filepath)
{

	enum class ShaderType
	{
		NONE = -1,VERTEX = 0,FRAGMENT =1
	};

	//input file stream
	std::ifstream stream(filepath);

	std::string line;
	std::stringstream ss[2];
	ShaderType type = ShaderType::NONE;
	
	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() };

}

//GLenum也就是unsigned int,这里不用GLenum是为了解耦
static unsigned int CompileShader(unsigned int type,
	const std::string& source)
{
	//创建着色器对象,向 OpenGL 申请一个空的“容器”来存放你的代码
	//把光标放在 glCreateShader( 的左括号后面,然后按下 Ctrl + Shift + Space。会弹出一个小黑框,显示:GLuint glCreateShader(GLenum type)
	//到glew.h搜索,发现typedef unsigned int GLenum;
	unsigned int id = glCreateShader(type);
	//返回一个指向该字符串首地址的只读指针(const char*)
	const char* src = source.c_str();

	//有了容器后,你需要把写好的字符串代码塞进去(拷贝)
	//3种用法
	//1. glShaderSource(id, 1, &src, nullptr);	只有一个字符串,且它是以 \0 结尾的。
	//2. glShaderSource(id, 1, &src, &len);	只有一个字符串,长度由 len 指定。
	//3. glShaderSource(id, 2, strings, lengths);	有两个字符串片段(数组形式),长度分别由 lengths[0] 和 lengths[1] 指定。
	glShaderSource(id, 1, &src, nullptr);

	//将你的 GLSL 代码翻译成显卡能理解的机器指令
	glCompileShader(id);

	//错误处理
	int result;

	// 查询编译状态:询问 OpenGL 这个 shader 编译成功了吗?
	// GL_COMPILE_STATUS 会把结果存入 result 中(成功为 GL_TRUE,失败为 GL_FALSE)
	glGetShaderiv(id, GL_COMPILE_STATUS, &result);

	if (result == GL_FALSE)
	{
		int length;
		//获取错误日志长度:如果编译失败,先问一下错误信息一共有多少个字符
		glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
		//在栈上申请内存
		//如果申请的内存太大,alloca会导致栈溢出(1MB)
		char* message = (char*)alloca(length * sizeof(char));
		//提取错误信息:把显卡驱动里的具体报错文字拷贝到我们刚才申请的 message 内存中
		glGetShaderInfoLog(id, length, &length, message);
		std::cout << "Failed to compile " << ((type == GL_VERTEX_SHADER) ? "vertex" : "fragment") << " shader!";
		std::cout << message << std::endl;
		//清理:编译失败了,这个 shader 对象也就没用了,删掉它防止内存泄漏
		glDeleteShader(id);
		return 0;
	}


	return id;
}

static unsigned int CreateShader(const std::string& vertexShader,
	const std::string& fragmentShader)
{
	//在 GPU 中申请一个空的“程序对象”
	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);
	//验证当前的程序是否能在当前的 OpenGL 状态下执行(
	// 检查顶点着色器的输出是否与片段着色器的输入匹配,并
	// 生成最终的可执行二进制代码。)
	glValidateProgram(program);

	//删除临时的中间产物(类似编译完 C++ 后删除 .obj
	glDeleteShader(vs);
	glDeleteShader(fs);


	return program;
}

int main(void)
{
#pragma region 一些初始化
	GLFWwindow* window;
	// 初始化 GLFW 库,失败则退出
	if (!glfwInit())
		return -1;

	//强制指定使用 Core Profile(核心模式),如果
	//没有手动写着色器则不会渲染;如果不是核心模式,
	//在固定管线中默认颜色是白色,且默认顶点在NDC标准
	//设备坐标系中
	//glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	//glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	//glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);

	// 创建窗口对象
	window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
	if (!window)
	{
		// 创建失败则清理并退出
		glfwTerminate();
		return -1;
	}

	// 将当前窗口的上下文设置为 OpenGL 渲染的目标
	glfwMakeContextCurrent(window);

	// 初始化 GLEW 以加载 OpenGL 函数指针,需在有上下文后执行
	if (glewInit() != GLEW_OK)
	{
		std::cout << "Error!" << std::endl;
	}

#pragma endregion

	// 定义三角形的顶点坐标(CPU 内存)
	float positions[6] = {
		-0.5f, -0.5,
		0.0f, 0.5f,
		0.5f, -0.5f,
	};

	unsigned int buffer;
	// 生成一个缓冲区 ID
	glGenBuffers(1, &buffer);
	// 绑定该 ID 到顶点缓冲区插槽
	glBindBuffer(GL_ARRAY_BUFFER, buffer);
	// 将顶点数据从 CPU 内存拷贝到 GPU 显存,设为静态读取模式
	glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float),
		positions, GL_STATIC_DRAW);


	// 启用索引为 0 的(顶点)属性 
	glEnableVertexAttribArray(0);


	//1. 打标签:它把当前 GL_ARRAY_BUFFER 里的数据流,贴上了“0号”的标签。2. 定规则:它告诉 GPU,当你(Shader)想要 location = 0 的数据时,请按照“每 2 个 float 为一组”的规则去缓存里抓取。
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, (const void*)0);
	 
	//这是一个相对路径,相对于 工作目录
	//如果不是在visual studio中运行,就会相对于 可执行文件 所在的目录
	//如果在visual studio中,工作目录被设置为$(ProjectDir) (右键目录-属性-调试-woking directory)
	ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");

	std::cout << source.VertexSource << std::endl;
	std::cout << source.FragmentSource << std::endl;

	// 将字符串源码编译并链接成一个完整的程序对象
	unsigned int program = CreateShader(source.VertexSource, source.FragmentSource);
	// 告诉 OpenGL 状态机:接下来的所有绘制指令(如 glDrawArrays)
	// 都请使用这个编译好的着色器程序进行渲染
	glUseProgram(program);


	// 游戏/渲染主循环
	while (!glfwWindowShouldClose(window))
	{
		// 清理屏幕颜色缓冲区
		glClear(GL_COLOR_BUFFER_BIT);

		// 读取当前绑定的缓冲区数据并绘制三角形:从
		// 索引0即第1个顶点开始画,读取三个顶点(
		//并行地读第1个顶点的0号属性_以及_第1个顶点的1号属性) 
		glDrawArrays(GL_TRIANGLES, 0, 3);

		// 交换前后缓冲区以刷新画面
		glfwSwapBuffers(window);

		// 轮询并处理窗口事件(如键盘输入、关闭动作)
		glfwPollEvents();
	}

	// 手动通知显卡驱动释放该程序占用的显存
	glDeleteProgram(program);

	// 退出前清理资源
	glfwTerminate();
	return 0;
}

#endif

着色器

07编写一个着色器

概述#

编写最基础的顶点着色器 (Vertex Shader)片段着色器 (Fragment Shader),并将它们链接成一个可以在显卡上运行的着色器程序。

  • 课程引入 回顾上一集关于着色器的理论概念,明确本集目标:编写实际代码让上一集绘制的三角形呈现特定的颜色(不再是默认的白色或黑色)。
  • 创建基础字符串结构 演示如何在 C++ 中以字符串形式编写 GLSL 代码。由于目前还没有文件读取系统,Cherno 直接在 main 函数中使用原始字符串(Raw Strings)来定义顶点和片段着色器的源码。
  • 编写顶点着色器 (Vertex Shader)
    • 指定版本:#version 330 core
    • 定义输入属性:layout(location = 0) in vec4 position;
    • 主函数 main:将输入的坐标赋值给内置变量 gl_Position
  • 编写片段着色器 (Fragment Shader)
    • 定义输出颜色:layout(location = 0) out vec4 color;
    • 主函数 main:为像素指定颜色值(例如红色 vec4(1.0, 0.0, 0.0, 1.0))。
  • 创建编译与链接函数 CreateShader 开始实现逻辑:如何将上述两个字符串交给 OpenGL。
    • 调用 glCreateProgram 创建一个程序容器。
    • 调用自定义的 CompileShader 函数分别编译顶点和片段部分。
  • 实现 CompileShader 函数 这是核心步骤:
    1. glCreateShader:根据类型创建着色器对象。
    2. glShaderSource:将 C++ 字符串传递给 OpenGL。
    3. glCompileShader:调用显卡驱动进行编译。
  • 错误处理与调试 (Shader Debugging) 这是新手最容易卡住的地方。Cherno 详细演示了如何使用 glGetShaderiv 检查编译状态,并用 glGetShaderInfoLog 打印出显卡报错信息。
  • 链接着色器程序 (Linking) 使用 glAttachShader 将编译好的两个着色器附加到程序上,然后调用 glLinkProgram 进行链接,最后用 glValidateProgram 验证。
  • 清理工作 链接成功后,调用 glDeleteShader 删除临时的中间产物(类似编译完 C++ 后删除 .obj 文件),以节省资源。
  • 实际运行测试 将编写好的 CreateShader 函数集成到渲染循环之前。
  • 结果展示与总结 屏幕上成功出现了一个红色的三角形。Cherno 总结了着色器在现代渲染管线中的重要角色。

学习要点提示:

06着色器原理

什么是着色器 (Shaders)#

着色器本质上是运行在 GPU(显卡)上的程序。在 OpenGL 绘图流程中,我们需要在 GPU 上运行代码来处理我们发送过去的顶点数据

  • 当你调用 glCompileShader(id) 时,显卡驱动(运行在 CPU 上)会获取这个字符串。显卡驱动里内置了一个编译器。它会将你的 GLSL 源代码翻译成该显卡硬件能听懂的机器码(微指令)。由于不同品牌的显卡(NVIDIA, AMD, Intel)底层指令集完全不同,所以 Shader 必须在你的电脑上“现场编译”,而不能像 C++ 那样预先编译成 .exe。编译好的二进制指令会被发送并存储在 GPU 的显存中。当你执行 glDrawArrays 时,GPU 会激活这些指令,大规模并行地处理你传进去的顶点数据。
  • 我们需要显卡的能力在屏幕上绘制图形,所以需要程序完全在显卡上运行,当然部分代码需要在CPU上运行
  • 着色器的本质,就是告诉GPU如何处理CPU发送给他的数据

渲染管线概述 (The Graphics Pipeline)#

解释了数据从 CPU 发送到 GPU 后的处理过程渲染管线是一系列将 3D 坐标转换为屏幕上 2D 像素的步骤。

渲染管道:在CPU上写了一堆数据,之后向显卡发送了一些数据,然后发送了DrawCall指令(发送指令前绑定了一些状态),最后进入了着色器的阶段,GPU实际处理DrawCall指令并在屏幕上绘制一些东西

着色器#

顶点着色器和片段着色器是顺着管道的两种不同的着色器类型,发出DrawCall指令后,顶点着色器被执行,然后再经过一些阶段后 是片段着色器

顶点着色器 把 3D 空间的坐标转换成屏幕上的 2D 坐标(术语叫标准化设备坐标) ->几何着色器 增减图形:可以把一个点变成一个三角形,或者把一个三角形删掉。这里,几何着色器根据这 3 个点,计算出了第 4 个点的位置,并把它们重新组合。输出:它“发射”出了足够的顶点,组成了两个独立的三角形图元。 ->图元组装 把处理好的点“连线” ->光栅化 计算这个三角形到底盖住了屏幕上哪些像素格子 ->片段着色器 根据光照、纹理、材质,给这个像素算出一个最终的 RGBA 值 ->测试与混合 深度测试 (Depth Test):检查这个像素是不是被别的物体挡住了。模板测试:一些特殊遮罩效果。混合 (Blending):如果是半透明的,就把它和背景颜色融合

05顶点属性和内存布局

在第 04 集里你把数据塞进了显存,但 GPU 面对那一串二进制数字是“瞎”的。这一集的核心就是给数据贴标签,告诉 GPU 如何读取这些坐标。

  • 问题的引入:为什么屏幕还是黑的?
    • 数据(Positions)已经在 VRAM 里了,但 GPU 不知道这堆 float 代表什么。
    • 是一组 坐标?还是 颜色?或者是贴图坐标?
    • 我们需要一份“说明书”来定义数据的布局 (Layout)
  • 核心函数:glVertexAttribPointer 详解
    • 这是 OpenGL 中参数最复杂的函数,Cherno 逐一拆解了这 6 个参数:
      1. index: 属性的索引(例如:0号属性给位置,1号属性给颜色,2号属性给纹理)。
      2. size: 每个属性包含几个分量(位置是3,纹理是2)。
      3. type: 数据类型(GL_FLOAT)。
      4. normalized: 是否归一化(通常选 GL_FALSE)。
      5. stride (步长): 最关键参数。从某个属性的起始点下一个属性起始点的字节数。(这里第一组到第二组,位置index从0->5,纹理index从3->8,这里是5 * sizeof(float)
      6. pointer (偏移量): 在单个顶点数据块内,该属性距离(这个块的)起始位置的字节偏移。
//这里举一个特殊的例子,LearnOpenGL中的
float vertices[] = {
-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
 0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
 };
 
//配置位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); 

//配置纹理属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
//第二个2,和上面第一个参数,也就是 vs文件中的location一致,
//表示要启用 2 号属性(对应纹理)
glEnableVertexAttribArray(2);
  • 内存布局的可视化演示
    • Cherno 使用画图工具展示了数据在内存中是如何排列的。
    • 如果每个顶点只有 2 个 float 坐标,那么 Stride 就是 2 * sizeof(float)
    • 如果以后加上纹理坐标(又多 2 个 float),Stride 就会变成 4 * sizeof(float)
  • 启用顶点属性 (glEnableVertexAttribArray)
    • 仅仅定义了“说明书”还不够,必须通过这个函数手动开启对应的属性索引(本例中是 0)。
    • 默认情况下,为了性能,所有属性槽位都是关闭的。
  • 链接缓冲区与属性
    • 强调了一个隐式逻辑:glVertexAttribPointer 关联的是当前绑定在 GL_ARRAY_BUFFER 上的那个 ID
    • 这再次体现了 OpenGL “状态机”的特性。
  • 总结与运行结果
    • 加上这两行关键代码后,配合默认或简单的着色器,屏幕上终于能画出三角形的轮廓了。
    • 预告:下一集将进入 Shader (着色器) 的世界,那是真正赋予图形灵魂的地方。
  • 核心操作流程(接续你的代码)

在你的 glBufferData 之后,必须补充这两行,否则 glDrawArrays 找不到数据:

04顶点缓冲区和绘制三角形

总述#

这一集(时长 20:06)标志着你正式从“配置环境”跨越到了“图形编程”。这里讲解了现代 OpenGL 的灵魂:如何把 CPU 内存里的数据塞进 GPU 显存里

  • 什么是顶点缓冲区 (Vertex Buffer / VBO)?

    • 核心定义:它本质上是 GPU 显存中的一块内存区域,用于存储顶点的各种数据(坐标、颜色等)。
    • 为什么要用它?:CPU 到 GPU 的总线传输相对较慢。我们希望一次性把大量数据推送到显存,然后让 GPU 在内部高速读取,而不是每帧都由 CPU “手把手”教 GPU 怎么画。
  • 定义顶点数据

    • 在 C++ 中创建一个浮点数数组 float positions[6],存入三个顶点的 坐标。
    • 注意:此时这些数据还在 RAM(内存) 中,GPU 还看不见它们。
  • 生成缓冲区 ID (glGenBuffers)

    • OpenGL 是基于状态机的。我们不直接操作对象指针,而是操作 ID (unsigned int)
    • 调用 glGenBuffers(1, &buffer):向 OpenGL 申请一个 ID,代表我们要创建的一个缓冲区。
  • 绑定缓冲区 (glBindBuffer)

    • 这是初学者最容易晕的地方。OpenGL 像是一个只有一个插槽的播放器。
    • 调用 glBindBuffer(GL_ARRAY_BUFFER, buffer):告诉 OpenGL,“从现在起,所有关于 GL_ARRAY_BUFFER 的操作,都请针对我刚刚申请的这个 buffer 进行”。
  • 填充数据 (glBufferData)

    • 这是真正发生“搬运”动作的代码。
    • 参数解释:
      1. 目标:GL_ARRAY_BUFFER
      2. 大小:6 * sizeof(float)
      3. 数据指针:positions
      4. 使用方式:GL_STATIC_DRAW(告诉显卡:这些数据我画一次后基本不动了,请优化存储)。
  • 为什么屏幕上还是没东西?

01-03基础配置

01-03基础配置#

[16:04] 01-欢迎来到 OpenGL-Welcome to OpenGL
[22:03] 02-在 C++ 中设置 OpenGL 并创建窗口-Setting up OpenGL and Creating a Window in C++
[18:21] 03-在 C++ 中使用现代 OpenGL-Using Modern OpenGL in C++
[20:06] 04-顶点缓冲区与在 OpenGL 中绘制三角形-Vertex Buffers and Drawing a Triangle in OpenGL
[18:54] 05-OpenGL 中的顶点属性与布局-Vertex Attributes and Layouts in OpenGL
[17:37] 06-OpenGL 中的着色器工作原理-How Shaders Work in OpenGL
[28:21] 07-在 OpenGL 中编写着色器-Writing a Shader in OpenGL
[21:15] 08-我是如何处理 OpenGL 中的着色器的-How I Deal with Shaders in OpenGL
[16:54] 09-OpenGL 中的索引缓冲区-Index Buffers in OpenGL
[23:42] 10-处理 OpenGL 中的错误-Dealing with Errors in OpenGL
[11:27] 11-OpenGL 中的 Uniform 变量-Uniforms in OpenGL
[21:50] 12-OpenGL 中的顶点数组-Vertex Arrays in OpenGL
[26:46] 13-将 OpenGL 抽象为类-Abstracting OpenGL into Classes
[30:07] 14-OpenGL 中的缓冲区布局抽象-Buffer Layout Abstraction in OpenGL
[21:56] 15-OpenGL 中的着色器抽象-Shader Abstraction in OpenGL
[14:43] 16-在 OpenGL 中编写基础渲染器-Writing a Basic Renderer in OpenGL
[31:44] 17-OpenGL 中的纹理-Textures in OpenGL
[12:37] 18-OpenGL 中的混合-Blending in OpenGL
[18:07] 19-OpenGL 中的数学-Maths in OpenGL
[20:10] 20-OpenGL 中的投影矩阵-Projection Matrices in OpenGL
[15:53] 21-OpenGL 中的 MVP 矩阵-Model View Projection Matrices in OpenGL
[14:36] 22-在 OpenGL 中使用 ImGui-ImGui in OpenGL
[12:21] 23-在 OpenGL 中渲染多个物体-Rendering Multiple Objects in OpenGL
[16:52] 24-为 OpenGL 设置测试框架-Setting up a Test Framework for OpenGL
[22:46] 25-在 OpenGL 中创建测试-Creating Tests in OpenGL
[28:13] 26-在 OpenGL 中进行纹理测试-Creating a Texture Test in OpenGL
[11:37] 27-如何让你的 UNIFORMS 更快-How to make your UNIFORMS FASTER in OpenGL
[12:25] 28-批量渲染:简介-Batch Rendering - An Introduction
[09:15] 29-批量渲染:颜色-Batch Rendering - Colors
[15:51] 30-批量渲染:纹理-Batch Rendering - Textures
[23:17] 31-批量渲染:动态几何体-Batch Rendering - Dynamic Geometry

01WelcomToOpengl#

  • 课程初衷与背景
    • 作者介绍了为什么要制作这个系列:市面上很多教程只教“怎么做”,而不教“为什么”。
    • 本系列的目标是不仅让你写出代码,还要让你理解 OpenGL 的底层工作原理及其与 GPU 的交互逻辑。
  • 什么是 OpenGL?
    • 核心定义:OpenGL 本质上是一个规范(Specification) 接口,API ,而不是一个具体的库。它规定了函数应该如何命名、参数是什么以及预期的行为允许我们实际访问我们的GPU,GraphicsProcessingUnit,图形处理单元。OpenGL是其中之一的接口,还有Direct3D,Vulkan,Metal等
    • 实现者具体的实现通常由显卡厂商(NVIDIA, AMD, Intel)编写在驱动程序中
    • 跨平台,Windows、Linux、Mac、Android、IOS
    • 状态机(State Machine):初步引入 OpenGL 是一个状态机的概念,你设置好状态(如颜色、缓冲区),然后发出指令进行渲染
  • 现代 OpenGL vs 传统 OpenGL
    • Legacy OpenGL (固定管线):简单但不够灵活,很多功能已经废弃(如 glBegin, glEnd)。
    • Modern OpenGL (可编程管线):通过 着色器 (Shaders) 控制渲染过程。虽然代码量显著增加,但提供了极大的灵活性和性能优化空间。
    • Shader(着色器)是程序,在==GPU==上运行的代码程序
    • 本系列将专注于 Modern OpenGL (版本 3.3 及以上)
  • 学习 OpenGL 的难点与心态
    • 强调了学习曲线:初期需要写==大量的“模板代码”(Boilerplate code)==才能在屏幕上画出一个简单的三角形。
    • 图形管线 (Graphics Pipeline):解释了数据如何从 CPU 传输到 GPU,并经过顶点处理、光栅化最终变成像素的过程。
  • 开发环境与工具链预告
    • 虽然是跨平台的,但本系列主要在 Windows 下使用 Visual Studio 演示。
    • 提到后续会使用的关键第三方库:GLFW(窗口管理)和 GLEW/GLAD(访问 OpenGL 扩展函数)。
  • 总结与后续计划
    • 鼓励初学者不要被前几集的复杂配置和概念吓退。
    • 下一集将正式进入 C++ 环境配置,动手创建第一个窗口。

02设置Opengl和创建窗口#

主要讲解如何在 Windows 环境下使用 Visual Studio 搭建 OpenGL 开发环境,并编写代码成功弹出一个窗口。这是所有图形编程的起点。

04HelloTriangle

你好,三角#

在 OpenGL 中,所有物体都处于三维空间,但屏幕或窗口是一个二维像素数组,因此 OpenGL 的大部分工作是将所有三维坐标转换为适合屏幕的二维像素。将三维坐标转换为二维像素的过程由 OpenGL 的*图形管线 graphics pipeline *管理。图形管线可以分为两大部分:==第一部分将三维坐标转换为二维坐标,第二部分将二维坐标转换为实际的彩色像素 第二部分第一步:把由坐标定义的几何形状,切割成数以万计的待处理像素(片段)。第二步才是填充像素 ==。本章将简要讨论图形管线以及如何利用它来创建精美的像素。

图形管线以一组三维坐标作为输入,并将其转换为屏幕上的彩色二维像素。图形管线可以分为多个步骤,每个步骤都需要前一步的输出作为输入 串行,阶段性依赖 。所有这些步骤都高度专业化(每个步骤都具有特定的功能),并且可以轻松地并行执行 同一个步骤并行 。由于其并行特性,如今的显卡拥有数千个小型处理核心,以便在图形管线中快速处理数据。这些处理核心在 GPU 上运行小型程序,用于执行管线的每个步骤。这些小型程序被称为着色器

并行还有另一个意思:当第一批顶点处理完进入“光栅化”车间时,顶点车间并不需要闲着。它可以立刻开始处理下一批模型或者下一帧的顶点

开发者可以配置部分着色器,从而编写自定义着色器来替换现有的默认着色器。这使我们能够对渲染管线的特定部分进行更精细的控制,并且由于它们运行在 GPU 上,因此还可以节省宝贵的 CPU 时间。着色器使用 OpenGL 着色语言 (GLSL) ~~OpenGL Shading Language ~~ 编写,我们将在下一章中深入探讨。

下方是图形管线所有阶段的抽象表示。请注意,蓝色部分表示我们可以注入自定义着色器的部分。

顶点着色器 把 3D 空间的坐标转换成屏幕上的 2D 坐标(术语叫标准化设备坐标) ->几何着色器 增减图形:可以把一个点变成一个三角形,或者把一个三角形删掉。这里,几何着色器根据这 3 个点,计算出了第 4 个点的位置,并把它们重新组合。输出:它“发射”出了足够的顶点,组成了两个独立的三角形图元。 ->图元组装 把处理好的点“连线” ->光栅化 计算这个三角形到底盖住了屏幕上哪些像素格子 ->片段着色器 根据光照、纹理、材质,给这个像素算出一个最终的 RGBA 值 ->测试与混合 深度测试 (Depth Test):检查这个像素是不是被别的物体挡住了。模板测试:一些特殊遮罩效果。混合 (Blending):如果是半透明的,就把它和背景颜色融合

如您所见,图形管线包含大量模块 应该说的是这六个 ,每个模块负责将顶点数据转换为完整渲染像素的特定 某个步骤 步骤。我们将以简化的方式简要解释管线的每个部分,以便您对管线的工作原理有一个大致的了解。

作为图形管线的输入,我们传入一个名为 Vertex Data 的数组,其中包含三个 3D 坐标,它们应该构成一个三角形;这个顶点数据是一个顶点集合。每个顶点都是一个包含 3D 坐标数据的集合。顶点数据使用顶点属性来表示,这些属性可以包含我们想要的任何数据,但为了简单起见,我们假设每个顶点仅包含一个 3D 位置和一个颜色值

105弱指针

共享指针与唯一指针例子#

#ifdef LY_EP105
#include <iostream>
#include <memory>

class Entity {
public:
	Entity() { std::cout << "Entity Created" << std::endl; }
	~Entity() { std::cout << "Entity Destroyed" << std::endl; }
};

int main() {
	// --- unique_ptr 示例 ---
	{
		std::unique_ptr<Entity> e1 = std::make_unique<Entity>();
		// std::unique_ptr<Entity> e2 = e1; // 错误!禁止复制
		std::unique_ptr<Entity> e2 = std::move(e1); // 允许移动所有权,e1 现在为空
	} // e2 离开作用域,Entity 在这里被销毁

	std::cout << "-----------------" << std::endl;

	{
		// --- shared_ptr 示例 ---
		std::shared_ptr<Entity> sharedOuter;
		{
			//std::shared_ptr对象实例内部有一个引用计数,简单记录有多少个共享指针指向内部Entity对象的实例
			std::shared_ptr<Entity> sharedInner = std::make_shared<Entity>();

			sharedOuter = sharedInner; // 允许复制(拷贝赋值函数),引用计数变为 2
			std::cout << "Inner scope ending1..." << std::endl;
			std::shared_ptr<Entity> sharedInner1 = std::move(sharedOuter);//这里把sharedInner的所有权移走了,计数减一,但是本身又导致计数加一,所以目前是2 

			std::cout << "Inner scope ending2..." << std::endl;
		} // sharedInner1 离开作用域,sharedOuter所有权被移走,所以没有任何共享指针指向Entity了,  Entity 被销毁 

		std::cout << "Outer scope still holds Entity?" << std::endl;
	} //作用域结束,sharedOuter 离开作用域,Entity 最终在这里被销毁

	std::cin.get();
}
/*
Entity Created
Entity Destroyed
-----------------
Entity Created
Inner scope ending1...
Inner scope ending2...
Entity Destroyed
Outer scope still holds Entity?
*/
#endif

共享指针、强引用,能防止对象销毁,也被称作具名引用。即他们对一个对象各自都拥有所有权