阅读《计算机图形学编程(使用OpenGL和C++)》4

绘制一个对象,它的顶点数据需要发送给顶点着色器。通常会把顶点数据在C++端放入一个缓冲区,并把这个缓冲区和着色器中声明的顶点属性相关联。其步骤如下:

只做一次的步骤,一般放在 init() 中。

1、创建一个缓冲区。

2、将顶点数据复制进缓冲区。

如果是动画场景的话,每帧都要做,一般在 display() 中。

1、启用包含了顶点数据的缓冲区。

2、将这个缓冲区和一个顶点属性相关联。

3、启用这个顶点属性。

4、使用 glDrawArrays() 绘制对象。

在 OpenGL 中,缓冲区被包含在顶点缓冲对象 (Vertex Buffer Object, VBO) 中,通常在程序开始的时候统一创建。

当 glDrawArrays() 执行时,缓冲区中的数据开始流动,从缓冲区的开头开始,按顺序流过顶点着色器。顶点着色器对每个顶点执行一次。3D空间中的顶点需要3个数值,所以着色器中的顶点属性常常会以vec3类型接收到这3个数值。然后,对缓冲区中的每组这3个数值,着色器会被调用。指定 GL_TRIANGLES时,光栅化是逐个三角形完成的。对 glDrawArrays() 的调用通常在其他调整这个模型的渲染设置的命令之前。

OpenGL中还有一种相关结构,叫作顶点数组对象 (Vertex Array Object, VAO),至少创建一个VAO。

下面两个 OpenGL 命令分别创建 VAO 和 VBO,并返回它们的整数型ID。这两个命令各自有两个参数,1、创建多少个ID。2、用来保存返回的 ID 的数组。

glGenVertexArrays(GLsizei n, GLuint* arrays); 
glGenBuffers(GLsizei n, GLuint* buffers);

glBindVertexArray()命令将指定的 VAO 标记为“活跃”,这样生成的缓冲区就会和这个 VAO相关联。

glBindVertexArray(GLuint array)

每个缓冲区需要有在顶点着色器中声明的相应的顶点属性变量。顶点属性通常是着色器中首先被声明的变量。我们在顶点着色器中可以这样声明:

layout (location = 0) in vec3 position;

关键字 "in" 是输入 (input) 的意思,表示这个顶点属性将会从缓冲区中接收数值。

"vec3" 是着色器每次调用会抓到3个浮点类型数值,分别表示x、y、z,它们组成一个顶点数据。

变量名字 "position"。

"layout (location = 0)" 叫作 "layout修饰符",就是我们把顶点属性和特定缓冲区关联起来的方法。这个顶点属性的识别号是0。

现在我们绘制一个立方体。

要渲染一个场景以使它看起来是3D的,需要构建适当的变换矩阵。并将它们应用于模型的每个顶点。在顶点着色器中应用所需的矩阵运算是最有效的,并且习惯上会将这些矩阵从 C++/OpenGL 应用程序发送给着色器中的统一变量。

关键字 "uniform" 在着色器中声明统一变量。

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

关键字 "mat4"表示这些是4×4矩阵。因为3D变换是4×4,因此mat4是GLSL着色器统一中常用的数据类型。

在片段着色器光栅化之前,由顶点定义的图元被转换为片段。光栅化过程会线性插值顶点属性值,以便显示的像素能无缝连接建模的曲面。

统一变量类似初始化过的常量,并且在每次顶点着色器调用(即从缓冲区发送的每个顶点)中保持不变。统一变量本身不是插值的;无论有多少顶点,它始终包含相同的值。

在顶点着色器中看到顶点属性被声明为 "in",表示它们从缓冲区接收值。顶点属性还可以改为声明为 "out",它们会将值发送到管线中的下一个阶段。OpenGL 有一个内置的vec4变量名字叫作 gl_Position 为顶点位置声明的一个 "out" 变量。变换后的顶点将自动输出到光栅着色器,最终将相应的像素位置发送到片段着色器。

可以构建3个矩阵并将它们发送到统一变量。

模型矩阵在世界坐标空间中表示对象的位置和朝向。如果模型移动,需要不断重建该矩阵。

视图矩阵移动并旋转世界中的模型,以模拟相机在所需位置的效果。因为相机可以移动,所以它也需要每帧创建一次。根据所需的摄像机位置和朝向构建。将模型和视图矩阵结合成单个 "MV" 矩阵。

透视矩阵是一种变换,它根据所需的视锥提供3D效果。只需要创建一次,它需要使用屏幕窗口的宽度和高度(以及所需的视锥体参数),除非调整窗口大小,否则通常不变。

将 MV 和投影矩阵发送到相应的着色器统一变量。

main.cpp

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "Utils.h"

using namespace std;

#define numVAOs 1
#define numVBOs 2

float cameraX, cameraY, cameraZ;
float cubeLocX, cubeLocY, cubeLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs]; // OpenGL要求这些数值以数组的形式指定
GLuint vbo[numVBOs];

// 给display()用
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat;

