OpenGL 学习笔记 II:初始化 API,第一个黑窗,游戏循环和帧率,OpenGL 默认垂直同步,glfw 帧率

前情提要:

上一篇: OpenGL 学习笔记 I:OpenGL glew glad glfw glut 的关系,OpenGL 状态机,现代操作系统的窗口管理器,OpenGL 窗口和上下文 OpenGL context_我说我谁呢 --CSDN博客 讲解了 OpenGL glew glad glfw glut 的关系,一笔带过 OpenGL 状态机,几句话讲解了现代操作系统的窗口管理器,理解了 OpenGL 窗口和上下文 OpenGL context 的概念。

重新备注一下,这系列笔记可以认为是基于 learnOpenGL 的个人复习补充笔记,主要是补充一些非机械式的直觉理解内容,比如为什么这样设置,我为什么要这样调 API,底层做了什么,以及一些他没有讲到的点,供我自己钻牛角尖用


这一段其实和图形学 OpenGL 没什么关系,复习可以跳过看下面的。如果复习 C/C++ 可以看一看

动静态库的复习:为什么要宏定义 GLEW_STATIC

  • GLEW 的实现有 static 版本和 shared 版本,不过公用一个头文件. 对于 shared 版本,一般需要链接到 so 文件(lib 在 windows)。实际 so 或者 lib 是怎么共享的呢?
  • 实际这个宏定义 is actually only needed for Windows platforms。下面解释这个事实。
  • 间接引用:ELF (PE)读入的时候,首先会解析符号表,一般来说,一层的引用(指针)很容易想到直接 map 好了共享库的地址空间之后,去把用到 shared 库的空位的给根据共享库的符号表给填好就行了,但是前面也说的,对于层层引用,根本是做不到直接写死的,比如你在dll的头文件里面 extern 了一个别的共享库的变量。因为链式加载库也是有顺序的,所以必须要根据 GOT 表来进行间接跳转。所以间接跳转是必要的开销。
  • extern 关键字:默认所有的非 static 函数都是 extern 的,即能被其他翻译单元访问。对于共享库,external linkage 保证引用了这个库(当然要经过头文件)的源码能够访问这些函数。这样的符号查询会推迟到链接时刻。对于共享库来说,链接的时候也要链接到 so 作为输入,只不过实际填写的时候是运行时。对于静态库来说,他直接被做掉了,因为静态库和生成的可执行文件一起包含翻译了。
  • extern 的查询顺序:ld.so 链接在 gcc 里面是 -l xxx 或者 -lxxx 对于在当前翻译单元能找到的符号,就会用在翻译单元里面。然后首先考虑(先绝对当前目录在走环境变量和系统目录)共享库的,最后考虑静态库的。默认的共享库以 so 后缀,静态库以 a 后缀。
  • 运行时:链接了之后 elf 里面就会有一个段存了他运行需要些什么共享库。这样 os 执行 exec 的时候,解析 elf,就能把共享库读进来然后 map 进虚拟地址空间。期间要做一些地址随机化的事情这里就不复习了。
  • M$ dll : 在微软这里的 PE 格式,类似 GOT 的东西是 IAT(import address table)实际来说就是 shared library(在微软是 dll)的 extern 都要坐在 GOT 表里面,然后运行通过间接访问来进行,至于怎么定义的就是加载库的 elf 段文件有定义一个 extern 变量(.data 数据段)的地方就把他地址写到整个程序的 GOT 里面。extern 这里其实是两用的, 导出定义用 extern,引入声明也用 extern。dll 比较特殊他的 dll 不是像 so 共享库那样默认全部都 export 的,而是要用 def 表来定义,或者通过__declspec(dllimport) 和 dllexport 对来实用。对于函数的 dllimport 默认的,对于数据来说必须指定使用 dllimport 因为 dll 的数据他不是放在 IAT 里面的,这是因为 windows 下的 dll 数据做了 CoW 处理。
  • 参考阅读:
  • 虽然讲了这么多,但是我并不打算用 glew,前面说了新的项目(不过这哪算什么项目啊...)可能使用 glad 是不错的。

使用 GLAD,轻装上阵

  • 前一篇文章没有解释为什么采用 glad,因为 glad 是一个你想要什么就生成什么的静态源码,默认自带所有 OpenGl 特定版本的 spec,然后还能自定义显卡厂商的 extension。https://glad.dav1d.de 通过这个网站就能生成 glad.c 和 .glad.h 文件,然后直接放进项目里配置好 CMake 就能用了,不需要安装,不需要考虑平台,下载 dll、lib 还是 so、a 文件。
  • GLAD 是一个纯纯静态源码使用的,非常好,我们直接用他。由此在不用区分什么 glew3s 和 glew3 以及要不要 define static 的区别了。
  • 需要注意的是我们说过 glew 和 glad 都是 loading library,所以实际使用是不会用到大量的 glad 函数的,而是就是正统的 glXXXX 的 opengl 函数
  • 不过 glad 就是太轻量级了,所以他必须(不是,实际你可以调用 wgl 或者 glx 的)配合 GLFW 使用。因为他要用到一个 GetProcAddress 函数(下面会讲为什么他在 glfw 里面),这个函数里面就是进行动态链接库查询的,然后返回函数指针。
  • glad 不需要文档,因为他的实际用途只有一开始 load,之后用的都是 glxxx 了,所以每次用只需要根据 glad 的 github README 把那几行 usage 给抄过来就行了

这里给出第一个 OpenGL 黑色窗口的代码

直接抄 LearnOpenGL 的和 glad README 的

