[OpenGL] 学习小纪 - 初体验

由于OpenGL是一个图形API,并不是一个独立的平台,它需要一个编程语言来工作,在这里我们使用的是C++。

然而,OpenGL本身并不是一个API,它仅仅是一个由 Khronos组织制定并维护的规范(Specification)。OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现的,将由OpenGL库的开发者自行决定。

实际的OpenGL库的开发者通常是显卡的生产商。你购买的显卡所支持的OpenGL版本都为这个系列的显卡专门开发的。当你使用Apple系统的时候,OpenGL库是由Apple自身维护的。在Linux下,有显卡生产商提供的OpenGL库,也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时,基本都是库的开发者留下的bug。

OpenGL的一大特性就是对扩展(Extension)的支持,当一个显卡公司提出一个新特性或者渲染上的大优化,通常会以扩展的方式在驱动中实现。如果一个程序在支持这个扩展的显卡上运行,开发者可以使用这个扩展提供的一些更先进更有效的图形功能。

if(GL_ARB_extension_name)
{
    // 如果当前显卡使用硬件支持的全新的现代特性
}
else
{
    // 不支持此扩展: 用旧的方式去做
}

OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。

假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。

在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集。比如,我们可以用一个对象来代表绘图窗口的设置,之后我们就可以设置它的大小、支持的颜色位数等等。可以把对象看做一个C风格的结构体(Struct):

struct object_name {
    float  option1;
    int    option2;
    char[] name;
};

使用OpenGL的类型的好处是保证了在各平台中每一种类型的大小都是统一的。你也可以使用其它的定宽类型(Fixed-width Type)来实现这一点。

// 创建对象
unsigned int objectId = 0;
glGenObject(1, &objectId);
// 绑定对象至上下文
glBindObject(GL_WINDOW_TARGET, objectId);
// 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 将上下文对象设回默认
glBindObject(GL_WINDOW_TARGET, 0);

举个例子,这里创建了一个对象并且绑定了 GL_WINDOW_TARGET,然后给GL_WINDOW_TARGET 设置了宽高,其实这个宽高配置是和我们创建的对象绑定了,这个时候我们可以取消绑定对象和target,当你重新绑定的时候,target会自动应用我们设置的宽高。


Start up

环境搭建:https://www.jianshu.com/p/891d630e30af

  • 如果你在brew install的时候报错权限问题可以看看是不是这个:
    https://stackoverflow.com/questions/61899041/how-to-fix-the-error-permission-denied-apply2files-usr-local-lib-node-modul
    如果是的话可以酱紫sudo chown -R 用户名:staff /usr/local/lib/python3.8/site-packages/saflib-0.3.2-py3.8.egg/saflib/.DS_Store解决~

  • 如果你遇到了 build 成功但是窗口不显示并且报错 adhoc sign 有问题可以参考这个:https://blog.csdn.net/Niall_L/article/details/101468571

环境搭建

Glut / freeglut / glfw / glew / glad / gl3w库都是做什么的

OpenGL 会依赖窗口交互,但是我们的系统有很多,linux、windows、mac,于是就产生了专门负责窗口的库,你可以不用关系不同系统的窗口细节,只要调用库的方法就可以创建和处理窗口。

  • glut (Opengl Utility Tool)
    定义以及控制视窗
    侦测并处理键盘和鼠标事件
    以一个函数呼叫绘制某些常用立体图形,如长方体,球,犹他茶壶
    提供了简单选单列的实现
    所有glut的库函数均已glut开头, 例如glutPostRedisplay(). 后来以及停止维护了

  • freeglut
    glut的替代品,最新稳定的版本是Freeglut3.0.0 (2015年3月7日)

  • glfw (Graphics Library Framework)
    创建管理窗口和opengl的上下文
    处理手柄,键盘,鼠标输入
    目前glfw还在维护,可以说glfw库可以是代替glut和freeglut的库的

支持opengl的驱动版本众多,大多数函数的地址(内存地址)无法在编译时候确定下来,需要运行的时候查询。所以在运行的时候获取函数的内存地址并把其保存在一个函数指针中供后续使用,glew, glad, gl3w基本都是实现类似的功能。

如果不通过库的话,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。取得地址的方法因平台而异,在Windows上会是类似这样:

// 定义函数原型
typedef void (*GL_GENBUFFERS) (GLsizei, GLuint*);
// 找到正确的函数并赋值给函数指针
GL_GENBUFFERS glGenBuffers  = (GL_GENBUFFERS)wglGetProcAddress("glGenBuffers");
// 现在函数可以被正常调用了
GLuint buffer;
glGenBuffers(1, &buffer);

  • 窗口管理
    老产品:glut/freeglut
    替代品:glfw

  • 函数加载
    老产品:glew
    替代品:glad

  • 项目开发,通常有三种组合
    (1)freeglut+glew
    (2)glfw+glew
    (3)glfw+glad
    其中组合1是经典,组合3比较新潮。


1. 窗口创建

首先需要import头文件:

#include 
#include 

然后就可以使用 glfw 啦~

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 主版本号
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 最小版本号
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 核心模式
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 向前兼容,mac需要

    return 0;
}

调用glfwInit函数来初始化GLFW,然后我们可以使用glfwWindowHint函数来配置GLFW。glfwWindowHint函数的第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;第二个参数接受一个整型,用来设置这个选项的值。

