zl程序教程

您现在的位置是:首页 >  工具

当前栏目

(第三章)OpenGL超级宝典学习:认识渲染管线

学习 渲染 超级 认识 OpenGL 第三章 宝典 管线
2023-09-11 14:19:58 时间
OpGL超级宝典学习:认识渲染管线

前言

本章作为OpenGL学习的第三章节
在本章节我们将认识OpenGL的渲染管线
对管线内各个过程有一个初步的认识


★提高阅读体验★

👉 ♠ 一级标题 👈

👉 ♥ 二级标题 👈

👉 ♣ 三级标题 👈

👉 ♦ 四级标题 👈

---


♠ 管线认知

按照蓝宝书第三章的内容管线按照顺序可以分为以下几个阶段:

  1. 顶点着色器
  2. 细分曲面
  3. 几何着色器
  4. 基元装配、裁剪和光栅化阶段
  5. 片段着色器
  6. 帧缓存运算
  7. 计算着色器

什么是渲染管线

简单的理解就是一堆原始图像数据会经过几个阶段最终显示在屏幕上,这几个阶段组合起来就是渲染管线,本章节将简单的去认识管线的各个组成部分


♥ 顶点着色器

我们在上一章的学习中已经简单的认识了顶点着色器,顾名思义,顶点着色器就是用来处理顶点数据的阶段,它包含了以下几个特性

  • OpenGL管线的第一个可编程阶段
  • 图形管线中唯一的必须阶段

♥ 细分曲面

这一章节对细分曲面阶段介绍的不多,我们也先做认识吧,后学的章节会继续学习

OpenGL4.0引入的新特性,细分曲面阶段是将高级基元分解为许多更小、更简单的基元进行渲染的过程,举个例子:

  • A模型由1000个三角面组成
  • 通过1000个三角面直接渲染A模型,面多顶点也多
  • 我们只导入了200个大三角面(高级基元)
  • 200个三角面在细分曲面阶段分裂成1000个面(低级基元)
  • 通过算法计算新的顶点位置

在这里插入图片描述

细分曲面主要有三部分组成,细分曲面控制着色器、固定函数细分曲面引擎、细分曲面评估着色器


♣ 细分曲面控制着色器

作为细分曲面第一阶段,控制着色器(Tessellation Control Shader)简称TCS主要是是划定了细分等级,我们写一个简单的细分曲面控制着色器

static const char * tcs_source[] =
{
    "#version 410 core                                                                 \n"
    "                                                                                  \n"
    "layout (vertices = 3) out;                                                        \n"
    "                                                                                  \n"
    "void main(void)                                                                   \n"
    "{                                                                                 \n"
    "    if (gl_InvocationID == 0)                                                     \n"
    "    {                                                                             \n"
    "        gl_TessLevelInner[0] = 5.0;                                               \n"
    "        gl_TessLevelOuter[0] = 5.0;                                               \n"
    "        gl_TessLevelOuter[1] = 5.0;                                               \n"
    "        gl_TessLevelOuter[2] = 5.0;                                               \n"
    "    }                                                                             \n"
    "    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;     \n"
    "}                                                                                 \n"
};

这是官方例子tessellatedtri当中的tcs代码,我们看其中包含了哪些要点

要点1: layout (vertices = 3) out, 这里设置贴片控制点的数量为3
要点2: gl_TessLevelInner, 内部复杂度划分,gl_TessLevelInner[0] = 5.0意味着将内部纵向划分为5块
要点3: gl_TessLevelOuter, 外部复杂度划分,gl_TessLevelOuter[0] = 5.0意味着一条边被分成五部分
要点4: gl_out, 内置输出参数数组,给下一阶段用的
要点5: gl_in, 内置输入参数数组,给自己用的,包含了所有顶点信息
要点6: gl_InvocationID, 内置参数,可以作为gl_in和gl_out的索引
这里比较难理解,我们可以看看下边图方便理解,三角形每条边被分成了5分,内部纵向划成5个区域,然后再连线分成小三角形

