本章作为OpenGL学习的第三章节 在本章节我们将认识OpenGL的渲染管线 对管线内各个过程有一个初步的认识 |
★提高阅读体验★ ♠一级标题♥二级标题♥ 三级标题♥ 四级标题 |
按照蓝宝书第三章的内容管线按照顺序可以分为以下几个阶段:
什么是渲染管线
简单的理解就是一堆原始图像数据会经过几个阶段最终显示在屏幕上,这几个阶段组合起来就是渲染管线,本章节将简单的去认识管线的各个组成部分
我们在上一章的学习中已经简单的认识了顶点着色器,顾名思义,顶点着色器就是用来处理顶点数据
的阶段,它包含了以下几个特性
这一章节对细分曲面阶段介绍的不多,我们也先做认识吧,后学的章节会继续学习
OpenGL4.0引入的新特性,细分曲面阶段是将高级基元分解为许多更小、更简单的基元进行渲染的过程,举个例子:
细分曲面主要有三部分组成,细分曲面控制着色器、固定函数细分曲面引擎、细分曲面评估着色器
作为细分曲面第一阶段,控制着色器(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);
可以通过glCullFace接口设置剔除类型
光栅化是指哪些片段可被线或三角形等基元覆盖的过程,以下图为例
我们已经知道了需要绘制的几何图元(点、线、三角形),上左图的圆形
理论上一个圆形,无论怎么放大缩小都是一个理论上的圆形,但是,实际上我们屏幕显示是一个个像素格子,所以我们绘制圆到屏幕的时候需要计算以下几点:
以上就是光栅化的过程
参考:
OpenGL中着色器,渲染管线,光栅化
片段着色器在之前的章节我们已经有所接触,作为管线最后一个可编程的阶段,片段着色器的作用是给光栅化后所有的基元片段去着色,这里不再过多介绍,例子可以参考官方示例fragcolorfrompos
,在本篇后续内容会介绍顶点着色器传递数据到片段着色器
帧缓存是OpenGL图形管线的最后一个阶段,该缓存可以表示屏幕的可见内容,以及用于存储除颜色外每个像素值的其他内存区域
像素运算是保存为帧缓存对象前的一系列操作,包括下面几种:
判断像素是否丢弃
比较我们设置的参照值和模板缓存之间的大小
比较片段z值和深度缓存内容,更小的值会被渲染
就是把这一帧的内容(还未显示)与之前的内容进行与、或、异或等计算,这部分是OpenGL的一个高度可配置的阶段,书中后续章节具体介绍
到这里OpenGL的渲染管线基本就结束了,计算着色器可以看做是一个独立的工作项,他不直接参与图形的渲染,只做一些非渲染的计算任务,在本书中第十章节有具体介绍,后续再讲
第一部分内容我们学习如何向顶点着色器传递内容,并且实现在阶段之间传递数据
在第二章节的内容里我们认识了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_out
和fs_in
为了区分,以免出现混淆
下面是整体代码和效果,效果和第一阶段类似,加了一个在移动时三角形随时间变色的逻辑
#include
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)
https://github.com/KingSun5
本章东西比较多,但也只是对管线各阶段的初步认识,后续章节会更加细致的去学习,若是觉得博主的文章写的不错,不妨关注一下博主,点赞一下博文,另博主能力有限,若文中有出现什么错误的地方,欢迎各位评论指摘。
本文属于原创文章,转载请著名作者出处并置顶!!