OpenGL超级宝典(第7版)笔记2 清单的初始环境配置part2

OpenGL超级宝典(第7版)笔记2 清单的初始环境配置part2


书接上文,part1中我们配置了一下GLFW窗口库,还顺便配置了一下GLEW帮助我们更方便的应对OpenGL的各种版本,这次我们将亲自用GLFW打开一个窗口,并做一些推进(仍然是基于LearnOpenGL的内容,其中一些部分我稍微调整了一下顺序和说明,让整个过程更加连贯)。

转载自learnOpenGL的窗口创建

在part1中我们已经创建了一个工程,并且将GLFW GLEW 的头文件(.h文件)和二进制库(.lib文件)都添加到了我们的工程中,那么下面我们在这个工程中开始添加cpp文件开始编写我们的环境。(为了替代无法安装sb7.h极其相关文件的困境)

1 设置一下头文件

让我们试试能不能让GLFW正常工作。首先,新建一个.cpp文件,然后把下面的代码粘贴到该文件的最前面。注意,之所以定义GLEW_STATIC宏,是因为我们使用的是GLEW静态的链接库。

// GLEW
#define GLEW_STATIC
#include <GL/glew.h>//这里注意是否glew.h是放在了GL这个子文件夹下的
// GLFW
#include <GLFW/glfw3.h>//这里注意是否glfw3.h是放在了GLFW这个子文件夹下的

注意
请确认在包含GLFW的头文件之前包含了GLEW的头文件。在包含glew.h头文件时会引入许多OpenGL必要的头文件(例如GL/gl.h),所以你需要在包含其它依赖于OpenGL的头文件之前先包含GLEW。

2 创建main函数配置GLFW、GLEW、视口

2.1 处理GLFW相关的配置

接下来我们创建main函数,在这个函数中我们将会实例化GLFW窗口:

int main()
{
    glfwInit();
    //调用glfwInit函数来初始化GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    //告诉GLFW我们要用的OpenGL主版本号是3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    //告诉GLFW我们要用的OpenGL小版本号是3,和上一句合起来是OpenGL3.3
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    //告诉GLFW我们用的是OpenGL核心模式
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
    //告诉我们的GLFW,我们的窗口,用户是不能随意更改大小的

    return 0;
}

首先,我们在main函数中调用glfwInit函数来初始化GLFW,然后我们可以使用glfwWindowHint函数来配置GLFW。glfwWindowHint函数的第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;第二个参数接受一个整形,用来设置这个选项的值。该函数的所有的选项以及对应的值都可以在 GLFW’s window handling 这篇文档中找到。如果你现在编译你的cpp文件会得到大量的 undefined reference (未定义的引用)错误,也就是说你并未顺利地链接GLFW库。

由于本站的教程都是基于OpenGL 3.3版本展开讨论的,所以我们需要告诉GLFW我们要使用的OpenGL版本是3.3,这样GLFW会在创建OpenGL上下文时做出适当的调整。这也可以确保用户在没有适当的OpenGL版本支持的情况下无法运行。我们将主版本号(Major)和次版本号(Minor)都设为3。我们同样明确告诉GLFW我们使用的是核心模式(Core-profile)(可以参考这篇博客),并且不允许用户调整窗口的大小(这里我们不允许是为了不产生一些奇怪的现象,因为会产生一些视口、裁切的问题,目前我们先把它放在一边)。

在明确告诉GLFW使用核心模式的情况下,使用旧版函数将会导致invalid operation(无效操作)的错误,而这不正是一个很好的提醒吗?在我们不小心用了旧函数时报错,就能避免使用一些被废弃的用法了。如果使用的是Mac OS X系统,你还需要加下面这行代码到你的初始化代码中这些配置才能起作用:

glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

注意
请确认您的系统支持OpenGL3.3或更高版本,否则此应用有可能会崩溃或者出现不可预知的错误。如果想要查看OpenGL版本的话,在Linux上运行glxinfo,或者在Windows上使用其它的工具(例如OpenGL
Extension Viewer)。如果你的OpenGL版本低于3.3,检查一下显卡是否支持OpenGL
3.3+(不支持的话你的显卡真的太老了),并更新你的驱动程序,有必要的话请更新显卡。

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

GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
if (window == nullptr)
{
    std::cout << "Failed to create GLFW window" << std::endl;
    glfwTerminate();
    return -1;
}
glfwMakeContextCurrent(window);

glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数;第三个参数表示这个窗口的名称(标题),这里我们使用"LearnOpenGL",当然你也可以使用你喜欢的名称;最后两个参数我们暂时忽略,先设置为空指针就行。它的返回值GLFWwindow对象的指针会在其他的GLFW操作中使用到。创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了。

2.2 处理GLEW相关的配置

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

glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
    std::cout << "Failed to initialize GLEW" << std::endl;
    return -1;
}