在这里插入图片描述


♣ 固定函数细分曲面引擎

这个阶段我们不用手动编辑,主要负责生产调用细分曲面评估着色器,也就是下一阶段着色器需要的参数


♣ 细分曲面评估着色器

Tessellation Evaluation Shader,简称TES作用和顶点着色器类似,处理由固定函数细分曲面引擎传来的新的顶点,我们简单看一下代码

static const char * tes_source[] =
{
    "#version 410 core                                                                 \n"
    "                                                                                  \n"
    "layout (triangles, equal_spacing, cw) in;                                         \n"
    "                                                                                  \n"
    "void main(void)                                                                   \n"
    "{                                                                                 \n"
    "    gl_Position = (gl_TessCoord.x * gl_in[0].gl_Position) +                       \n"
    "                  (gl_TessCoord.y * gl_in[1].gl_Position) +                       \n"
    "                  (gl_TessCoord.z * gl_in[2].gl_Position);                        \n"
    "}                                                                                 \n"
};

要点1: layout (triangles, equal_spacing, cw) in, 意旨三角形模式,equal_spacing, cw等其他限定符表示应沿着多边形边缘等距、顺时针环绕生成三角形
要点2: gl_TessCoord, 新生成的顶点的重心坐标


♣ 总结

这一段内容不好理解,博主看了好久感觉也只是一知半解,后续章节再深入学习,学习期间除了蓝宝书,也受到了不少文章的启发,建议大家看看,TCS和TES的加载和顶点着色器一样,这里不再贴完整代码,需要的同学可以看官方示例tessellatedtri结合蓝宝书可以有比较清晰的认识

Vulkan_曲面细分(Tessellation Shader)

GLSL Tessellation Shader的编程入门介绍

OpenGL 4.0的Tessellation Shader(细分曲面着色器)


♥ 几何着色器

几何着色器也是一个可编程的阶段,它接收一个图元的顶点,输出的是另一组的顶点,它的作用包含但不限于以下几点:

  • 修改图元顶点位置
  • 改变输出图元(三角形变点)
  • 减少输出顶点的数量

我们看一下官方示例tessellatedgstri中的代码实例

static const char * gs_source[] =
{
    "#version 410 core                                                                  \n"
    "                                                                                   \n"
    "layout (triangles) in;                                                             \n"
    "layout (points, max_vertices = 3) out;                                             \n"
    "                                                                                   \n"
    "void main(void)                                                                    \n"
    "{                                                                                  \n"
    "    int i;                                                                         \n"
    "                                                                                   \n"
    "    for (i = 0; i < gl_in.length(); i++)                                           \n"
    "    {                                                                              \n"
    "        gl_Position = gl_in[i].gl_Position;                                        \n"
    "        EmitVertex();                                                              \n"
    "    }                                                                              \n"
    "}                                                                                  \n"
};

要点1: layout (triangles) in, 声明输入的图元类型,这里输入的是三角形,还有别的类型
要点2: layout (points, max_vertices = 3) out, 声明输出的类型,这里输出类型是点,最大顶点数是3
要点3: EmitVertex(), 内置函数,在输出中生成一个点

该段几何着色器的作用是将原来的三角形绘制改成了顶点绘制,位置不变,我们看例子运行后的效果

在这里插入图片描述


♥ 基元装配、裁剪和光栅化

此阶段就是将我们在之前处理过的一系列顶点信息转为像素的过程


♣ 基元装配

这一阶段没什么东西,基元装配指的就是将顶点转为线和三角形的过程


♣ 裁剪

顾名思义,裁剪就是抛弃掉一些不需要显示的顶点


♦ 齐次坐标

  • 百度百科
https://baike.baidu.com/item/%E9%BD%90%E6%AC%A1%E5%9D%90%E6%A0%87/511284
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);

