从0开始的OpenGL学习(二十四)-深入探究GLSL

本章主要对GLSL作一些深入的研究

引言

GLSL是一个强大的着色器语言,笔者对其研究也不过是些皮毛而已。学了本章之后,你不会一下子就成为着色器大师,或者弄出什么非常酷炫的东西。本章也不打算把GLSL的所有东西都呈现出来,如果你想完整的学习GLSL的所有内容,推荐你到Anton的网站上学习。

当然,多学点东西总是好的。对GLSL做一些深入的研究非常有必要,这样我们就能更好的在应用中组合使用GLSL的各个知识块,更快更好的实现想要的效果。

本章中,我们会讨论一些其他的内置变量,也会讨论数据块接口(重点是uniform块)。闲话少说,直接上正餐!

GLSL内置变量

目前为止,我们遇到的GLSL内置变量只有两个:gl_Position和gl_FragCoord。gl_Position表示顶点的位置,gl_FragCoord表示片元坐标。接下来我们将要讨论的五个内置变量(这只是很少一部分而已,不信你看):gl_PointSize(顶点尺寸),gl_VertexID(顶点ID),gl_FragCoord(片元坐标),gl_FrontFacing(是否正面)以及gl_FragDepth(片元深度)。这些内置变量有些是顶点着色器的,有些是片元着色器的,应该很容易区分,前两个是顶点着色器的,后三个是片元着色器的。

顶点着色器内置变量

gl_PointSize

gl_PointSize可以用来设置顶点的显示尺寸大小,当我们绘制的是顶点时,这个设置更加明显。

我们来试试这个效果。新建一个工程命名成GLSLComing,将必要的文件都拷贝进去,修改顶点着色器如下:

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    gl_PointSize = gl_Position.z;  //gl_PointSize是float类型,因此我们直接赋值就行了。
}

我们将顶点的z分量赋值给了gl_PointSize,这样当我们把视点往后移时,我们将看到绘制的顶点越来越大!


运行效果

附上源码给遇到困难的童鞋一点帮助。

gl_VertexID

当前正在被处理的顶点索引。如果你当前渲染的顶点是非索引形式的,那么它表示当前顶点的一个有效索引(已经处理的顶点数+首元素,有点像偏移)。如果你当前渲染的顶点是索引形式的,它就是一个从缓存中获取顶点的索引。

至于你可能会感到困惑的索引形式,glDrawElements绘制使用索引形式,glDrawArrays绘制使用非索引形式。

这个东西现在我们用不到,先把它介绍一下,备着以后用。

片元着色器变量

gl_FragCoord

对这个变量我们熟悉的很,在深度测试的时候就已经打过几次交到了。到目前为止,我们对其的认识就是其z分量是当前片元的深度值信息。当然,这个变量中可不仅有深度信息,用屁股想都知道肯定还有x和y的坐标信息。

值得高兴的是,gl_FragCoord变量中的x和y坐标就已经是窗口坐标了,原点在左下角,x的范围为0窗口宽度,y的范围为0窗口高度。有了这个信息,我们就可以对某些特殊位置做一些特殊处理了(例如某个地方是小地图,那么这个地方就不用绘制场景)。我们来做一些简单的实验,将窗口左半边的物体全部弄成绿色,右半边的物体全部弄成黄色:

void main()
{
  if (gl_FragCoord.x < 640)
    FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  else
    FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}

运行之后,效果如下:


运行效果

非常简单粗暴的效果,嘿嘿~

gl_FragDepth

gl_FragCoord变量虽然好用,但它有个缺点,那就是它是只读的。我们只能用其进行一些判断测试,无法修改它的内容。gl_FragDepth变量就稍稍弥补了其缺点,因为我们可以对其赋值从而修改片元的深度信息。

如果着色器没有给gl_FragDepth变量赋值,那么它就会自动从gl_FragCoord.z中获取。