void setupVertices(void)
{
    float vertexPositions[108] = {
        -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f, -1.0f, -1.0f,
         1.0f, -1.0f, -1.0f,  1.0f,  1.0f, -1.0f, -1.0f,  1.0f, -1.0f,
         1.0f, -1.0f, -1.0f,  1.0f, -1.0f,  1.0f,  1.0f,  1.0f, -1.0f,
         1.0f, -1.0f,  1.0f,  1.0f,  1.0f,  1.0f,  1.0f,  1.0f, -1.0f,
         1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f,  1.0f,  1.0f,  1.0f,
        -1.0f, -1.0f,  1.0f, -1.0f,  1.0f,  1.0f,  1.0f,  1.0f,  1.0f,
        -1.0f, -1.0f,  1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
        -1.0f, -1.0f, -1.0f, -1.0f,  1.0f, -1.0f, -1.0f,  1.0f,  1.0f,
        -1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f, -1.0f,
         1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,  1.0f,
        -1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f,  1.0f,
         1.0f,  1.0f,  1.0f, -1.0f,  1.0f,  1.0f, -1.0f,  1.0f, -1.0f,
    };

    glGenVertexArrays(1, vao); // 创建一个vao,并返回它的整数型ID存进数组vao中
    glBindVertexArray(vao[0]); // 激活vao
    glGenBuffers(numVBOs, vbo);// 创建两个vbo,并返回它们的整数型ID存进数组vbo中

    glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); // 激活vbo第0个缓冲区
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW); // 将包含顶点数据的数组复制进活跃缓冲区(这里是第0个VBO)
}

void init(GLFWwindow* window) {
    renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");
    cameraX = 0.0f;  cameraY = 0.0f;    cameraZ = 8.0f;
    cubeLocX = 0.0f; cubeLocY = -2.0f; cubeLocZ = 0.0f; // 沿Y轴下移以展示透视
    setupVertices();
}

void display(GLFWwindow* window, double currentTime)
{
    glClear(GL_DEPTH_BUFFER_BIT);
    glUseProgram(renderingProgram);

    // 获取MV矩阵和投影矩阵的统一变量的引用
    mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix"); 
    projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");

    // 构建透视矩阵
    glfwGetFramebufferSize(window, &width, &height);
    aspect = (float)width / (float)height;
    pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f); // 1.0472 radians = 60 degrees

    // 构建视图矩阵、模型矩阵和视图-模型矩阵
    vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
    mMat = glm::translate(glm::mat4(1.0f), glm::vec3(cubeLocX, cubeLocY, cubeLocZ));
    mvMat = vMat * mMat;

    // 将透视矩阵和MV矩阵复制给相应的统一变量
    glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat)); // GLM函数调用value_ptr()返回对矩阵数据的引用
    glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));

    // 将VBO关联给顶点着色器中相应的顶点属性
    glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);                  // 标记第0个缓冲区为“活跃”
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);  // 将第0个属性关联到缓冲区
    glEnableVertexAttribArray(0);                           // 启用第0个顶点属性

    // 调整OpenGL设置,绘制模型
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glDrawArrays(GL_TRIANGLES, 0, 36);                      // 执行该语句,第0个VBO中的数据将被传输给拥有位置0的layout修饰符的顶点属性中。这会将立方体的顶点数据发送到着色器。

}

int main(void)
{
    GLFWwindow* window;

    /* Initialize the library */
    if (!glfwInit())
        return -1;

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(400, 300, "Hello World", NULL, NULL); //没用到的参数分别用来允许全屏显示以及资源共享
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);

    GLenum err = glewInit();
    if (err != GLEW_OK)
    {
        std::cout << "Error: " << glewGetErrorString(err) << std::endl;
    }
    glfwSwapInterval(1); // 交互缓冲区间隔设为1,即每帧更新一次,交换间隔表示交换缓冲区之前等待的帧数,通常称为Vsync垂直同步
    init(window);

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        display(window, glfwGetTime()); // glfwGetTime()返回GLFW初始化之后经过的时间

        /* Swap front and back buffers */
        glfwSwapBuffers(window); // GLFW默认使用两个缓冲区

        /* Poll for and process events */
        glfwPollEvents(); // 处理窗口相关事件(如按键事件)
    }

    glfwDestroyWindow(window);
    glfwTerminate();
    return 0;
}

顶点着色器  vertShader.glsl

#version 460
layout (location = 0) in vec3 position;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
void main(void)
{ 
   gl_Position = proj_matrix * mv_matrix *vec4(position, 1.0);
}

片段着色器 fragShader.glsl

#version 460
out vec4 color;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
void main(void)
{ 
   color = vec4(1.0, 0.0, 0.0, 1.0);
}

结果如下:

阅读《计算机图形学编程(使用OpenGL和C++)》4

 

 这里建立了两个VBO,但只用了一个,将立方体顶点加载到第0个VBO缓冲区中。

init() 函数还给定了立方体和相机在世界中的位置。

对 translate() 函数的 GLM 调用的形式,构建一个变换矩阵:从单位矩阵和以向量的形式指定变换值。许多 GLM 变换操作使用这种方法。

vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));

注意着色器,它们都包含相同的统一变量声明块。并不总是一定要这样做,但在特定渲染程序中的所有着色器中包含相同的统一变量声明块通常是一种好习惯。

上一篇:QT中使用OpenGL的方法(一)


下一篇:OpenGL学习随笔(三)——2022.1.24