这里先补充一个关于坐标的概念,我们所有对顶点位置的信息设置用的都是一个vec4,xyzw,三维坐标都是xyz,这种n+1的表示方式称为齐次坐标,这里的w作用是表示透视关系


♦ 标准化设备空间

OpenGL把标准坐标规定在(-1,1)之间,xyz的最大范围在-1到1之间,所有的顶点坐标会根据w的值进行投影分割,投影坐标xy在-1到1,z在0到1之间才会被显示到屏幕上,其他会被抛弃


♣ 视口转化

我们的设备分辨率是各不相同的,在裁剪后我们获得了需要显示的归一化设备坐标,将这些坐标通过比例转换和偏移,最终映射到设备坐标即为视口转化

glViewport(GLint x,GLint y,GLsizei width,GLsizei height)

我们通过内置函数glViewport来设置视口大小,xy是视口左下角位置,width和height是视口大小,一般是设备宽高

void glDepthRange(GLdouble nearVal,GLdouble farVal)

通过glDepthRange接口设置z轴的显示范围,默认是0-1,可以改成0-0.5,超过的就会被裁剪了


♣ 剔除

在进一步处理三角形前,可以选择对其进行剔除判断,三角形分背面正面,正面就是我们可以通过视口看到的位置,背面就是看不到的位置,OpenGL默认是渲染全部三角形,开启剔除后会舍弃背面三角形

glEnable(GL_CULL_FACE);

通过设置内置接口glEnable可以开启剔除功能

glCullFace(GL_FRONT);
  • GL_BACK:剔除背面
  • GL_FRONT:剔除正面
  • GL_FRONT_AND_BACK:剔除正面背面

可以通过glCullFace接口设置剔除类型


♣ 光栅化

光栅化是指哪些片段可被线或三角形等基元覆盖的过程,以下图为例

在这里插入图片描述

我们已经知道了需要绘制的几何图元(点、线、三角形),上左图的圆形

理论上一个圆形,无论怎么放大缩小都是一个理论上的圆形,但是,实际上我们屏幕显示是一个个像素格子,所以我们绘制圆到屏幕的时候需要计算以下几点:

  • 几何圆上的线和三角形所需要占用的像素格
  • 占用像素格需要填充的颜色

以上就是光栅化的过程

参考: OpenGL中着色器,渲染管线,光栅化


♥ 片段着色器

片段着色器在之前的章节我们已经有所接触,作为管线最后一个可编程的阶段,片段着色器的作用是给光栅化后所有的基元片段去着色,这里不再过多介绍,例子可以参考官方示例fragcolorfrompos,在本篇后续内容会介绍顶点着色器传递数据到片段着色器


♥ 帧缓存运算

帧缓存是OpenGL图形管线的最后一个阶段,该缓存可以表示屏幕的可见内容,以及用于存储除颜色外每个像素值的其他内存区域


♣ 像素运算

像素运算是保存为帧缓存对象前的一系列操作,包括下面几种:

  • 裁剪测试

判断像素是否丢弃

  • 模板测试

比较我们设置的参照值和模板缓存之间的大小

  • 深度测试

比较片段z值和深度缓存内容,更小的值会被渲染

  • 混合和逻辑运算

就是把这一帧的内容(还未显示)与之前的内容进行与、或、异或等计算,这部分是OpenGL的一个高度可配置的阶段,书中后续章节具体介绍


♥ 计算着色器

到这里OpenGL的渲染管线基本就结束了,计算着色器可以看做是一个独立的工作项,他不直接参与图形的渲染,只做一些非渲染的计算任务,在本书中第十章节有具体介绍,后续再讲


♠ 数据传递

第一部分内容我们学习如何向顶点着色器传递内容,并且实现在阶段之间传递数据


♥ in和out关键字