另外,手动设置深度值有一个非常严重的问题,就是如果我们在片元着色器中对gl_FragDepth赋值了,那么OpenGL会自动禁用前期深度测试(early depth testing)。原因是OpenGL在运行片元着色器之前无法知道某个片元的深度值(前期深度测试是在片元着色器运行之前进行的)。所以,在使用gl_FragDepth变量时,你也需要稍稍考虑一下性能问题。

从OpenGL 4.2版本之后,我们有一种新的方法来缓解这个问题,那就是用下面这行代码来设置gl_FragDepth的条件:

layout (depth_<condition>) out float gl_FragDepth;

条件参数可以取以下这些值:

条件 描述
any 默认值。前期深度测试关闭,可能造成性能损失
greater 只能设置比gl_FragCoord.z大的值
less 只能设置比gl_FragCoord.z小的值
unchanged 如果你写入gl_FragDepth,这个值也会写入gl_FragCoord.z

通过指定greater或者less条件,OpenGL会认为你只会设置比gl_FragCoord.z大或者小的值,那么它仍然可以进行前期深度测试。另外两个参数则会禁用前期深度测试。

注意,这个设置只在4.2及更高版本有效

数据块接口

数据块,类似于C语言中的结构体,将一些变量集中起来进行管理。随着实现的功能越来越多,效果越来越华丽,我们需要用到的变量也会疯狂增加,那么,我们自然要想个办法将这些变量管理起来。根据经验,将变量整合到一个结构中是非常好的一个选择,于是,数据块就应运而生了。

根据变量的作用,一共有四种数据块:in块、out块、uniform块、buffer块。in/out块非常容易理解,就是将输入和输出变量整合到一起,uniform块将uniform变量整合到一起,buffer块还没到用它的时候,先无视。

in/out块没啥好介绍的,简单给出一个例子:

//顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out VS_OUT
{
    vec2 TexCoords;
} vs_out;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);    
    vs_out.TexCoords = aTexCoords;
}  

//片元着色器
#version 330 core
out vec4 FragColor;

in VS_OUT
{
    vec2 TexCoords;
} fs_in;

uniform sampler2D texture;

void main()
{             
    FragColor = texture(texture, fs_in.TexCoords);   
} 

顶点着色器中定义了一个输出块,名字是VS_OUT,注意这是块名字,这个块的实例名是vs_out。要使用其中的元素,只需vs_out.TexCoords就可以了,和C中访问结构体成员一样。

相应的,片元着色器中需要定义一个名字一样的块,作为输入块,不过实例的名字可以不同。像上面的代码那样,片元着色器中的实例名是fs_in。使用方式和之前一样,fs_in.TexCoords即可使用。

(是不是觉得没啥实际用处,笔者开始也这么觉得~)

uniform块

之所以要将uniform块单独拿出来讲,是因为它重要并且理解较难。要定义一个uniform块,我们可以简单地像定义in/out块那样,也可以给它指定一些参数,这些参数就是难点所在!

#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;

layout (std140) uniform Matrics {
    mat4 view;
    mat4 projection;
};

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

如上面的代码所写,我们把view和projection都放在了uniform块中(因为这两个矩阵不太会变,我们只要设置一次就好了,之前每次都设置太麻烦了。)如何去获取uniform块中的值我们后面再讲,这里先聚焦于新出现的layout(std140)上。

unifrom块布局

layout(std140)指定了uniform块中的数据内存对齐的方式(这是从C语言copy过来的名词)。C语言的struct结构体中,我们有时也会对其布局进行调整,不过时候不多,而且不需要我们去指定其内存对齐方式,因为我们用它默认的就好了。但是在GLSL中,我们对它的布局就非常感兴趣了,因为我们必须通过变量的位置去操作变量,对OpenGL来说着色器程序中的变量名是不可见的。

可以用的布局限定符如下所示:

布局限定符 描述
shared 设置uniform块是多个程序间共享的(默认布局方式)
packed 设置uniform块占用最小的内存空间,但是这样会禁止程序间共享这个块
std140 1.4版本之后的标准布局方式
std430 4.3版本之后的标准布局方式
row_major 使用行主序的方式来存储uniform块中的矩阵
column_major 使用列主序的方式来存储uniform块中的矩阵(默认值)

