OpenGL:渲染管线

OpenGL图形管线主要有:顶点着色器->曲面细分着色器->几何着色器->光栅化(将点图元或三角形图元转换成像素位置即片段)->片段着色器(生成像素颜色)->像素操作

其中上述的四种着色器可以由GLSL编写;将GLSL程序载入这些着色器阶段也是C++/OpenGL程序(一个C++程序包含OpenGL调用)的责任之一,其过程如下:

  1. 使用C++获取GLSL着色器代码,就可以从文件中读取,也可以硬编码在字符串中。

  1. 创建OpenGL的着色器对象,并将GLSL着色器代码加载到着色器对象中。

  1. 用OpenGL命令编译并链接着色器对象,将它们装载到GPU。

在实践中,一般至少需要提供顶点着色器和片段着色器的GLSL代码,而曲面细分着色器和几何着色器阶段是可以省略的

顶点着色器和片段着色器

你可能会惊讶于OpenGL只能绘制几类非常简单的东西,如点、线、三角形。这些简单的东西叫做图元。多数3D模型通常由许多三角形图元构成。图元由顶点组成,例如三角形有3个顶点。顶点可以有多个来源,比如从文件读取并且由C++/OpenGL应用载入缓冲区、直接在C++文件中硬编码、或者直接在GLSL代码中生成。

在加载顶点之前,C++/OpenGL应用程序必须编译并链接合适的GLSL顶点着色器和片段着色器程序,之后将它们载入管线。

所有的顶点都会被传入顶点着色器,顶点会被逐个处理,即着色器会对每个顶点执行一次。多拥有很多顶点的大型复杂模型而言,顶点着色器会执行成百上千甚至上百万次,这些执行过程通常都是并行的。下面通过一个示例,它仅包含硬编码于顶点着色器中的一个顶点;为了显示这个点还需要提供片段着色器片段:

#include <GL/glew.h>      //glew为OpenGL的扩展库。类似的还有GL3W、GLAD等
#include <GLFW/glfw3.h>   //glfw为窗口管理库
#include <iostream>
using namespace std;

#define numVAOs 1

GLuint renderingProgram;
GLuint vao[numVAOs];

GLuint createShaderProgram() {
    /*下面第一行指明OpenGL的一个版本,这里是4.3版。
       内置vec4变量gl_Position用来设置顶点在3D空间中的坐标位置,并将其发送至下一个管线阶段;这里需要
       注意给gl_Position指定out标签不是必须的,因为其是预定义的输出变量。
       数据类型vec4用来存储四元组,适合用来存储坐标,前三个值分别为xyz坐标,第四个值在这里设为1.0 */
    const char *vshaderSource =
        "#version 430    \n"
        "void main(void) \n"
        "{ gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }";
    /*顶点沿着管线经过光栅化处理后会在这里被转换成像素位置(更精确地说是片段)。最终这些像素(片段)
      到达片段着色器。所有片段着色器地目的都是给要展示地像素赋予颜色。在本例中所指的地输出颜色
      为(0.0, 0.0, 1.0, 1.0),代表蓝色,其中第四个值1.0是不透明度。在这里out标签表面color变量
      是输出变量。*/
    const char *fshaderSource =
        "#version 430    \n"
        "out vec4 color; \n"
        "void main(void) \n"
        "{ color = vec4(0.0, 0.0, 1.0, 1.0); }";
    /*分别创建每个着色器对象(初始值为空)的时候会返回一个整形ID作为后面引用它的序号*/
    GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
    GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
    /*创建一个程序对象,用来存储指向它整数ID*/
    GLuint vfprogram = glCreateProgram();
    /*glShaderSource将GLSL代码从字符串载入空着色器对象。其中第一个参数用来存放着色器的着色器对象,
    第二个参数存放着色器源代码中的字符串数量、第三个参数为包含源代码的字符串指针*/
    glShaderSource(vShader, 1, &vshaderSource, NULL);
    glShaderSource(fShader, 1, &fshaderSource, NULL);
    /*分别编译各个着色器*/
    glCompileShader(vShader);
    glCompileShader(fShader);
    /*OpenGL的"程序"对象包含一系列编译过的着色器,glAttachShader将着色器加入程序对象,接
    着glLinkProgram请求GLSL编译器,以确保它们的兼容性*/     
    glAttachShader(vfprogram, vShader);
    glAttachShader(vfprogram, fShader);
    glLinkProgram(vfprogram);

    return vfprogram;
}