// cpp 文件
//
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
GLFWwindow *window;
void glfw_chore() {
    // init
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
    // create window
    window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr) {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        abort();
    }
    glfwMakeContextCurrent(window);
}
void glad_chore() {
    // glad: load all OpenGL function pointers
    if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) {
        std::cout << "Failed to initialize GLAD" << std::endl;
        abort();
    }
}

int main() {
    glfw_chore();
    glad_chore();
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
        std::cout << "One" << std::endl;
        glfwSwapBuffers(window);
    }
    glfwTerminate();
    return 0;
}

GLFW Window OpenGL context

  • 上下文:上一篇说过了对于 OpenGL 来说,状态机的状态是针对一个Context 来设置的,这个一个 Context 需要包含一个窗口的缓冲区,从而让 GPU 渲染到窗口的 framebuffer 上去。存在形式是一个全局变量挂到那个上下文上,所以是通过 MakeContextCurrent 这种名字的函数(实际 OpenGL 没有规定这个接口,由操作系统和显卡厂商共同实现)指定了之后就针对那个上下文进行操作。
  • 管理窗口的钩子:对于 GLFW 来说,他管理的是一个操作系统的窗口的交互然后架起到 OpenGL 的桥梁。所以实际他管理的对象是窗口,存在形式是通过一个窗口指针来指定要设置的窗口。实际一个窗口在 glfw 里面是一个很复杂的结构体,包括了窗口的各种属性。
  • : 因为我们不直接访问暴露的 framebuffer,实际因为我们通过 glfw 管理缓冲区(一个窗口一个),所以实际调用的 glfwMakeContextCurrent 来设置,里面会调用 OS、驱动 的 makecontextcurrent 的。这么看来,glfw 窗口就是我们以后管理 OpenGL 上下文的东西了,之后操作上下文的行为通过 glfwxxx 函数进行调用。而涉及在当前上下文上进行渲染、数据传送等操作就都通过 glxxx 函数进行
  • 配置与 getProcAddress而为什么对整体的 OpenGL 参数进行设置(比如核心立即模式和最小最大版本号这种设置)以及加载函数指针也通过 glfw 提供呢?我们其实知道 getProcAddress 是 x window (linux)和 wgl (win)提供的,这是因为整个 GUI 都是基于 OS 的,就算 OpenGL 想要直接写显卡framebuffer 使用像素图形模式功能,也需要透过 OS 查询显存映射地址,查询驱动本身也走 OS 查询,所以这部分直接就做在 OS 的窗口管理器接口下面了,所以归入 glx 和 wgl 函数簇下,对于使用跨平台的 glfw 的我们来说,自然这些工作就通过调用 glfw 来做了。

VSYNC 垂直同步

  • 垂直同步的好处和坏处,对不同游戏的适用范围玩游戏的时候其实肯定都学习过了,这里就不说了。我们从 OS  调度的方向来讲,VSYNC 有一个好处是很明显的,那就是减少 CPU 的 usage。至于具体的 gameloop 和各方面的分析,还涉及 gameloop 的写法,游戏逻辑时间、逻辑帧和渲染帧等概念,如果是网游还涉及网络质量问题,这个前面有三篇学游戏理论的时候的笔记我分析过了(其中一篇:游戏开发基础笔记:逻辑帧和物理帧辨析 | Gameloop | 游戏循环),不赘述。
  • 我们观察基本代码的运行结果,可以发现,打印的速度好像是一卡一卡的,这就是因为他启动了垂直同步,这样 CPU 就不会 100%(指一个 core ,实际可能 25% 或者 12.5%)了(主要是我回想 SDL 写的 RoboCat 游戏 gameloop 必100%,而 learnOpenGL 教程又没讲这一点,我忍不住)。
  • 最简单的 gameloop 里面,PollEvent 做的事是简单调用 windows 系统的消息模块,非阻塞 peek 全部已经 queued 的事件,然后调用 callback 之后返回。实际 callback 的事情可能什么都不做,或者不会太复杂,我们一般会把复杂的逻辑放到逻辑帧里面用一个 processInput()  wrapper 来处理所有我们关心的事件(比如通过 getKey 这种名字的函数)。这一点和网络编程是差不多的,因为 callback 一般是 stateless 的,不可能让他处理完全部东西,这个和 js 那种恶魔语言不同。对于需要急速捕获用户连击事件的话,用 callback 应该是更适用。总之一种(callback)是异步无状态的,一种是 backlog 自己 dispatch 输入,还是看用途。
  • 所以实际 vsync 部分发生在 swap buffer 里面。首先glfw的文档的确表明这个 If the swap interval is greater than zero, the GPU driver waits the specified number of screen updates before swapping the buffers. 在  window 下,我 step into glfw_swapbuffers 最后发现调用的是 wingdi 的 swapbuffers。查阅我没有看到具体实现为什么会引发 interval,而且他另外还有一个 swapinterval 函数,结果发现 swapinterval 底层 wglSwapIntervalEXT 这个函数其实是设置 interval 的意思,而不是和 swapbuffers 并列的。所以是通过  swapinterval 影响 swapbuffers 。吐槽 glfw 的文档搜索功能稀巴烂(可能是我不会用)。
  • 关闭 VSYNC需要注意 OpenGL (实际是 OS 和驱动实现)默认开启了垂直同步。我们当然是直接操作 glfw 设置各种和 context 有关的参数,所以就调用这个函数就行了:glfwSwapInterval。附带的参考连接是:I have a question about fps - support - GLFW opengl - My limited FPS : 60 - Stack Overflow
  • 实现一个 FPS 显示:An FPS counter (opengl-tutorial.org) 这种做法应该所有游戏里面通行的做法,实际显卡驱动提供的或者某些第三方插件(游戏盒子什么的)则应该是在操作系统、驱动里面进入显卡 render (每个渲染帧)之前挂上这段代码就能实现帧数显示了。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值