当前位置: 首页 > news >正文

OpenGL基础

OpenGL 入门复习笔记

1. CMake 和第三方库

OpenGL 入门通常会用到这些库:

  • GLFW:创建窗口、创建 OpenGL context、处理键盘鼠标输入。
  • GLAD:加载 OpenGL API 函数指针。
  • stb_image:读取图片文件,用于纹理。
  • glm:处理向量、矩阵、相机和投影变换。
  • spdlog:打印日志,方便排错。

一个容易混淆的点:

find_package(glfw3 REQUIRED)
target_link_libraries(BaseProj PRIVATE glfw)

这里的 glfw3glfw 不是写错了,它们表示的层级不同。

  • find_package(glfw3 REQUIRED) 里的 glfw3 是包名。
  • target_link_libraries(... glfw) 里的 glfw 是 CMake 导入目标名。
  • glfw3.lib 是磁盘上的真实库文件名。

这三个名字不一定相同。

判断 target_link_libraries 应该链接什么名字时,可以去安装目录下找 *Targets.cmake,例如:

D:/tools/CodeLib/GLFW/lib/cmake/glfw3/glfw3Targets.cmake

如果里面有:

# Create imported target glfw
add_library(glfw STATIC IMPORTED)

那就说明 CMake 导出的目标名是 glfw,所以应该写:

target_link_libraries(BaseProj PRIVATE glfw)

现代 CMake 里,target_link_libraries 优先链接的是“目标 target”,不是单纯盯着 .lib 文件名。这个 target 内部会记录真实库文件路径、头文件 include 路径以及其他依赖信息。

CMAKE_PREFIX_PATH

find_package 不是下载库,它只是找已经安装好的包。CMAKE_PREFIX_PATH 用来告诉 CMake 去哪些安装前缀目录下找包。

例如 GLFW 安装在:

D:/tools/CodeLib/GLFW

并且里面有:

D:/tools/CodeLib/GLFW/lib/cmake/glfw3/glfw3Config.cmake

那就可以把 D:/tools/CodeLib/GLFW 作为 prefix。

注意:

  • CMAKE_INSTALL_PREFIX:安装某个库时用,决定库安装到哪里。
  • CMAKE_PREFIX_PATH:使用某个库时用,告诉 CMake 去哪里找它。

GLAD 的引入方式

GLAD 通常没有单独的 .lib 文件,它一般由:

external/glad/include/glad/glad.h
external/glad/src/glad.c

组成。

所以 CMake 里需要做两件事:

  • glad.c 加入可执行目标,作为一个编译单元。
  • external/glad/include 加入 include 路径。

GLAD 的 include 顺序一般是:

#include <glad/glad.h>
#include <GLFW/glfw3.h>

GLAD 放在 GLFW 前面,避免系统 OpenGL 头文件先被包含造成冲突。

2. GLFW 窗口和 OpenGL Context

基本流程:

glfwInit
设置 OpenGL 版本
glfwCreateWindow
glfwMakeContextCurrent
gladLoadGLLoader
进入渲染循环
glfwTerminate

glfwMakeContextCurrent

glfwCreateWindow 创建窗口,也创建了这个窗口对应的 OpenGL context。但是 OpenGL API 调用本身通常不带 window 参数,例如:

glClear(...)
glDrawArrays(...)
glCreateShader(...)

OpenGL 怎么知道这些调用作用在哪个窗口上?

答案是:OpenGL 调用作用于当前线程的 current context。

glfwMakeContextCurrent(window);

意思是:

把这个 window 对应的 OpenGL context 设置为当前线程正在使用的 context。

所以 GLAD 必须在它之后初始化:

先有 current context
GLAD 才能加载当前 OpenGL 环境里的函数地址

双缓冲

渲染循环里会调用:

glfwSwapBuffers(window);

这是双缓冲机制:

front buffer:当前显示器正在显示的画面
back buffer:当前帧正在绘制的画面