请注意,我们在初始化GLEW之前设置glewExperimental变量的值为GL_TRUE,这样做能让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术,如果把它设置为GL_FALSE的话可能会在使用OpenGL的核心模式时出现一些问题。

2.3 视口(Viewport)

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

int width, height;
glfwGetFramebufferSize(window, &width, &height);

glViewport(0, 0, width, height);

glViewport函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素),这里我们是直接从GLFW中获取的。我们从GLFW中获取视口的维度而不设置为800*600是为了让它在高DPI的屏幕上(比如说Apple的视网膜显示屏)也能正常工作。

我们实际上也可以将视口的维度设置为比GLFW的维度小,这样子之后所有的OpenGL渲染将会在一个更小的窗口中显示,这样子的话我们也可以将一些其它元素显示在OpenGL视口之外。

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

3 创建循环准备运行我们的窗口

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

while(!glfwWindowShouldClose(window))
{
    glfwPollEvents();
    glfwSwapBuffers(window);
}
  • glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回true然后游戏循环便结束了,之后为我们就可以关闭应用程序了。
  • glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等),然后调用对应的回调函数(可以通过回调方法手动设置)。我们一般在游戏循环的开始调用事件处理函数。
  • glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。

双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。
这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。
为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
这里会有两个缓冲区,一个是我们看到的显示图像的缓冲,另一个是我们看不到的用于绘制下一帧的缓冲,我们将各种三角形贴图等都绘制在这个缓冲中,当调用glfwSwapBuffers时我们刚刚绘制的图像才会显示在屏幕上,当然这时电脑已经开始绘制再下一帧的画面了

4 运行窗口前的收尾工作

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

glfwTerminate();
return 0;

这样便能清理所有的资源并正确地退出应用程序。现在你可以尝试编译并运行你的应用程序了,如果没做错的话,你将会看到如下的输出:
在这里插入图片描述
如果你看见了一个非常无聊的黑色窗口,那么就对了!如果你没得到正确的结果,或者你不知道怎么把所有东西放到一起,请到这里参考源代码。

如果程序编译有问题,请先检查连接器选项是否正确,IDE中是否导入了正确的目录(前面教程解释过)。并且请确认你的代码是否正确,直接对照上面提供的源代码就行了。

5 更进一步(上个色)

仅仅是一个黑色的窗口也太没意思了吧,不如我们换个颜色吧!

// 程序循环
while(!glfwWindowShouldClose(window))
{
    // 检查事件
    glfwPollEvents();

    // 渲染指令
    ...

    // 交换缓冲
    glfwSwapBuffers(window);
}

还记着上面这个循环吧,我们不断的检查是否有事件(比如输入了一些按键,或是鼠标移动后调用了一些事件等等),并且交换前后缓冲使新绘制的图案显示出来(虽然我们这里什么也没画)。
那么思路就来了,如果我们在渲染指令中添加一些绘制或是填色的函数就好了。
这里我们设置的颜色是RGB为0.2、0.3、0.3的深蓝绿色

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
//设置当进行清屏时,用深蓝绿色进行填充,单单调用这个函数不能实现清屏,它仅仅是设置
glClear(GL_COLOR_BUFFER_BIT);
//对后缓冲进行清屏,只清除后缓冲中的颜色内容(上面设置了用深蓝绿色进行填充,所以清屏后填充了深蓝绿色)

同时为了测试一切都正常工作,我们使用一个自定义的颜色(这里为RGB分别为0.2f, 0.3f, 0.3f的一个深蓝绿色,你想设置什么颜色都行)清空屏幕。

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

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

注意
glClearColor函数是一个状态设置函数,而glClear函数则是一个状态应用的函数。
以后OpenGL中还会有很多类似的函数出现,它们也是把状态设置和状态应用分开处理的。

当然你也可以通过glfw获取时间并让颜色根据时间进行变化

while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
        float timeValue = glfwGetTime();
        //调用函数获取时间
        glClearColor(sin(timeValue)/2.0f+0.5f, sin(timeValue+1.0f) / 2.0f + 0.5f, sin(timeValue+2.0f) / 2.0f + 0.5f, 1.0f);
        //设置让颜色随时间变化
        //其中进行sin运算、除以2.0f、加0.5f,是由于颜色的值是取0到1.0f之间的,过大过小会强制转换为0或1.0f
        //加1.0f或是2.0f是让RGB不同时变化,要不然就仅仅是黑白灰的变化了,不那么好看O(∩_∩)O
        glClear(GL_COLOR_BUFFER_BIT);
        //进行清屏
        glfwSwapBuffers(window);
    }

6 再更进一步(输入控制)

6.1 再更进一步(按键控制)

