OpenGL从入门到放弃 #02Create a window


  在上一节把环境搭建完毕后,今天就来学习怎么创建一个窗口,并在这个窗口实现一些基本功能。这里面涉及的函数很多,我尽力将逐个函数的功能以及怎么调用讲述清楚。
  首先,导入GLFW和GLEW两个库的头文件,验证一下环境是否搭建成功:

#include<iostream>
#include<GL/glew.h>
#include<GLFW/glfw3.h>

1.初始化和配置GLFW

  先创建一个main函数,在这个函数里面初始化GLFW:

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);  //使用的OpenGL版本号3.3
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);      //使用流水线配置模式

    return 0;
}

glfwInit():初始化GLFW
glfwWindowHint(int hint, int value):在初始化完GLFW后,要调用这个函数来配置GLFW,告知GLFW我们要使用OpenGL的版本号是多少,且用的是什么模式。
GLFW_CONTEXT_VERSION_MAJOR:告知GLFW使用OpenGL的主版本号,这里为3。
GLFW_CONTEXT_VERSION_MINOR:告知GLFW使用OpenGL的次版本号,这里也是3,即使用的OpenGL版本是3.3
GLFW_OPENGL_PROFILE:告知GLFW要使用的模式是什么。
GLFW_OPENGL_CORE_PROFILE:告知GLFW使用的核心模式(Core-profile)

核心模式意味着我们只能使用OpenGL功能的一个子集(没有了我们已不再需要的向后兼容特性)

2.创建一个窗口对象

  在完成对GLFW的初始化后,我们就可以创建一个窗口对象,这个窗口对象存放的所有与窗口相关的数据和信息,且会被其他函数频繁用到。