OpenGL 先把一帧画到 back buffer,glfwSwapBuffers 再把它交换到前面显示。这样可以避免直接在屏幕上边画边显示造成闪烁。

glViewport 和窗口大小

窗口变大不等于 OpenGL 自动更新绘制区域。OpenGL 把 NDC 坐标映射到屏幕像素区域,这个区域由 glViewport 决定。

窗口大小变化时,应通过 framebuffer callback 更新:

窗口 framebuffer 尺寸变化
-> glViewport(0, 0, width, height)

3. Shader 类

OpenGL 需要 shader 才能绘制图形。最基础的是:

  • 顶点着色器:处理每个顶点的位置、纹理坐标等。
  • 片段着色器:决定每个片段/像素的颜色。

一个 Shader 类通常负责:

读取 shader 文件
编译 vertex shader
编译 fragment shader
链接 shader program
使用 glUseProgram
设置 uniform
析构时删除 program

编译和链接流程

完整流程是:

读取 vertex shader 源码
读取 fragment shader 源码
glCreateShader(GL_VERTEX_SHADER)
glShaderSource
glCompileShader
检查 GL_COMPILE_STATUSglCreateShader(GL_FRAGMENT_SHADER)
glShaderSource
glCompileShader
检查 GL_COMPILE_STATUSglCreateProgram
glAttachShader
glLinkProgram
检查 GL_LINK_STATUSglDeleteShader(vertex)
glDeleteShader(fragment)

链接成功后,单独的 vertex shader 和 fragment shader 对象可以删除,因为 program 已经包含了链接后的结果。

Shader 资源生命周期

OpenGL 的 shader program 是 GPU 资源,不是普通整数。ID 只是资源句柄。

所以 Shader 类要注意:

  • ID 初始化为 0
  • 析构时 glDeleteProgram(ID)
  • 如果编译或链接失败,要清理已经创建的 shader/program。
  • 禁止拷贝,避免两个对象持有同一个 OpenGL program,导致重复删除。

一个实用原则:

谁 glCreate 了资源,失败返回前就要对应 glDelete。

4. 顶点数据、VAO、VBO、EBO

OpenGL 绘制图形时,需要把顶点数据上传到 GPU。

常见对象:

  • VBO:存储顶点数据。
  • VAO:记录顶点属性布局。
  • EBO:存储索引数据,避免重复顶点。

VBO

VBO 保存原始顶点数据,例如:

position: x y z
color:    r g b
texCoord: u v

如果一个顶点有 3 + 3 + 2 = 8 个 float,那么 stride 就是:

8 * sizeof(float)

VAO

VAO 记录“如何解释 VBO 里的数据”。

例如:

location 0 -> position,3 个 float,从 offset 0 开始
location 1 -> color,3 个 float,从 offset 3 * sizeof(float) 开始
location 2 -> texCoord,2 个 float,从 offset 6 * sizeof(float) 开始

对应 shader:

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCoord;

EBO 的易错点

EBO 的绑定状态会被 VAO 记录。

所以如果你这样做:

绑定 VAO
绑定 EBO
设置数据
在 VAO 仍然绑定时解绑 EBO

等于把 VAO 里的 EBO 记录清掉了。使用 glDrawElements 时就会没有有效索引缓冲。

正确记法:

VBO 可以在设置完顶点属性后解绑
EBO 不要在 VAO 绑定期间解绑

5. 纹理

纹理的学习流程:

用 stb_image 读取图片
glGenTextures 创建纹理对象
glBindTexture 绑定纹理
glTexParameteri 设置纹理参数
glTexImage2D 上传图片数据
glGenerateMipmap 生成 mipmap
stbi_image_free 释放 CPU 图片数据

stb_image

stb_image.h 是单头文件库,但实现只能在一个 .cpp 中启用:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

不能在多个 .cpp 里都定义 STB_IMAGE_IMPLEMENTATION,否则会重复定义。