当然仅仅是靠程序自己进行颜色变换,还是无聊了些,如果我们控制键盘鼠标能够自由调整颜色就更好了(虽然learnOpenGL只是想用按键控制来关闭它…),这就要讲到输入。

我们希望能够在GLFW中实现一些键盘控制,这可以通过使用GLFW的回调函数(Callback Function)来完成。回调函数事实上是一个函数指针,当我们设置好后,GLWF会在合适的时候调用它。按键回调(KeyCallback)是众多回调函数中的一种。当我们设置了按键回调之后,GLFW会在用户有键盘交互时调用它。该回调函数的原型如下所示:

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);

按键回调函数接受一个GLFWwindow指针作为它的第一个参数;第二个整形参数用来表示按下的按键;action参数表示这个按键是被按下还是释放;最后一个整形参数表示是否有Ctrl、Shift、Alt、Super等按钮的操作。GLFW会在合适的时候调用它,并为各个参数传入适当的值。

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    // 当用户按下ESC键,我们设置window窗口的WindowShouldClose属性为true
    // 设置状态为:应关闭窗口window,这只是状态函数,并不是在调用该函数时执行关闭动作
    if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);
}    

在我们(新创建的)key_callback函数中,我们检测了键盘是否按下了Escape键。如果键的确按下了(不释放),我们使用 glfwSetwindowShouldClose 函数设定 WindowShouldClose 属性为 true 从而关闭GLFW。main函数的while循环下一次的检测将为失败,程序就关闭了。

最后一件事就是通过GLFW注册我们的函数至合适的回调,代码是这样的:

glfwSetKeyCallback(window, key_callback); 
//该函数时通知GLFW当检测到输入时调用我们给的函数(这里为key_callback)
//要不然GLFW怎么知道当检测到输入时应该干什么呢~

除了按键回调函数之外,我们还能我们自己的函数注册其它的回调。例如,我们可以注册一个回调函数来处理窗口尺寸变化、处理一些错误信息等。我们可以在创建窗口之后,开始游戏循环之前注册各种回调函数。

整体代码大致这样:

#include<iostream>

#define GLEW_STATIC
#include<glew.h>
#include<glfw3.h>
#include<math.h>
int windowhigh = 720;
int windowwide = 720;
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) {
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);           
}

int main() {
    /**********part_mark**********/
    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);

	/**********part_mark**********/
    GLFWwindow* window = glfwCreateWindow(windowhigh, windowwide, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr) {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

	/**********part_mark**********/
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) {
        std::cout << "Failed to initialize GLEW" << std::endl;
        return -1;
    }
    int win_width = windowwide, win_height = windowhigh;
    glfwGetFramebufferSize(window, &win_width, &win_height);
    glViewport(0, 0, win_width, win_height);


    /**********part_mark**********/
    glfwSetKeyCallback(window, key_callback);
    //设置回调函数为key_callback
    while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
        //当检测到输入时glfwpollevent会调用之前设置的回调函数(即key_callback)
        float timeValue = glfwGetTime();
        render(timeValue);
        glClearColor(sin(timeValue)/2.0f+0.5f, sin(timeValue+1.0f) / 2.0f + 0.5f, sin(timeValue+2.0f) / 2.0f + 0.5f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        glfwSwapBuffers(window);
    }

    /**********part_mark**********/
    glfwTerminate();
    return 0;
}

当然我们是想通过输入来改变颜色(现在的颜色是跟随时间不断自动变换的),我们可以修改一下key_callback中的内容,当按一些键的时候变成一个固定的颜色,比如:

int color_control = 0;
//先添加一个全局变量,要不然怎么向渲染循环传递信息呢
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);
    if(key == GLFW_KEY_G && action == GLFW_PRESS)
   		color_control = 1- color_control;
   		//让color_control在0和1之间来回变换
}    

渲染循环如下:

while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
        float timeValue = glfwGetTime();
        render(timeValue);
        if (color_control == 0) {
            glClearColor(sin(timeValue) / 2.0f + 0.5f, sin(timeValue + 1.0f) / 2.0f + 0.5f, sin(timeValue + 2.0f) / 2.0f + 0.5f, 1.0f);
        }
        else {
            glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
        } 
        //当color_control 为0时颜色根据时间变化
        //当color_control 为1时颜色为我们设置的颜色(灰色)
        glClear(GL_COLOR_BUFFER_BIT);
        glfwSwapBuffers(window);
    }

这里按G就可以来回切换灰色和自动变色模式!!
当然你可以设置更多的全局变量,实现更复杂的颜色变化(比如按Q让红色变多,按A让红色变少,…,这样就可以做一个RGB调色盘,当然你还有更多的点子更好)

6.2 再再更进一步(鼠标控制)