接下来我们创建一个窗口对象,这个窗口对象存放了所有和窗口相关的数据,而且会被GLFW的其他函数频繁地用到。

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);

在之前的教程中已经提到过,GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLAD

if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
    std::cout << "Failed to initialize GLAD" << std::endl;
    return -1;
}

我们给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数。GLFW给我们的是glfwGetProcAddress,它根据我们编译的系统定义了正确的函数。

在我们开始渲染之前还有一件重要的事情要做,我们必须告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport),这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。我们可以通过调用glViewport函数来设置窗口的维度(Dimension):

glViewport(0, 0, 800, 600);

glViewport函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)。

OpenGL幕后使用glViewport中定义的位置和宽高进行2D坐标的转换,将OpenGL中的位置坐标转换为你的屏幕坐标。例如,OpenGL中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。注意,处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)

然而,当用户改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用

void framebuffer_size_callback(GLFWwindow* window, int width, int height); // 声明

glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //注册监听

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

我们可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口。我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。因此,我们需要在程序中添加一个while循环,我们可以把它称之为渲染循环(Render Loop),它能在我们让GLFW退出前一直保持运行。下面几行的代码就实现了一个简单的渲染循环:

while(!glfwWindowShouldClose(window))
{
    glfwSwapBuffers(window);
    glfwPollEvents();    
}
  • glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回true然后渲染循环便结束了,之后为我们就可以关闭应用程序了。

  • glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)。

  • glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。

双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。
为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。

当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。我们可以在main函数的最后调用glfwTerminate函数来完成。

glfwTerminate();
return 0;

2. 接受输入

我们同样也希望能够在GLFW中实现一些输入控制,这可以通过使用GLFW的几个输入函数来完成。我们将会使用GLFW的glfwGetKey函数,它需要一个窗口以及一个按键作为输入。这个函数将会返回这个按键是否正在被按下。我们将创建一个processInput函数来让所有的输入代码保持整洁。

void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

这里我们检查用户是否按下了返回键(Esc)(如果没有按下,glfwGetKey将会返回GLFW_RELEASE。如果用户的确按下了返回键,我们将通过glfwSetwindowShouldClose使用把WindowShouldClose属性设置为 true的方法关闭GLFW。下一次while循环的条件检测将会失败,程序将会关闭。

我们接下来在渲染循环的每一个迭代中调用processInput:

while (!glfwWindowShouldClose(window))
{
    processInput(window);

    glfwSwapBuffers(window);
    glfwPollEvents();
}

这就给我们一个非常简单的方式来检测特定的键是否被按下,并在每一帧做出处理。

为了测试一切都正常工作,我们使用一个自定义的颜色清空屏幕。在每个新的渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果(这可能是你想要的效果,但通常这不是)。我们可以通过调用glClear函数来清空屏幕的颜色缓冲,它接受一个缓冲位(Buffer Bit)来指定要清空的缓冲,可能的缓冲位有GL_COLOR_BUFFER_BITGL_DEPTH_BUFFER_BITGL_STENCIL_BUFFER_BIT。由于现在我们只关心颜色值,所以我们只清空颜色缓冲。

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

注意,除了glClear之外,我们还调用了glClearColor来设置清空屏幕所用的颜色。当调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。在这里,我们将屏幕设置为了类似黑板的深蓝绿色。

完整代码可参考:https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/1.2.hello_window_clear/hello_window_clear.cpp


初体验到此结束啦,下一节会写一下怎么写三角形神马的,这一篇真的水的很就是环境搭建hhhh,因为我也还不会啦,感觉 OpenGL 好神奇,API都如此的原始...

最后的最后聊点儿废话技术无关哈,可能是因为最近看了心灵奇旅(很好看!),也是身在互联网这两年的感受叭,其实我们很难变得伟大,变得和别人不一样,前几天看左耳朵耗子的直播,他似乎提到了如何变得不平庸这个topic,可是名利双收就不平庸么?

我也很羡慕那些早早继承家业做了上市公司CEO的同学,也羡慕家里亲戚的财富自由,也羡慕有些朋友特别好看,但是其实大家都只是活着,没有什么区别,顶多他家大一点,还得雇几个保姆打扫而已。努力奋斗想多挣钱其实争的只不过是死不带去的东西,别为了这个枉费了生的快乐和平静。

如果追求的是名利,那并不是不平庸,其实挺世俗的。在我看来的不平庸是真的能影响别人,或者帮助别人,比如袁老、钟老、钱老,然鹅我自知做不到这个程度。剩下的其实活的都差不多,那就开心点儿叭,别太在乎别人的眼光,别年纪轻轻把自己虐到三高,也别忘了最爱你的人永远是父母。人生苦短,活在当下。我们总是追名逐利,最近看了太多豪宅了,但是住豪宅开豪车会带来快乐么?我想如果没有爸妈,即使让我住梵悦我也不会觉得快乐的,这么想好像就知道自己想要什么了,家人平安才是我真的求的东西。虽不恋生,却希望生不要负人。

深夜碎碎念结束,周末愉快啊!

Reference:
https://learnopengl-cn.github.io/01%20Getting%20started/03%20Hello%20Window/
https://www.jianshu.com/p/ad75b6a307f1
https://blog.csdn.net/libaineu2004/article/details/105879521

你可能感兴趣的:([OpenGL] 学习小纪 - 初体验)