图片路径

图片和 shader 文件路径是相对于程序运行时 working directory,不是相对于 .cpp 文件。

如果 VS Code/CMake Tools 的工作目录是项目根目录,那么可以写:

assets/shaders/basic.vert
assets/textures/container.jpg

纹理坐标

纹理坐标通常是:

左下角: (0.0, 0.0)
右下角: (1.0, 0.0)
右上角: (1.0, 1.0)
左上角: (0.0, 1.0)

OpenGL 纹理坐标通常把 (0,0) 当左下,而图片文件很多以左上为原点,所以经常需要:

stbi_set_flip_vertically_on_load(true);

纹理单元和 sampler

多纹理时要理解:

sampler uniform 存的是纹理单元编号,不是 texture 对象 ID。

例如:

shader.setInt("texture1", 0);
shader.setInt("texture2", 1);

意思是:

shader 里的 texture1 去 GL_TEXTURE0 采样
shader 里的 texture2 去 GL_TEXTURE1 采样

真正把纹理对象放到纹理单元上,需要:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);

渲染前明确绑定纹理是好习惯,因为 OpenGL 是状态机,别的代码可能改变当前绑定。

6. Transform 和矩阵顺序

2D 矩形进入 3D 前,先学习 transform:

  • 平移 translate
  • 旋转 rotate
  • 缩放 scale

GLM/OpenGL 常用列向量约定:

gl_Position = transform * vec4(aPos, 1.0);

如果:

transform = T * R * S

实际作用到顶点的顺序是从右往左:

先 S 缩放
再 R 旋转
最后 T 平移

GLM 写法通常是:

先 translate
再 rotate
再 scale

最终得到:

T * R * S

实际效果就是:

先缩放,再旋转,最后移动

一句话:

代码调用顺序决定矩阵从左到右生成;
顶点实际变换顺序看矩阵从右到左作用。

每帧从单位矩阵开始

动画里通常每一帧都从单位矩阵重新计算:

当前姿态 = 时间函数

如果把 transform 定义在循环外,每帧都在旧矩阵上继续 rotate,就会变成累计变换。再加上直接使用 glfwGetTime(),旋转会越来越夸张。

区分:

glfwGetTime():绝对时间
deltaTime:两帧之间经过的时间

7. 3D 坐标系统

3D 绘制的核心是 MVP:

gl_Position = projection * view * model * vec4(aPos, 1.0);

实际作用顺序:

先 model
再 view
最后 projection

完整坐标流程:

局部坐标-> model
世界坐标-> view
观察坐标/相机坐标-> projection
裁剪坐标-> 透视除法
NDC-> viewport
屏幕坐标

Model 矩阵

model 作用于每个模型,把模型从局部坐标变换到世界坐标。

同一份立方体顶点数据可以通过不同的 model 矩阵摆到不同位置:

第一个 cube 在原点
第二个 cube 在远处
第三个 cube 旋转 40 度

所以绘制多个物体时,通常复用同一个 VAO/VBO,但每次 draw 前换一个 model 矩阵。

View 矩阵

view 表示相机观察变换,它把世界坐标变换到相机坐标。

OpenGL 里通常不是真的移动相机,而是反过来移动整个世界。

例如相机在:

(0, 0, 3)

看向原点,可以理解成:

把整个世界往 z 负方向移动 3 个单位

自由相机里常用:

glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp)

其中:

cameraPos:相机在哪里
cameraFront:相机朝哪个方向看
cameraPos + cameraFront:构造一个临时目标点
cameraUp:哪边是上

front 不是目标点,而是方向向量。

Projection 矩阵

projection 定义相机能看到的空间范围,并把 3D 观察空间投影到裁剪空间。

透视投影常用:

glm::perspective(fov, aspect, near, far)

含义:

fov:视野角
aspect:宽高比
near:近裁剪面
far:远裁剪面