void init(GLFWwindow* window) {
    renderingProgram = createShaderProgram();
    /*当准备将数据集发送给管线时,数据集是以缓冲区形式发送的。这些缓冲区最后都会被存入顶点数组对象VAO中。
    在本例中,我们向顶点着色器硬编码了一个点,因此不需要任何缓冲区。但是,即使拥有程序没有用到任何缓冲区,
    OpenGL仍然需要在使用着色器的时候拥有至少一个创建好的VAO,所以以下这2行用来创建OpenGL要求的VAO*/
    glGenVertexArrays(numVAOs, vao);
    glBindVertexArray(vao[0]);
}

void display(GLFWwindow* window, double currentTime) {
    /*glUseProgram并没有运行着色器,而是将着色器加载进硬件。这里将含有二个已编译着色器的程序载入OpenGL
    管线阶段(在GPU上!)*/
    glUseProgram(renderingProgram);
    /*OpenGL中默认点的大小是1像素,因此默认情况下我们的点最终被渲染成了单个像素。现在通过如下命令后,
    在栅格化阶段会设置该像素的大小为30个像素点*/
    glPointSize(30.0f);
    /*第一个参数表示图元的类型(对于三角形,我们使用GL_TRIANGLES)。第二个参数表示从哪个顶点开始绘制
    (通常是顶点0,即第一个顶点)。第三个参数表示总共要绘制的顶点数。当调用该函数时,管线中的GLSL代码便
    开始执行*/
    glDrawArrays(GL_POINTS, 0, 1);
}

int main(void) {
    if (!glfwInit()) { exit(EXIT_FAILURE); }
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    GLFWwindow* window = glfwCreateWindow(600, 600, "OpenGL Demo", NULL, NULL);
    /*创建GLFW窗口并不会自动将它于当前OpenGL上下文关联起来,因此我们需要调用glfwMakeContextCurrent */
    glfwMakeContextCurrent(window);
    if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
    /*glfwSwapInterval和glfwSwapBuffers二个命令用来开启垂直同步,因为GLFW窗口默认是双缓冲的*/
    glfwSwapInterval(1);

    init(window);

    while (!glfwWindowShouldClose(window)) {
        display(window, glfwGetTime());
        glfwSwapBuffers(window);
        /*处理窗口相关事件,比如按键事件 */
        glfwPollEvents();
    }

    glfwDestroyWindow(window);
    glfwTerminate();
    exit(EXIT_SUCCESS);
}

当然也可以从文件中读取GLSL代码

#include <string>
#include <iostream>
#include <fstream>
......

string readFile(const char *filePath) {
    string content;
    ifstream fileStream(filePath, ios::in);
    string line = "";
    while (!fileStream.eof()) {
        getline(fileStream, line);
        content.append(line + "\n");
    }
    fileStream.close();
    return content;
}

GLuint createShaderProgram() {
    //与上述相同,同时加入如下代码
    string vertShaderStr = readFile("vertShader.glsl");
    string fragShaderStr = readFile("fragShader.glsl");
    const char *vertShaderSrc = vertShaderStr.c_str();
    const char *fragShaderSrc = fragShaderStr.c_str();

    glShaderSource(vShader, 1, &vertShaderSrc, NULL);
    glShaderSource(fShader, 1, &fragShaderSrc, NULL);
    //构建如前的渲染程序
}

曲面细分着色器

可编程曲面细分阶段是在OpenGL 4.0引入的,它提供了一个曲面细分着色器以生成大量三角形,通常以网格的形式排列。

当在简单形状(比如在方形区域或曲面上)上需要很多顶点时,曲面细分着色器就发挥作用了。它在生成复杂地形时也很有用。对于这种情况,有时用GPU中的曲面细分着色器在硬件里生成三角形网格比在C++中生成要高效得多。

几何着色器

顶点着色器可赋予程序员一次操作一个顶点("按顶点"处理)的能力,片段着色器可赋予程序员一次操作一个像素("按片段"处理)的能力,几何着色器可赋予程序员一次操作一个图元("按图元"操作)的能力。

当到达几何着色器时,管线肯定已经完成了将顶点组合为三角形的过程(这个过程叫做图元组装),接下来几何着色器会让程序员可以同时访问每个三角形的所有顶点。

