• OpenGLEBO索引缓冲对象使用 安静点


    索引缓冲对象

     看下面的数据:

    float vertices[] = { 
    // 第一个三角形 
    0.5f, 0.5f, 0.0f,  // 右上 
    0.5f, -0.5f, 0.0f, // 右下
    -0.5f, 0.5f, 0.0f, // 左上
    // 第二个三角形 
    0.5f, -0.5f, 0.0f, // 右下 
    -0.5f, -0.5f, 0.0f,// 左下
    -0.5f, 0.5f, 0.0f  // 左上
    }; 

    如果我们要画2个三角形,使用上面的数据,最终这2个三角形会构成一个长方形(之所以是长方形不是正方形和像素有关系,后面会了解),使用下面代码绘制:

    glDrawArrays(GL_TRIANGLES, 0, 6);

     第三个参数之所以是6,是因为这里要绘制出6个顶点元素,结果:

    可以看到,有几个顶点叠加了。我们指定了右下角左上角两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。

     我们可以指定用线框模式来绘制,这样我们就能看的更加清晰直接了:

    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)

    这个代码执行一次就可以,没必要放在循环里, 

     

     为了不造成浪费,我们需要使用索引(元素)缓冲对象,首先更改一个顶点数组,将重复的去掉:

    float vertices[] = {
        0.5f, 0.5f, 0.0f,   // 右上角
        0.5f, -0.5f, 0.0f,  // 右下角
        -0.5f, -0.5f, 0.0f, // 左下角
        -0.5f, 0.5f, 0.0f   // 左上角
    };
    
    unsigned int indices[] = {
        // 注意索引从0开始! 
        // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
        // 这样可以由下标代表顶点组合成矩形
    
        0, 1, 3, // 第一个三角形
        1, 2, 3  // 第二个三角形
    };

    我们也可以不用元素缓冲对象,直接根据上面的这2个定义来处理,替换掉glDrawArray,但是不建议,最好还是用EBO:

            // glDrawArrays(GL_TRIANGLES, 0,6);
            // 还是绘制6个顶点,
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, indices);

     下面我们介绍EBO的使用流程,首先创建一个EBO对象:

    unsigned int EBO; 
    glGenBuffers(1, &EBO);

    与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER

     注意:我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,表示我们要从索引缓冲区渲染三角形。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:

    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

    glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取其索引。这意味着我们每次想要使用索引渲染对象时都必须绑定相应的EBO,这又有点麻烦。碰巧顶点数组对象也跟踪元素缓冲区对象绑定。 在绑定VAO时,绑定的最后一个元素缓冲区对象存储为VAO的元素缓冲区对象。然后,绑定到VAO也会自动绑定该EBO,比如下面代码:

    // GLAD的include文件包含所需的OpenGL头文件(如GL/GL.h),因此确保在其他需要OpenGL的头文件(如GLFW)之前包含GLAD。就是#include <glad/glad.h> 放在最前面
    #include <glad/glad.h> 
    #include <GLFW/glfw3.h>
    #include <iostream>
    float vertices[] = {
    0.5f, 0.5f, 0.0f, // 右上 
    0.5f, -0.5f, 0.0f, // 右下
    -0.5f, -0.5f, 0.0f, // 左下
    -0.5f, 0.5f, 0.0f // 左上
    };
    unsigned int indices[] = { // 注意,我们从零开始算! 
    0, 1, 3, // 第一个三角形 
    1, 2, 3 // 第二个三角形 
    };
    
    
    const char* vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";
    
    const char* fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
    "}\n\0";
    
    void framebuffer_size_callback(GLFWwindow* window, int width, int height);
    void processInput(GLFWwindow* window);
    
    int main() {
        // 初始化GLFW,只有初始化完成之后才能够使用GLFW的函数
        glfwInit();
        // GLFW配置设置
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        // 如果是苹果系统的话使用下面代码
    #ifdef __APPLE__ 
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    #endif  
    
        // 创建窗口 大小和名称
        GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
        if (window == NULL) {
            std::cout << "Failed to create GLFW window" << std::endl;
            // 此函数销毁所有剩余的窗口和光标
            glfwTerminate();
            return -1;
        }
        //GLFW将窗口的上下文设置为当前线程的上下文
        glfwMakeContextCurrent(window);
        // 告诉GLFW我们希望每当窗口调整大小的时候调用这个函数
        glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
        //GLAD
        // glad: 加载所有OpenGL函数指针
        if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
            std::cout << "Failed to initialize GLAD" << std::endl;
            return -1;
        }
    #pragma region 着色器
    
        // 创建和编译着色器程序 
    //顶点着色器
        unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
        glCompileShader(vertexShader);
        // 检查编译错误
        int success;
        char infoLog[512];
        glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
        if (!success) {
            glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
        }
        // 片段着色器
        unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
        glCompileShader(fragmentShader);
        // 检查编译错误
        glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
        if (!success) {
            glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
        }
        //着色器程序
        unsigned int shaderProgram = glCreateProgram();
        glAttachShader(shaderProgram, vertexShader);
        glAttachShader(shaderProgram, fragmentShader);
        glLinkProgram(shaderProgram);
        //链接错误检查
        glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
        if (!success) {
            glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
        }
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);
    
    #pragma endregion
    
        //创建VBO和VAO对象,并赋予ID
        unsigned int VBO, VAO, EBO;
        //创建1个VAO对象
        glGenVertexArrays(1, &VAO);
        //创建1个VBO对象
        glGenBuffers(1, &VBO);
        //绑定VBO和VAO对象
        glBindVertexArray(VAO);
        //缓冲对象如果绑定的是顶点属性则用:GL_ARRAY_BUFFER
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        //为当前绑定到target的缓冲区对象创建一个新的数据存储。
        //如果data不是NULL,则使用来自此指针的数据初始化数据存储
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
        //上面我们将数据存到了缓冲区对象中,下面就需要
        //告知Shader(着色器)如何解析缓冲里的属性值
        //第一个属性是从哪个位置开始解析
        //第二个属性是每个顶点属性由几个组合而成
        //第三个属性是数组中每个元素的类型
        //第四个表示是否标准化,这里暂时不需要,需要注意只有整型值才会有效,如果浮点型的数据不会起作用
        //第五个是步长,因为我们这个是数组中每3个元素组成一个顶点属性,而且是float类型
        //第六个是偏移量,我们这里为0,就是从0开始读取数组中的数据的
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
        //开启VAO管理的第一个属性值
        glEnableVertexAttribArray(0);
        //作为习惯,用完之后将VBO,VAO解绑,需要的时候可以重新绑定
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
        // EBO对象创建并绑定  

           glGenBuffers(1, &EBO); 

         glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); 

        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices),indices, GL_STATIC_DRAW);
        // 解绑EBO
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); 
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    
        // 渲染循环 一个循环就是一帧 只要是窗体不关闭,就会一直循环
        while (!glfwWindowShouldClose(window)) {
            processInput(window);
    
            // 在这里,我们将屏幕设置为了类似黑板的深蓝绿色
            glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
            // 调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
            glClear(GL_COLOR_BUFFER_BIT); //状态使用
    
    #pragma region 绘制三角形
    
    // 首先,要选择使用哪个着色器 每个着色器调用和渲染调用都会使用这个程序对象
            glUseProgram(shaderProgram);
    
            // 需要注意的是前面我们已经解绑了VAO,所以现在是无法解析数据的,所以我们需要重新绑定,
            // 至于数据我们已经存到缓冲区了
            glBindVertexArray(VAO);// 从数据数组中indexwei0处开始读取,每三个做一个三角形的顶点(这是在VAO定义的),。
            // glDrawArrays(GL_TRIANGLES, 0,6);
            // 还是绘制6个顶点,
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    
    #pragma endregion
    
                // glfw: 交换缓冲区 该函数在指定窗口的前后缓冲区交换
                // 前缓冲区:屏幕上显示的图像
                // 后缓冲区:正在渲染的图像
                // glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),
                // 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上
                glfwSwapBuffers(window);
            // 轮询IO事件(按键按下 / 释放、鼠标移动等)通过下面方法就可以是得窗体对鼠标做出的动作做出反应,比如关闭,移动窗体等
            glfwPollEvents();
        }
        // glfw: 回收前面分配的GLFW先关资源. 一定要注意,只有关闭窗体之后才会跳出while循环走到这一步!!!
        glfwTerminate();
    
        // 在while循环退出后释放内存:
        glDeleteVertexArrays(1, &VAO);
        glDeleteBuffers(1, &VBO);
        glDeleteProgram(shaderProgram);
    
        return 0;
    }
    // glfwGetKey函数:需要一个窗口以及一个按键作为输入;函数将会返回这个按键是否正在被按下
    void processInput(GLFWwindow* window)
    {
        // 如果按下了ESC键,设置窗体的关闭标志为true,代表窗体可以退出
        if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
            glfwSetWindowShouldClose(window, true);
    }
    
    // 当改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用
    void framebuffer_size_callback(GLFWwindow* window, int width, int height)
    {
        // 设置窗口维度
        // glViewport(前两参数为窗口左下角位置,3.宽度,4.高度)
        glViewport(0, 0, width, height);
    }

    结果还是会成功绘制,虽然已经解绑了EBO,因为在绑定VAO的时候自动绑定了该EBO,,所以我们在解绑EBO之后才能解绑VAO,顺序不能乱。然后在循环里使用的时候需要重新绑定EBO:

    // GLAD的include文件包含所需的OpenGL头文件(如GL/GL.h),因此确保在其他需要OpenGL的头文件(如GLFW)之前包含GLAD。就是#include <glad/glad.h> 放在最前面
    #include <glad/glad.h> 
    #include <GLFW/glfw3.h>
    #include <iostream>
    float vertices[] = {
    0.5f, 0.5f, 0.0f, // 右上 
    0.5f, -0.5f, 0.0f, // 右下
    -0.5f, -0.5f, 0.0f, // 左下
    -0.5f, 0.5f, 0.0f // 左上
    };
    unsigned int indices[] = { // 注意,我们从零开始算! 
    0, 1, 3, // 第一个三角形 
    1, 2, 3 // 第二个三角形 
    };
    
    
    const char* vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";
    
    const char* fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
    "}\n\0";
    
    void framebuffer_size_callback(GLFWwindow* window, int width, int height);
    void processInput(GLFWwindow* window);
    
    int main() {
        // 初始化GLFW,只有初始化完成之后才能够使用GLFW的函数
        glfwInit();
        // GLFW配置设置
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        // 如果是苹果系统的话使用下面代码
    #ifdef __APPLE__ 
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    #endif  
    
        // 创建窗口 大小和名称
        GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
        if (window == NULL) {
            std::cout << "Failed to create GLFW window" << std::endl;
            // 此函数销毁所有剩余的窗口和光标
            glfwTerminate();
            return -1;
        }
        //GLFW将窗口的上下文设置为当前线程的上下文
        glfwMakeContextCurrent(window);
        // 告诉GLFW我们希望每当窗口调整大小的时候调用这个函数
        glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
        //GLAD
        // glad: 加载所有OpenGL函数指针
        if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
            std::cout << "Failed to initialize GLAD" << std::endl;
            return -1;
        }
    #pragma region 着色器
    
        // 创建和编译着色器程序 
    //顶点着色器
        unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
        glCompileShader(vertexShader);
        // 检查编译错误
        int success;
        char infoLog[512];
        glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
        if (!success) {
            glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
        }
        // 片段着色器
        unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
        glCompileShader(fragmentShader);
        // 检查编译错误
        glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
        if (!success) {
            glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
        }
        //着色器程序
        unsigned int shaderProgram = glCreateProgram();
        glAttachShader(shaderProgram, vertexShader);
        glAttachShader(shaderProgram, fragmentShader);
        glLinkProgram(shaderProgram);
        //链接错误检查
        glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
        if (!success) {
            glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
        }
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);
    
    #pragma endregion
    
        //创建VBO和VAO对象,并赋予ID
        unsigned int VBO, VAO, EBO;
        //创建1个VAO对象
        glGenVertexArrays(1, &VAO);
        //创建1个VBO对象
        glGenBuffers(1, &VBO);
        //绑定VBO和VAO对象
        glBindVertexArray(VAO);
        //缓冲对象如果绑定的是顶点属性则用:GL_ARRAY_BUFFER
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        //为当前绑定到target的缓冲区对象创建一个新的数据存储。
        //如果data不是NULL,则使用来自此指针的数据初始化数据存储
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
        //上面我们将数据存到了缓冲区对象中,下面就需要
        //告知Shader(着色器)如何解析缓冲里的属性值
        //第一个属性是从哪个位置开始解析
        //第二个属性是每个顶点属性由几个组合而成
        //第三个属性是数组中每个元素的类型
        //第四个表示是否标准化,这里暂时不需要,需要注意只有整型值才会有效,如果浮点型的数据不会起作用
        //第五个是步长,因为我们这个是数组中每3个元素组成一个顶点属性,而且是float类型
        //第六个是偏移量,我们这里为0,就是从0开始读取数组中的数据的
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
        //开启VAO管理的第一个属性值
        glEnableVertexAttribArray(0);
        //作为习惯,用完之后将VBO,VAO解绑,需要的时候可以重新绑定
        glBindBuffer(GL_ARRAY_BUFFER, 0); 
        // EBO对象创建并绑定 
        glGenBuffers(1, &EBO);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); 
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices),indices, GL_STATIC_DRAW);
        // 解绑EBO
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        // 解绑VAO 在解绑EBO后面 
        glBindVertexArray(0);
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    
        // 渲染循环 一个循环就是一帧 只要是窗体不关闭,就会一直循环
        while (!glfwWindowShouldClose(window)) {
            processInput(window);
    
            // 在这里,我们将屏幕设置为了类似黑板的深蓝绿色
            glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
            // 调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
            glClear(GL_COLOR_BUFFER_BIT); //状态使用
    
    #pragma region 绘制三角形
    
    // 首先,要选择使用哪个着色器 每个着色器调用和渲染调用都会使用这个程序对象
            glUseProgram(shaderProgram);
    
            // 需要注意的是前面我们已经解绑了VAO,所以现在是无法解析数据的,所以我们需要重新绑定,
            // 至于数据我们已经存到缓冲区了
            glBindVertexArray(VAO);
            // 一定是要在VAO在的时候进行EBO的绑定
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
            // 从数据数组中indexwei0处开始读取,每三个做一个三角形的顶点(这是在VAO定义的),。
            // glDrawArrays(GL_TRIANGLES, 0,6);
            // 还是绘制6个顶点,
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    
    #pragma endregion
    
                // glfw: 交换缓冲区 该函数在指定窗口的前后缓冲区交换
                // 前缓冲区:屏幕上显示的图像
                // 后缓冲区:正在渲染的图像
                // glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),
                // 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上
                glfwSwapBuffers(window);
            // 轮询IO事件(按键按下 / 释放、鼠标移动等)通过下面方法就可以是得窗体对鼠标做出的动作做出反应,比如关闭,移动窗体等
            glfwPollEvents();
        }
        // glfw: 回收前面分配的GLFW先关资源. 一定要注意,只有关闭窗体之后才会跳出while循环走到这一步!!!
        glfwTerminate();
    
        // 在while循环退出后释放内存:
        glDeleteVertexArrays(1, &VAO);
        glDeleteBuffers(1, &VBO);
        glDeleteProgram(shaderProgram);
    
        return 0;
    }
    // glfwGetKey函数:需要一个窗口以及一个按键作为输入;函数将会返回这个按键是否正在被按下
    void processInput(GLFWwindow* window)
    {
        // 如果按下了ESC键,设置窗体的关闭标志为true,代表窗体可以退出
        if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
            glfwSetWindowShouldClose(window, true);
    }
    
    // 当改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用
    void framebuffer_size_callback(GLFWwindow* window, int width, int height)
    {
        // 设置窗口维度
        // glViewport(前两参数为窗口左下角位置,3.宽度,4.高度)
        glViewport(0, 0, width, height);
    }

      

    参考:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/#_10

  • 相关阅读:
    android 源码编译 问题 列表
    springboot总结
    设计模式学习笔记
    JWT入门1
    oauth2入门github
    mybatis面试题
    shiro入门
    knife4j swagger API文档
    pahole安装(编译)
    goMySql的逆向工程
  • 原文地址:https://www.cnblogs.com/anjingdian/p/16684428.html
Copyright © 2020-2023  润新知