如果加了 view 把物体放到 z = -3 附近,但 projection 仍然是单位矩阵,物体很可能会被裁掉。进入 3D 后,要同时使用 view 和真正的 perspective projection。

深度测试

3D 物体需要深度测试,否则前后关系会混乱。

开启:

glEnable(GL_DEPTH_TEST);

每帧清屏时要同时清颜色和深度:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

8. Camera

Camera 的核心状态:

position:相机位置
front:相机朝向
up:相机上方向
yaw:左右转头
pitch:抬头低头
fov:视野角

WASD 移动

前后移动:

W:position += front * speed * deltaTime
S:position -= front * speed * deltaTime

左右移动需要右方向:

right = normalize(cross(front, up))
A:position -= right * speed * deltaTime
D:position += right * speed * deltaTime

这里必须使用 deltaTime,否则不同帧率下移动速度不同。

Yaw 和 Pitch

yaw 控制左右转头,pitch 控制抬头低头。

用 yaw/pitch 计算 front:

front.x = cos(yaw) * cos(pitch)
front.y = sin(pitch)
front.z = sin(yaw) * cos(pitch)

GLM 的 sin/cos 使用弧度,所以要用 glm::radians

初始 yaw = -90 是为了让默认 front 指向负 Z:

yaw = -90 -> front 大约是 (0, 0, -1)

pitch 通常限制在:

-89 到 89

避免看向正上/正下时方向计算不稳定。

鼠标回调和 user pointer

GLFW 的回调函数签名是固定的,例如鼠标移动:

void callback(GLFWwindow* window, double xpos, double ypos)

不能额外加一个 Camera& 参数,因为 GLFW 只知道窗口和鼠标坐标,不知道你的 C++ 对象。

解决方式是:

glfwSetWindowUserPointer(window, &camera);

这相当于给 GLFWwindow 挂一个自定义 void* 指针。

回调里再取出来:

auto* camera = static_cast<Camera*>(glfwGetWindowUserPointer(window));

完整理解:

main 创建 camera
把 &camera 存进 window user pointer
鼠标移动时 GLFW 调用 callback(window, xpos, ypos)
callback 通过 window 找回 camera
调用 camera->processMouseMovement(...)
camera 更新 front
下一帧 getViewMatrix 使用新 front
画面视角改变

这是一种 C API 和 C++ 对象配合的常见方式。

滚轮 FOV

滚轮缩放通常不是移动相机,而是改变 fov

fov 变小 -> 视野变窄 -> 看起来放大
fov 变大 -> 视野变宽 -> 看起来缩小

处理滚轮时:

fov -= yoffset
fov clamp 到 1 到 45

注意 glm::clamp 返回结果,不会原地修改变量,所以要赋回去:

fov = glm::clamp(fov, 1.0f, 45.0f);

9. 常见错误总结

路径错误

shader 和 texture 文件路径是相对于运行时 working directory。

如果读取失败,不要只看源码目录,要先确认程序从哪里运行。

OpenGL context 顺序错误

GLAD 必须在:

glfwMakeContextCurrent

之后初始化。否则 OpenGL 函数地址没有有效 context 可查。

include 顺序错误

一般写:

#include <glad/glad.h>
#include <GLFW/glfw3.h>

EBO 被解绑

EBO 绑定被 VAO 记录。在 VAO 绑定期间不要解绑 EBO。

sampler 误解

sampler2D 的 uniform 值是纹理单元编号,不是 texture 对象 ID。

string_view::data

std::string_view::data() 不保证以 \0 结尾。传给需要 C 字符串的 API 时要小心。

如果传入的是字符串字面量,通常没问题;如果是任意 string_view,最好转成 std::string

矩阵顺序

projection * view * model * vec4(aPos, 1.0) 实际先作用 model,再 view,最后 projection。

不要只看代码从左往右,要记住矩阵作用在列向量上时是从右往左。

忘记清深度缓冲