在第二章节的内容里我们认识了out关键字,out关键字可以定义输出变量,例如输出颜色给绘制的三角形,在GLSL中就是使用in和out关键字来定义输出和输入的全局变量

顶点着色器代码

"#version 420 core                                                 \n"
"                                                                  \n"
"layout (location = 0) in vec4 offset;                             \n"
"                                                                  \n"
"void main(void)                                                   \n"
"{                                                                 \n"
"    const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0),  \n"
"                                   vec4(-0.25, -0.25, 0.5, 1.0),  \n"
"                                   vec4( 0.25,  0.25, 0.5, 1.0)); \n"
"                                                                  \n"
"    // Add 'offset' to our hard-coded vertex position             \n"
"    gl_Position = vertices[gl_VertexID] + offset;                 \n"
"}                                                                 \n"

要点1: offset我们用in关键地定义了一个向量offset,在设置gl_Position的时候加上了offset
要点2: layout (location = 0)layout布局设置,location用来标注向量的位置(具体作用关联后边讲)


♥ 设置输入

在上一段内容里,我们认识了用in关键字来定义输入变量,现在我们来学习如何给输入变量赋值

我们看下面一段代码

virtual void render(double currentTime)
{
    .....

	GLfloat attrib[] = { (float)sin(currentTime) * 0.5f,
						 (float)cos(currentTime) * 0.6f,
						 0.0f, 0.0f };

	glVertexAttrib4fv(0, attrib);

    ......
}

要点1: glVertexAttrib4fv(0, attrib)我们通过glVertexAttrib*()函数来给上一段的offset来赋值,第一个参数0就对应了layout (location = 0) in vec4 offset中的0,第二个参数就是具体值了


♥ 整体效果

我们已经学习了输入变量是如何设置的,现在我们写一段完整的代码,效果就是通过设置输入变量,去移动一个三角形,具体例子参考官方示例movingtri

class singlepoint_app : public sb7::application
{
    void init()
    {
        static const char title[] = "OpenGL SuperBible - Single Point";

        sb7::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    virtual void startup()
    {
        static const char * vs_source[] =
        {
            "#version 410 core                                                 \n"
            "                                                                  \n"
            "layout (location = 0) in vec4 offset;                             \n"
            "                                                                  \n"
            "void main(void)                                                   \n"
            "{                                                                 \n"
            "    const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0),  \n"
            "                                   vec4(-0.25, -0.25, 0.5, 1.0),  \n"
            "                                   vec4( 0.25,  0.25, 0.5, 1.0)); \n"
            "                                                                  \n"
            "    // Add 'offset' to our hard-coded vertex position             \n"
            "    gl_Position = vertices[gl_VertexID] + offset;                 \n"
            "}                                                                 \n"
        };

        static const char * fs_source[] =
        {
            "#version 410 core                                                 \n"
            "                                                                  \n"
            "out vec4 color;                                                   \n"
            "                                                                  \n"
            "void main(void)                                                   \n"
            "{                                                                 \n"
            "    color = vec4(0.0, 0.8, 1.0, 1.0);                             \n"
            "}                                                                 \n"
        };

        program = glCreateProgram();
        GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fs, 1, fs_source, NULL);
        glCompileShader(fs);

        GLuint vs = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vs, 1, vs_source, NULL);
        glCompileShader(vs);

        glAttachShader(program, vs);
        glAttachShader(program, fs);

        glLinkProgram(program);

        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
    }

    virtual void render(double currentTime)
    {
        static const GLfloat green[] = { 0.0f, 0.25f, 0.0f, 1.0f };
        glClearBufferfv(GL_COLOR, 0, green);

        glUseProgram(program);

        GLfloat attrib[] = { (float)sin(currentTime) * 0.5f,
                             (float)cos(currentTime) * 0.6f,
                             0.0f, 0.0f };

        glVertexAttrib4fv(0, attrib);

        glDrawArrays(GL_TRIANGLES, 0, 3);
    }

    virtual void shutdown()
    {
        glDeleteVertexArrays(1, &vao);
        glDeleteProgram(program);
    }