layout(std140)指定了使用1.4版本之后的标准布局方式,没有设置矩阵是行主序还是列主序,默认就使用列主序,等价于layout(std140, row_major)。

默认情况下,GLSL使用的布局方式是shared。这种布局方式表示一旦偏移量被硬件确定之后,它就能在多个程序之间共享。使用共享布局,只要变量的顺序不变,GLSL就可以优化并重新定位uniform变量。但是我们不知道某个变量的偏移量,可以通过glGetUniformIndices函数来查询,这个超出了本章的讨论范围,我们暂时不进行说明。

因为shared布局会优化变量占用的内存空间,所以我们无法事先知道变量的偏移量,也就无法通过硬编码去设置变量。这里我们用一种更确切的方式定死每个变量的偏移,从而可以通过偏移量来设置变量,这种布局方式就是std140,先介绍一下std140的布局规则:

变量类型 布局规则
标量类型:bool,float等 大小和对齐值都是基本机器类型的标量大小
两个分量的向量(如vec2) 大小和对齐值是基础类型大小的2倍
三分量向量和4分量向量(vec3,vec4) 大小和对齐值是基础标量类型大小的4倍
数组 数组中每个元素大小取整到vec4大小的整数倍
矩阵 类似于包含n个向量的数组,n是列总数
结构体 对齐值是最大结构成员的对齐值,取整到vec4大小的整数倍

感觉很复杂的样子?不要紧,我们来看一个例子就清楚了:


layout (std140) uniform ExampleBlock
{
                     // 大小            // 对齐值
    float value;     // 4               // 0 
    vec3 vector;     // 16              // 16  (必须对齐到16)
    mat4 matrix;     // 16              // 32  (第0列)
                     // 16              // 48  (第1列)
                     // 16              // 64  (第2列)
                     // 16              // 80  (第3列)
    float values[3]; // 16              // 96  (values[0])
                     // 16              // 112 (values[1])
                     // 16              // 128 (values[2])
    bool boolean;    // 4               // 144
    int integer;     // 4               // 148
}; 

计算清楚每个变量的偏移之后,我们就可以使用glBufferSubData函数来给变量赋值(填充内存块)了。std140保证了在每个从程序中uniform块的布局都是一致的,虽然可能有点浪费内存。

使用uniform缓存对象

OpenGL提供了一个名叫uniform缓存对象(uniform buffer objects)的工具来声明一组全局的uniform变量。使用这个对象,我们只需要对变量进行一次设置就行。要使用uniform缓存对象我们还是需要平常的三部曲工作:

unsigned int uboMatrices;
glGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW);

生成-绑定-分配内存!无论我们想啥时候对其中的变量进行赋值,我们都可以绑定uboMatrices,然后调用glBufferSubData函数来赋值。但是,OpenGL怎么知道缓存对象绑定的是哪一个uniform块呢?

在OpenGL环境中,有许多内置的绑定点,我们可以将uniform缓存绑定到其中一个上,然后在着色器中将uniform块绑定到相同的位置,这样,双方的数据就可以互通了。

要设置uniform块的绑定点,我们需要调用glUniformBlockBinding函数,将需要绑定的位置作为参数传入。该函数的原型如下:void glUniformBlockBinding( GLuint program,GLuint uniformBlockIndex,GLuint uniformBlockBinding);它需要3个参数,分别是:着色器ID,uniform块索引,以及一个绑定点。unifrom块索引可以通过glGetUniformBlockIndex获取,传入着色器ID和uniform块名就可以了。比如我们想将ExampleBlock绑定到位置2,就可以这么实现:

unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "ExampleBlock");   
glUniformBlockBinding(shaderA.ID, lights_index, 2);

如果有多个着色器共享这个uniform块,那么需要多次调用这段代码将每个着色器的相应块都绑定到2位置。

OpenGL 4.2之后可以在布局中指定绑定点,这样就需用在程序中调用函数了,比如:layout (std140, binding = 2) uniform ExampleBlock{...};