"按图元"处理有很多用途,可以让图元变形(比如拉伸或者缩小),还可以删除一些图元从而渲染物体上产生"洞"。

几何着色器也提供了生成额外图元的方法,这些方法也打开了通过转换简单模型得到复杂模型的"大门"。几何着色器有一种有趣的用法,就是在物体表面增加纹理,如凸起、"鳞"甚至"毛发"。

栅格化

3D世界中的点、三角形、颜色等全都要展现在一个2D显示器屏幕上。这个屏幕由栅格(矩形像素阵列)组成。3D物体栅格化后,会将物体的图元(通常为三角形)转换为片段。片段拥有关于像素的信息。栅格化过程确定了为了显示由3个顶点确定的三角形需要绘制的所有像素的位置。

栅格化操作本质上就是把三个顶点上的信息插值到这个三角形覆盖的的每个像素上,然后交给片段着色器。例如在glDrawArrays()之前调用如下代码来进行插值:

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)  //线性插值
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)  //插值过程会继续沿着栅格线填充三角形内部

栅格化不仅可以对像素插值,任何顶点着色器输出的变量和片段着色器的输入变量都可以基于对应的像素进行插值。我们将会使用该功能生成平滑的颜色渐变,实现真实光照及许多其他效果。

片段着色器

用于为栅格化的像素指的颜色。

在上面的示例中顶点着色器中顶点的输出坐标曾使用gl_Position。在片段着色器中,同样有一个内置变量可以让程序员访问输入片段的坐标,叫gl_FragCoord。例如

#version 430
out vec4 color;
void main(void)
{ if (gl_FragCoord.x < 295) color = vec4(1.0, 0.0, 0.0, 1.0)
   else color = vec4(0.0, 0.0, 1.0, 1.0)
}

像素操作

当我们在disypaly()中使用glDrawArrays()命令去绘制场景中的物体时,我们通常期望前面的物体挡住后面的物体,即只看物体的正面。为了实现这个效果,我们需要执行隐藏面消除HSR(Hidden Surface Removal)操作。

OpenGL可以精巧地协调二个缓冲区,即颜色缓冲区和深度缓冲区(Z-buffer),从而完成隐藏面消除。这二个缓冲区都和栅格的大小相同--对于屏幕上每个像素,在二个缓冲区都各有对应条目。

当绘制场景中的各种对象时,片段着色器会生成像素颜色。像素颜色会存放在颜色缓冲区中,而最终颜色缓冲区会被写入屏幕。当多个对象占据颜色缓冲区中的相同像素时,必须根据最接近观察者的对象来确定要保留的像素颜色。

隐藏面消除按照如下步骤(Z-buffer算法)完成。

1)在每个场景渲染前,将深度缓冲区全部初始化为表示最大深度的值。

2)当片段着色器输出像素颜色时,计算它到观察者的距离。

3)如果(对于当前像素)距离小于深度缓冲区存储的值,那么用当前像素颜色替换颜色缓冲区中的颜色,同时用当前距离替换深度缓冲区中的值;否则,抛弃当前像素。

例如:

一般情况display()可以重复调用,并且调用它的速率被称为帧率。也就是说,通过不断地快速重绘场景或帧,就可以实现动画。通常我们需要在渲染帧之前清除深度缓冲区,以便正确地进行HSR(不清除深度缓冲区有时会导致每个曲面都会被移除,从而黑屏)。默认情况下,OpenGL中的深度值范围为0.0~1.0。调用glClear(GL_DEPTH_BUFFER_BIT)就可以清除深度缓冲区,这时程序会使用默认值(1.0)填充深度缓冲区。

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

应对"Z冲突"伪影

在渲染多个对象时,OpenGL使用z-buffer算法来进行隐藏面消除。通常情况下,通过选择最接近相机的相应片段的颜色作为像素的颜色,这种方法可决定哪些物体的曲面可见并呈现到屏幕,而位于其他物体后面的曲面不应该被渲染。

然而有时候场景中的二个物体表面重叠并位于重合的平面中,这使得深度缓冲区算法难以确定应该渲染二个表面中的哪一个。当发生这种情况时,可能会导致渲染表面的某些部分使用其中一个对象的颜色,而其他部分则使用另一个对象的颜色。这种不自然的维影称为z冲突或者深度冲突。

如何应对呢?常用方法是稍微移动一个物体使得表面不再面。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张帅峰_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值