private:
    GLuint          program;
    GLuint          vao;
};

DECLARE_MAIN(singlepoint_app)

在这里插入图片描述


♠ 阶段传递数据

我们可以利用in和out关键字,在各着色阶段去传递变量。例如:我们可以在顶点着色器定义一个out全局变量,在片段着色器中获取他


♥ 着色器间的数据传递

  • 顶点着色器
"#version 420 core                                                 \n"
"                                                                  \n"
"layout (location = 0) in vec4 offset;                             \n"
"layout (location = 1) in vec4 color;                              \n"
"                                                                  \n"
"out vec4 vs_color;                                                \n"
"                                                                  \n"
"void main(void)                                                   \n"
"{                                                                 \n"
"    const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0),  \n"
"                                   vec4(-0.25, -0.25, 0.5, 1.0),  \n"
"                                   vec4( 0.25,  0.25, 0.5, 1.0)); \n"
"                                                                  \n"
"    // Add 'offset' to our hard-coded vertex position             \n"
"    gl_Position = vertices[gl_VertexID] + offset;                 \n"
"    vs_color = color;										       \n"
"}                                                                 \n"

要点1: vs_color我们定义了输出变量vs_color并且,给vs_color赋值color是我们的一个外部输入值

  • 片段着色器
"#version 420 core                             \n"
"                                              \n"
"in vec4 vs_color;                             \n"
"                                              \n"
"out vec4 color;                               \n"
"                                              \n"
"void main(void)                               \n"
"{                                             \n"
"    color = vs_color;                         \n"
"}                                             \n"

要点1: vs_color我们定义了和顶点着色器当中相同命名的一个输入变量,如此我们在片段着色器中就可以获取到顶点着色器当中定义的变量了


♥ 接口块

除去直接用in和out关键字定义的数据,针对可能存在的大量数据,我们还可以通过定义接口块来进行输入和输出变量

  • 顶点着色器
"#version 420 core                                                 \n"
"                                                                  \n"
"layout (location = 0) in vec4 offset;                             \n"
"layout (location = 1) in vec4 color;                              \n"
"                                                                  \n"
"out VS_OUT                                                         \n"
"{                                                                  \n"
"    vec4 color;                                                    \n"
"}vs_out;                                                          \n"
"                                                                  \n"
"void main(void)                                                   \n"
"{                                                                 \n"
"    const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0),  \n"
"                                   vec4(-0.25, -0.25, 0.5, 1.0),  \n"
"                                   vec4( 0.25,  0.25, 0.5, 1.0)); \n"
"                                                                  \n"
"    // Add 'offset' to our hard-coded vertex position             \n"
"    gl_Position = vertices[gl_VertexID] + offset;                 \n"
"    vs_out.color = color;										   \n"
"}                                                                 \n"

要点1: 我们在顶点着色器中用out关键字定义了一个VS_OUT结构,并定义了一个实例vs_out,目前结构体只有一个color变量

  • 片段着色器
"#version 420 core                             \n"
"                                              \n"
"in VS_OUT                                    \n"
"{                                             \n"
"    vec4 color;                               \n"
"}fs_in;                                       \n"
"                                              \n"
"out vec4 color;                               \n"
"                                              \n"
"void main(void)                               \n"
"{                                             \n"
"    color = fs_in.color;                      \n"
"}                                             \n"

要点1: 我们在片段着色器中用in关键字定义了一个和顶点着色器同名的VS_OUT结构,并定义了一个实例fs_out,结构体参数和顶点着色器保持一致
要点2: 我们可以通过同名的接口快VS_OUT实现阶段内数据的传递
要点3: 实例vs_outfs_in为了区分,以免出现混淆


♥ 整体效果