开启深度测试后,每帧要清:

GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT

deltaTime 放错位置

lastFrame 要放在循环外。如果放在循环内,每帧都会重置,deltaTime 就不是两帧间隔。

10. 当前阶段总结

到这里,基础部分已经完成:

CMake 引入 GLFW/GLAD/stb/glm/spdlog
创建窗口和 OpenGL context
加载 OpenGL API
编译和使用 shader
绘制三角形、矩形、立方体
使用 VAO/VBO/EBO
加载纹理和多纹理混合
理解 model/view/projection
开启深度测试
绘制多个 3D 物体
实现 WASD 相机移动
实现鼠标视角
实现滚轮 FOV

基础阶段最重要的收获不是记住某段代码,而是理解 OpenGL 是状态机:当前绑定的 VAO、VBO、EBO、texture、shader program、viewport、depth test 等状态共同决定下一次 draw call 的结果。

http://www.jsqmd.com/news/1013925/

相关文章:

  • 2026 Lazada流量转化专家/机构中立测评榜单|商家全域选型指南 - 品牌2026推荐
  • MPC8245 DUART深度解析:从异步串口原理到寄存器编程实战
  • WarcraftHelper完整指南:如何让魔兽争霸3焕然一新的终极解决方案
  • 鸿蒙原生应用实战(五)ArkUI 图片拼接/长图生成:多图合并 + Canvas 绘制 + 导出分享
  • 5分钟掌握猫抓Cat-Catch:浏览器资源嗅探工具的完整使用指南
  • BiliRaffle:让B站UP主告别手动抽奖的终极解决方案
  • 告别拍脑袋估算:用RUSLE模型+QGIS,5步搞定土壤侵蚀强度计算(附数据获取渠道)
  • 终极BT下载加速指南:如何用trackerslist项目彻底告别龟速下载
  • 2026年6月最新版莱芜正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • 2026 广州合同诈骗罪专业律师推荐:合同纠纷变刑事?怎么选对辩护律师 - 互联网科技品牌测评
  • 存算一体芯片软件双模式:单字符驱动网络(普通CPU也能跑)
  • 17-slots为什么有时反而更慢-属性查找的底层路径与描述符协议
  • AIOps 智能容量预测与弹性伸缩联动:从经验估算到数据驱动,云资源的成本与性能平衡
  • PyTorch训练避坑实录:在AMD平台(DirectML)上跑代码,为什么我的优化器不工作了?
  • 5步创新方案彻底解决CAD字体同步难题
  • Neura获14亿美元C轮融资,人形机器人赛道从实验室迈向工厂!
  • 3种高效方法在macOS上完美安装IINA专业播放器
  • ChatGPT API实战入门:从401报错到生产级对话服务
  • 核心必背!【中药学】必背100题及解析(卷号:06121219_04)
  • 深入解析MPC8309 eSDHC中断机制:SDIO通信稳定性的关键
  • 5分钟快速上手:免费获取海量小说资源的完整书源配置方案
  • LLM 验证代码题解:从输出校验到逻辑等价判定的工程实践
  • 2026年6月最新版酒泉正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • 2026年云端保姆级流程:如何部署OpenClaw?Token Plan配置及大模型API Key接入
  • 消费级柔性机器人公司SoulX获融资,首款产品MoYa将带来家庭智能关护新体验!
  • 18-生成器不只是省内存(上)-yield的状态机模型与帧暂停
  • 合肥市庐江县 家电维修清洗|维小达|空调、冰箱、洗衣机、热水器、油烟机一站式维保清洗服务 - 维小达科技
  • 广州擅长合同诈骗刑事辩护律师排名参考:2026 年经济犯罪辩护实务观察 - 互联网科技品牌测评
  • 跨平台BongoCat交互式桌宠:从事件捕获到视觉反馈的实时响应机制
  • Claudesidian:打造AI驱动的第二大脑,让知识管理从未如此简单高效