如果我们能够通过鼠标的移动来改变颜色就好了(当然以后鼠标的还可以控制三维空间中的视线移动,实现3D第一人称射击游戏的视线移动控制,这样我们离3D游戏更近了一步~)。
类似于键盘输入,鼠标的位置也需要回调函数,也需要告诉GLFW我们的回调函数是哪个,这里我们就写了一个回调函数:

void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    std::cout << xpos << "   " << ypos << std::endl;
}

别忘了通知GLFW我们的鼠标回调函数是啥:

glfwSetCursorPosCallback(window, mouse_callback);

注意到这里的回调函数mouse_callback中几乎啥都没干,它只是把当前鼠标的XY位置通过cout输出了出来,我们运行一下发现,当我们的鼠标位于窗口左上角时XY值最小,右下角时最大(而且最大值就为我们设置的窗口大小,我这里设置的是720*720)
在这里插入图片描述
由于RGB值的范围是从0到1的,所以我们把xpos的值除以720(窗口的X的最大值,你的最大值可能不是720)同理把ypos除以720(窗口的Y的最大值,你的最大值可能不是720),这样就得到了两个根据位置变化的量,我们就可以把它传入到渲染过程中去,去控制颜色变化。
这里我让X对应红色,Y对应绿色,做一个红绿的混合。
看看回调函数:

float color_control_mousex = 0.0f;
float color_control_mousey = 0.0f;
//别忘了全局变量传递信息啊~
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    color_control_mousex = xpos / 720.0f;
    color_control_mousey = ypos / 720.0f;
    //std::cout << xpos << "   " << ypos << std::endl;
}

看看渲染循环:

while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
        float timeValue = glfwGetTime();
        render(timeValue);
        if (color_control == 0) {
            glClearColor(color_control_mousex, color_control_mousey, 0.0f, 1.0f);
            //这样左右移动鼠标控制红色的量,上下移动鼠标控制绿色的量
            //当然对角线上就是红绿1:1混合,也就是黄色
            //注意这里把蓝色设置为0,如果不是0那么对角线上就不一定是黄色哦,想想为什么~
        }
        else {
            glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
            //我们仍然保留了上面的键盘控制,仍然可以按G
        } 
        glClear(GL_COLOR_BUFFER_BIT);
        glfwSwapBuffers(window);
    }

来回在窗口中移动就可以看到颜色的变化了!!当然你可以按G直接显示灰色,或是再按一次来会玩~
你还有什么点子呢,欢迎讨论。

6.3 留个小问题

我在写这篇的时候想做一个按住G不动就让颜色是灰色,松开就变成自动变色模式,你能试试解决吗?
(你可能会遇到按住G后灰色总是闪烁的情况,但有解决方法哦,提示 action == GLFW_RELEASE )

7 总结

这一次我们真正打开了一个窗口,还填充了颜色,还能读取键盘鼠标的输入来改变我们窗口中颜色,下次我们将介绍一下替代sb7.h的框架包括写我们自己的stratup() render() shutdown(),这很简单,如果你有一定的基础现在就可以写一个简易版的了,下一篇会很轻松。

我们下篇见~~

声明
(声明本篇是基于LearnOpenGL内容的搬运,原网站的网址为 https://learnopengl-cn.readthedocs.io/zh/latest/ )欢迎浏览(如侵速删)

我看过的相关内容
以下并不是全看完了,大部分看了15%就看不下去了,实在是没看懂。(本人没什么计算机编程基础,算是野生程序员吧,很多内容都不能标准表述,望见谅)
如果你对opengl的工作有了一定的了解,我一开始也是从这里开始的,但是仍然有很多的不懂的,最后至今为止,我杂糅了很多的网站内容包括LearnOpenGL极客学院哔哩哔哩的闫令琪计算机图形学哔哩哔哩的傅老师的OpenGL课程、OpenGL编程指南"也称为红宝书"、OpenGL超级宝典"也称为蓝宝书"、当然还有很多的csdn文章O(∩_∩)O这就不介绍了,等用到是时候我在放链接吧O(∩_∩)O

这里面图形学比较易懂也很基础推荐可以作为开始(如果你是学OpenGL需要马上用,应该可以跳过,但是其中的内容很是很重要,这会让后面涉及变换透视的章节更加易懂,推荐大家看看),之后是蓝宝书或是极客学院翻译的教程比较推荐,这两个还是比较适合你我这样的新手的。
这里不推荐看的是红宝书,这本书我看了有点类似于字典那样的工具书,不太适合新手上手学,而且讲的也并不是很通俗易懂(可能是我的书版本比较老吧…)

加油
当然如果你对我有信心,我也会持续更新(虽然前路漫漫),跟大家一同进步(虽然很可能没人看(╥╯^╰╥),无所谓了,当然如有错误还请大家指正∠(°ゝ°),哪里不懂我会尽力解决,哪里说的不好也可以指出我会及时修改~)

我们下篇见~~

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值