下面是整体代码和效果,效果和第一阶段类似,加了一个在移动时三角形随时间变色的逻辑

在这里插入图片描述

#include <sb7.h>

class test3_OpenGL : public sb7::application
{

	virtual void startup()
	{
		program = compile_shaders();
		glGenVertexArrays(1, &vao);
		glBindVertexArray(vao);
	}
	virtual void render(double currentTime)
	{
		static const GLfloat green[] = { 0.0f, 0.25f, 0.0f, 1.0f };
		glClearBufferfv(GL_COLOR, 0, green);

		glUseProgram(program);

		GLfloat attrib[] = { (float)sin(currentTime) * 0.5f,
							 (float)cos(currentTime) * 0.6f,
							 0.0f, 0.0f };

		glVertexAttrib4fv(0, attrib);

		GLfloat sColor[] = { (float)sin(currentTime),(float)cos(currentTime),0.0f, 1.0f };

		glVertexAttrib4fv(1, sColor);


		glDrawArrays(GL_TRIANGLES, 0, 3);
	}

	/
	// func:编写一个简单的着色器
	/
	GLuint compile_shaders(void)
	{
		GLuint vertex_shader;
		GLuint fragment_shader;
		GLuint program;

		//顶点着色器
		static const char * vs_source[] =
		{
			"#version 420 core                                                 \n"
			"                                                                  \n"
			"layout (location = 0) in vec4 offset;                             \n"
			"layout (location = 1) in vec4 color;                              \n"
			"                                                                  \n"
			"out VS_OUT                                                         \n"
			"{                                                                  \n"
			"    vec4 color;                                                    \n"
			"}vs_out;                                                          \n"
			"                                                                  \n"
			"void main(void)                                                   \n"
			"{                                                                 \n"
			"    const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0),  \n"
			"                                   vec4(-0.25, -0.25, 0.5, 1.0),  \n"
			"                                   vec4( 0.25,  0.25, 0.5, 1.0)); \n"
			"                                                                  \n"
			"    // Add 'offset' to our hard-coded vertex position             \n"
			"    gl_Position = vertices[gl_VertexID] + offset;                 \n"
			"    vs_out.color = color;										   \n"
			"}                                                                 \n"



		};
		//片段着色器
		static const char * fs_source[] =
		{
			"#version 420 core                             \n"
			"                                              \n"
			"in VS_OUT                                    \n"
			"{                                             \n"
			"    vec4 color;                               \n"
			"}fs_in;                                       \n"
			"                                              \n"
			"out vec4 color;                               \n"
			"                                              \n"
			"void main(void)                               \n"
			"{                                             \n"
			"    color = fs_in.color;                      \n"
			"}                                             \n"
		};

		vertex_shader = glCreateShader(GL_VERTEX_SHADER);
		glShaderSource(vertex_shader, 1, vs_source, NULL);
		glCompileShader(vertex_shader);

		fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
		glShaderSource(fragment_shader, 1, fs_source, NULL);
		glCompileShader(fragment_shader);


		program = glCreateProgram();
		glAttachShader(program, vertex_shader);
		glAttachShader(program, fragment_shader);
		glLinkProgram(program);

		glDeleteShader(vertex_shader);
		glDeleteShader(fragment_shader);

		return program;
	}


	void shutdown()
	{
		glDeleteVertexArrays(1, &vao);
		glDeleteProgram(program);
	}

private:
	GLuint          program;
	GLuint          vao;

};

DECLARE_MAIN(test3_OpenGL)



♠ 推送

  • Github
https://github.com/KingSun5

♠ 结语

本章东西比较多,但也只是对管线各阶段的初步认识,后续章节会更加细致的去学习,若是觉得博主的文章写的不错,不妨关注一下博主,点赞一下博文,另博主能力有限,若文中有出现什么错误的地方,欢迎各位评论指摘。
本文属于原创文章,转载请著名作者出处并置顶!!