这次使用的是 GLFW 和 GLAD
GLFW 管理窗口和事件GLAD 管理版本glfwInit(); // 初始化
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 设置主版本为 3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 设置次版本为 3. MAJOR + MINOR 得到 3.3 版本
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心模式 Core-Profile
// 在 macOS 上启用向前兼容(Forward Compatibility),确保代码符合苹果平台的严格限制
// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // MacOS 系统使用
GLFW_OPENGL_PROFILE 设置 OpenGL 使用模式:核心模式 or 兼容模式
一般需要显示设置目标模式,部分驱动可能默认选择兼容模式
使用 glfwCreateWindow 创建窗口,使用 glfwMakeContextCurrent 设置窗口环境上下文,使用 glViewport 设置视口大小
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
glViewport(0, 0, 800, 600);
需要明确的是 窗口 和 视口 不是同一个东西
| 特性 | 窗口(Window) | 视口(Viewport) | | --- | --- | --- | | 创建者 | 操作系统(通过 GLFW 创建) | OpenGL(通过 glViewport 设置) | | 作用 | 显示图形、接收输入事件 | 定义渲染内容在窗口中的显示区域 | | 坐标系 | 屏幕像素坐标(如 (0,0) 是左上角或左下角) | 相对于窗口的像素坐标(左下角为原点) | | 动态调整 | 用户可拖动窗口改变大小 | 需手动调用 glViewport 更新 | | 多视口支持 | 一个窗口只能有一个 | 一个窗口可以设置多个视口(多视角渲染) |
这里只是恰好 glfwCreateWindow 创建的窗口和 glViewport 创建的视口大小一样
glViewport 前两个参数 (x, y) 表示视口左下角相对窗口左下角的位置偏移glViewport 后两个参数 (width, height) 表示视口宽高(像素)在窗口大小改变的时候,通常期望修改视口大小,可以通过注册回调函数来实现这个功能
// 定义回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
// 绑定回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
在使用 glViewPort 之前,需要先初始化 glad
glad 用于管理 OpenGL 的函数指针,所以在使用 OpenGL 的函数之前,一定要先初始化 glad
在 glfw、flad、窗口、视口 都创建完毕之后,就可以开始渲染循环(Render Loop)
// 每帧检查是否需要关闭窗口,不需要则进入渲染循环
while (!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window); // 交换缓冲区
glfwPollEvents(); // 检查触发的事件
}
glfwSwapBuffers 用于交换缓冲区,那么什么是缓冲区?为什么需要交换缓冲区?
缓冲区用于存储绘制生成的图像
由于图片不是一次性绘制好,而是一个像素一个像素的从左到右从上到下的绘制出来的,如果使用单缓冲区很容易出现图像闪烁的问题,并且由于最终图像不是瞬间显示出来,可能会导致渲染结果不真实
为了规避上面说的问题,一般采用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,所有渲染指令都在后缓冲上绘制
当所有渲染指令执行完毕之后,直接交换(swap)前后缓冲区,就能立刻得到最终图片,所以这个函数叫 glfwSwapBuffers
OpenGL默认启用双缓冲,可以通过glGetIntegerv函数检查GL_DOUBLEBUFFER的值是否为 1
当关闭窗口,准备退出程序的时候,需要释放/删除分配的所有资源,也就是在 main 函数退出的地方调用 glfwTerminate
glfwTerminate();
return 0;
使用 glfwGetKey 可以通过 window 判断是否触发按键
void ProcessInput(GLFWwindow* window)
{
// 监听 ECS 按键
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
使用 glClear 来清空屏幕,可以通过 glClearColor 设置清空屏幕所用的颜色
// 如果不需要关闭窗口,则持续进入循环
while (!glfwWindowShouldClose(window))
{
ProcessInput(window); // 检查按键触发
// 清空屏幕颜色 防止上一帧的内容影响这一帧的内容
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// TestRenderImGUI(window); // ImGUI 测试 不用管
glfwSwapBuffers(window); // 交换缓冲区
glfwPollEvents(); // 检查触发的事件
}
https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/#_3
顶点着色器的核心作用之一是将顶点的 3D 坐标 转换为另一种 3D 坐标(通常是 裁剪空间坐标)。这个过程涉及多个坐标系的变换:
裁剪剪切体块之外的顶点后,剩余顶点的位置将标准化为称为 NDC(标准化设备坐标)的通用坐标系。
标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上
通过使用由 glViewport 函数提供的数据,进行视口变换(Viewport Transform),标准化设备坐标(Normalized Device Coordinates)会变换为屏幕空间坐标(Screen-space Coordinates)。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中
顶点着色器除了处理位置坐标之外,还能对顶点的其他属性进行初步计算,比如
顶点着色器只能处理单个顶点的数据,无法直接访问其他顶点的信息
顶点着色器必须输出 gl_Position 裁剪空间坐标,其他属性通过 out 传递到后续阶段
顶点着色器代码大致如下
#version 450 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec4 inColor;
layout(location = 2) in vec2 texCoord;
layout(location = 3) in float texIndex;
uniform mat4 u_MVP;
out vec2 v_TexCoord;
out vec4 v_Color;
out float v_TexIndex;
void main() {
gl_Position = u_MVP * vec4(position, 1.0);
v_TexCoord = texCoord;
v_TexIndex = int(texIndex);
v_Color = inColor;
}
在 OpenGLStudy 中有详细解释过 VertexArray、VertexBuffer、IndexBuffer,以及如何将数据传递给 OpenGL 以及顶点着色器
在 OpenGLStudy 项目中,有对 Texture 贴图的封装
void Texture::Init(const std::string& filePath)
{
m_FilePath = filePath;
stbi_set_flip_vertically_on_load(1);
m_LocalBuffer = stbi_load(filePath.c_str(), &m_Width, &m_Height, &m_BPP, 4);
GL_CALL(glGenTextures(1, &m_RenderID));
GL_CALL(glBindTexture(GL_TEXTURE_2D, m_RenderID));
// 一定要设置的纹理 否则只能得到黑色纹理
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); // 指定缩小器
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); // 指定放大器
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
// 传递数据 GL_RGBA8 后面加8 用与表示每个通道站多少位
GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Width, m_Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_LocalBuffer));
GL_CALL(glBindTexture(GL_TEXTURE_2D, 0));
if (m_LocalBuffer) {
stbi_image_free(m_LocalBuffer);
m_LocalBuffer = nullptr;
}
}
这里对贴图进行设置时,涉及到了几个设置 GL_TEXTURE_WRAP_T、GL_TEXTURE_WRAP_S、GL_TEXTURE_MAG_FILTER、GL_TEXTURE_MIN_FILTER
根据 docs.gl 对 glTexParameteri 函数的解释,对于贴图的设置还有很多种
| 贴图设置的参数名称 | 作用 | 可选值 | 默认值 |
|---|---|---|---|
| GL_TEXTURE_MIN_FILTER | 设置纹理缩小(远距离)时的采样方式 | GL_NEAREST, GL_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR | GL_NEAREST_MIPMAP_LINEAR |
| GL_TEXTURE_MAG_FILTER | 设置纹理放大(近距离)时的采样方式 | GL_NEAREST, GL_LINEAR | GL_LINEAR |
| GL_TEXTURE_WRAP_S/T/R | 设置纹理坐标在 S/T/R 轴超出 [0,1] 时的处理方式 | GL_REPEAT, GL_MIRRORED_REPEAT, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_BORDER | GL_REPEAT |
| GL_TEXTURE_BASE_LEVEL | 设置纹理 Mipmap 的最小使用级别(0 表示最高分辨率) | 非负整数 | 0 |
| GL_TEXTURE_MAX_LEVEL | 设置纹理 Mipmap 的最大使用级别 | 非负整数 | 000(实际由硬件决定) |
| GL_TEXTURE_MIN_LOD | 设置纹理细节级别(LOD)的最小值 | 浮点数 | 1000(允许最低细节) |
| GL_TEXTURE_MAX_LOD | 设置纹理细节级别(LOD)的最大值 | 浮点数 | 1000(允许最高细节) |
| GL_TEXTURE_LOD_BIAS | 设置 LOD 的偏移值(正值为模糊,负值为锐利) | 浮点数 | 0.0 |
| GL_TEXTURE_COMPARE_MODE | 设置深度/模板纹理的比较模式 | GL_NONE(禁用比较), GL_COMPARE_REF_TO_TEXTURE(启用比较) | GL_NONE |
| GL_TEXTURE_COMPARE_FUNC | 设置深度比较函数(需与 GL_COMPARE_REF_TO_TEXTURE 配合使用) | GL_LEQUAL, GL_GEQUAL, GL_LESS, GL_GREATER, GL_EQUAL, GL_ALWAYS 等 | GL_LEQUAL |
| GL_DEPTH_STENCIL_TEXTURE_MODE | 设置深度/模板纹理的采样模式(返回深度值还是模板值) | GL_DEPTH_COMPONENT, GL_STENCIL_INDEX | GL_DEPTH_COMPONENT |
| GL_TEXTURE_SWIZZLE_R/G/B/A | 设置纹理颜色通道的映射关系(重新排列或复制通道) | GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA, GL_ZERO, GL_ONE | 各通道对应自身(如 R→R) |
GL_TEXTURE_MIN_FILTER 和 GL_TEXTURE_MAG_FILTER 表示贴图放大缩小时,采用的纹理过滤方式
| GL_NEAREST | GL_LINEAR |
| --- | --- | --- |
| 中文 | 临近过滤 | 线性过滤 |
| 作用 | 选择离中心点最接近的纹理坐标的像素 | 基于纹理坐标附近的纹理像素,计算插值 |
| 表现效果 |
|
|
对比 GL_NEAREST 和 GL_LINEAR 在图片放大之后的表现效果,很明显发现 GL_NEAREST 锯齿感明显,GL_LINEAR 更加自然
贴图设置的 GL_TEXTURE_WRAP_S/T/R 的 S/T/R 对应可以理解为 x、y、z,这个设置的作用就是当纹理坐标超过 [0, 1] 的取值范围之后,应该如何处理
贴图设置的 GL_TEXTURE_SWIZZLE_R/G/B/A 用于通道映射
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_RED)
通过上述代码的设置,读取贴图的 alpha 通道的时,从 r 通道读取
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_RED);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
通过上述代码的设置,读取贴图的 RGB 都是通过 R 通道获取值,比如 R 值为 0.8, A 值为 1.0,最后得到的就是 (0.8, 0.8, 0.8, 1.0) 的灰度值
在游戏运行过程中,物体有远有近
现在有一个大房子,用了一张 1024 * 1024 的贴图,在近距离观看细节丰富,但是远距离一般不需要这么丰富的细节,为了性能考虑可以把贴图换成 512 * 512;如果距离再远一些,即使使用 256 * 256 的贴图效果也不差
为了运行时性能,会根据举例的远近使用不同尺寸的贴图,这就是 MipMap 多级渐远纹理
在 OpenGL 中可以使用 glGenerateMipmap 函数来创建多级纹理,而不需要自己手动创建
| 过滤方式 | 描述 | | --- | --- | | GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 | | GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 | | GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 | | GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
注意,只有在
GL_TEXTURE_MIN_FILTER也就是缩小的时候才设置MipMap,GL_TEXTURE_MAG_FILTER放大的时候设置无效
在 OpenGLStudy的着色器 中使用 uniform sampler2D u_Texture 来接收一个贴图
但是在代码中却使用 glUniform1i 设置贴图为 0
m_shader->SetUniform1i("u_Texture", 0);
这是因为,这里设置的 0 并不是指贴图自身,而是贴图的槽位,也被称为 纹理单元
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
使用 glActiveTexture 激活指定的纹理单元之后,使用 glBindTexture 会将贴图绑定到当前激活的纹理单元中
OpenGL 至少保证有 16 个纹理单元,根据机器不同有的可以支持 32 个,也就是支持 GL_TEXTURE0 ~ GL_TEXTURE15,可以写成 GL_TEXTURE0 + Index 的方式
GL_TEXTURE8等价于GL_TEXTURE0 + 8