//也是在main函数里,紧接上述代码
    GLFWwindow* window = glfwCreateWindow(800, 600, "Learn OpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

glfwCreateWindow(int width, int height, const char *title, GLFWmonitor *monitor, GLFWwindow *share):这个函数创建一个窗口对象,它需要窗口的宽和高作为它的前两个参数,需要一个窗口的名称(字符串)作为它的第三个参数,最后两个参数暂时忽略,这里并用不上。然后返回一个GLFWwindow对象,这个就是创建好的窗口对象。
  创建完成以后,并在后面跟了一个失败检查,如果失败就告知用户并执行glfwTerminate()
glfwTerminate():清理所有资源并正确地退出应用程序。
glfwMakeContextCurrent(GLFWwindow *window);这个函数通知GLFW将指定窗口的上下文(context)设置为当前线程的主上下文。我的理解是可能接下来某些函数的操作,虽然并没有指定哪个window,但由于使用的这个函数,所以相应的操作是作用于这个主窗口。(可能有误)

3.初始化GLEW

  GLEW是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLEW。在OpenGL环境没有完全建立时初始化GLEW有可能会失败,所以要在GLFW初始化之后再来初始化GLEW。

//同样在main函数里,紧接其上
    glewExperimental = true;
    if (glewInit() != GLEW_OK)
    {
        std::cout << "Failed to initial GLEW." << std::endl;
        glfwTerminate();
        return -1;
    }

glewExperimental:这个布尔变量意味着是否开启一些GLEW实验性质的功能。在使用核心模式的时候,这个布尔变量要设为ture,否则OpenGL程序有可能会崩溃。
glewInit():初始化GLEW,如果初始化成功会返回GLEW_OK这个枚举值。

4.渲染窗口

  虽然我们使用了glfwCreateWindow()创建了一个窗口对象,但其仅仅只是一个对象,并不能真正显示出来,要另外使用一个函数把这个窗口渲染出来:

//同样在main函数里,紧接其上
    glViewport(0, 0, 800, 600);//渲染窗口大小

glViewport(GLint x, GLint y, GLsizei witdh, GLsizei height):渲染一个窗口,前两个参数控制窗口左下角的位置,后两个参数控制渲染窗口的宽度和高度(pixel)。而对于GLintGLsizei是什么类型,这其实是int类型的另外一种名称,在glew.h里使用了typedef换了皮而已,实际上还是int类型。


  实际上也可以将视口的维度设置为比GLFW的维度小,这样子之后所有的OpenGL渲染将会在一个更小的窗口中显示,这样子的话我们也可以将一些其它元素显示在OpenGL窗口之外。至此这个窗口就能显示出来,但由于执行到这就已经到了main函数的末尾,main函数一旦结束这个窗口就会关闭。所以呈现的效果就是窗口在屏幕上一闪而过。

5.渲染循环

  我们可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口。我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。因此我们需要一个循环不断接收外界的输入并作出相应的反应,直到GLFW主动退出。这个循环称为渲染循环(Render Loop)

//同样在main函数里,紧接其上
    while (!glfwWindowShouldClose(window))
    {
        //接收输入,交换缓冲
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

glfwWindowShouldClose(GLFWwindow *window):这个函数会检查一次GLFW是否被要求退出(会有相应的函数要求GLFW退出),如果是该函数返回true,然后这个while循环就会结束。
glfwPollEvents():这个函数会检查是否有触发事件(会接收来自外部设备的输入)、更新窗口状态,并调用对应的回调函数。
glfwSwapBuffers(GLFWwindow *window):交换颜色缓冲区(保存着GLFW窗口每一像素颜色值的缓冲区),在这一迭代(iteration)中被用来绘制,并且将会作为输出显示在屏幕上。
  因为OpenGL采用双缓冲来渲染窗口,所以存在交换缓冲区这么一个说法,那么何为双缓冲,这值得深究:

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

  在有了渲染循环之后,我们就能看到这个窗口了,效果如图:


6.实现输入退出控制

  我想能够在GLFW中实现退出程序控制(键入某个按钮退出程序),这可以通过glfwGetKey()实现,这与Unity里的Input.GetKey()函数有几分相似。我在main()函数以外的地方创建了一个新的函数processInput去完成这个功能,但要注意的是,你必须要在调用这个函数之前定义好这个函数亦或是有这个函数的声明式,不然会报错说这个函数不存在或未定义。

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

int glfwGetKey(GLFWwindow *window, int key):这个函数检查指定窗口是否有指定按键的输入,如果有就返回GLFW_PRESS否则返回GLFW_RELEASE。而GLFW_KEY_ESCAPE对应的按键就是Esc。
glfwSetWindowShouldClose(GLFWwindow *window, int value):这个函数设置是否关闭指定的GLFW窗口对象,如果设为true,就是关闭。
  我们在这个函数里面检查用户是否键入了Esc,如果是关闭窗口对象。
  定义好这个函数,就要在渲染循环的每一个迭代上调用这个函数:

    while (!glfwWindowShouldClose(window))
    {
        //处理输入
        processInput(window);

        //接收输入,交换缓冲
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

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

7.清屏指令

  除了输入控制要放在渲染循环,所有的渲染操作也要放在渲染循环中,因为我们想让这些渲染指令在每次渲染循环迭代的时候都能被执行。这些操作的循环的顺序应该是这样的:

    while (!glfwWindowShouldClose(window))
    {
        //处理输入
        processInput(window);

        //渲染指令
        ...

        //接收输入,交换缓冲
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

  就是在每一帧的最后才接收输入,在每一帧开始时处理上一帧接收的输入,处理完在执行渲染指令。这与一般的想法不太一样,认为接收完输入就可以立刻处理输入。这是因为这个glfwPollEvents()执行起来是颇费时间的,放在前面有可能会造成帧率不稳定。
  在这里我想做的一个渲染操作就是使用一个自定义颜色清空屏幕。在每一个渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果,这一般不是我们想要的。

    while (!glfwWindowShouldClose(window))
    {
        //处理输入
        processInput(window);

        //渲染指令
        glClearColor(1.0f, 0, 0, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        //接收输入,交换缓冲
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

glClear(GLBitfield mask):这个函数就是用来清空屏幕的颜色缓冲,它接收一个缓存位(GLBitfield)来指定要清空的缓冲,可能的缓冲有GL_COLOR_BUFFER_BITGL_DEPTH_BUFFER_BITGL_STENCIL_BUFFER_BIT,我们现在只想清空颜色,所以指定GL_COLOR_BUFFER_BIT

glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha):这个函数设置清空屏幕所用的颜色,它接收RGB值以及一个透明度值。这里我设置成了大红色。

  当渲染循环结束后我们需要正确释放之前分配的(allocated)的资源,我们可以在main函数的最后使用glfwTerminate()函数来完成,这个函数刚有提到,不再赘述。

//同样在main函数里,紧接其上
    glfwTerminate();
    return 0;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350