绑定完着色器中的uniform块之后,我们还要将ubo绑定到位置2。调用glBindBufferBase或者glBindBufferRange都是不错的选择。

glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboMatrices); 
// or
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboMatrices, 0, 152);

两个函数都需要BUFFER类型,绑定点,以及uboID作为参数。不同的是,glBindBufferRange函数可以指定绑定的范围,而glBindBufferBase会将整个区间都绑定。

完成绑定后,调用glBufferSubData就可以对某个位置写入数据了(注意笔者这里说的是某个位置,不是某个变量,因为我们在应用程序中是“看”不到变量名的,只能通过偏移值来对某块内存赋值):

glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // 在glsl中bool值是4字节的,与int占用内存一样,所以我们保存成int型
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b); 
glBindBuffer(GL_UNIFORM_BUFFER, 0);

一个简单的例子

照例,学完新知识后弄一个简单的东西来当练习。现在,要用4个着色器分别应用到4个盒子上,着色器有公用的uniform块,显示不同的颜色,我们来实现它。回顾之前的所有例子,观察矩阵和投影矩阵显然是可以共用的,我们把它放到一个uniform块中。块名就叫做Matrices:

#version 330 core
layout (location = 0) in vec3 aPos;

layout (std140) uniform Matrices
{
    mat4 projection;
    mat4 view;
};
uniform mat4 model;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
} 

接下来,把每个着色器的Matrices块都绑定到0位置:

unsigned int uniformBlockIndexRed    = glGetUniformBlockIndex(shaderRed.ID, "Matrices");
unsigned int uniformBlockIndexGreen  = glGetUniformBlockIndex(shaderGreen.ID, "Matrices");
unsigned int uniformBlockIndexBlue   = glGetUniformBlockIndex(shaderBlue.ID, "Matrices");
unsigned int uniformBlockIndexYellow = glGetUniformBlockIndex(shaderYellow.ID, "Matrices");  
  
glUniformBlockBinding(shaderRed.ID,    uniformBlockIndexRed, 0);
glUniformBlockBinding(shaderGreen.ID,  uniformBlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.ID,   uniformBlockIndexBlue, 0);
glUniformBlockBinding(shaderYellow.ID, uniformBlockIndexYellow, 0);

再然后,生成一个uniform缓存对象,并绑定到绑定点0处:

unsigned int uboMatrices
glGenBuffers(1, &uboMatrices);
  
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
  
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));

一切准备就绪,是时候往指定位置填充数据了。使用sizeof(glm::mat4)的方式来计算变量占用的空间和偏移值,生成view和projection矩阵后用glBufferSubData函数将数据复制到指定内存位置去:

glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);  

glm::mat4 view = camera.GetViewMatrix();           
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);  

上面的操作都是针对uniform块中的变量的。剩下的就是平常的操作了,没什么花头,跟着之前的代码走就行了:

glBindVertexArray(cubeVAO);
shaderRed.use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f));  // 移到左上角
shaderRed.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);        
// ... 绘制绿色盒子
// ... 绘制蓝色盒子
// ... 绘制黄色盒子     

整理完代码之后编译运行,你看到的结果应该和下面的图类似:


运行结果

这正是我们想要的结果!附上源码以供参考。

总结

本章中,我们学习了GLSL的一些有趣的内置变量,包括gl_PointSize(顶点尺寸),gl_VertexID(顶点ID),gl_FragCoord(片元坐标),gl_FrontFacing(是否正面)以及gl_FragDepth(片元深度)。着重学习了数据块接口中的uniform块。使用uniform块,我们需要将需要共享的uniform块绑定到OpenGL的相同绑定点,然后用OpenGL的GL_UNIFORM_BUFFER绑定到一致的位置,通过操作uniform缓存对象来设置着色器中uniform块中的变量值。节省了对每个着色器都设置一遍的累赘操作。在最后的例子中,亲眼看到了学习的操作方式可行,不是停留在理论层面的飘飘然的东西。

呼~好累啊,休息休息

下一篇
目录
上一篇

参考资料

www.learnopengl.com(非常好的网站,建议学习)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容