Opengles shader讲解

转自http://m.blog.csdn.net/qq_31518167/article/details/51982781

什么是着色器

在 OpenGL ES 中你必须创建两种着色器:顶点着色器 (vertex shaders) 和片段着色器 (fragment shaders)。这两种着色器是一个完整程序的两半,你不能仅仅创建其中任何一个;想创建一个完整的着色程序,两个都是必须存在。

顶点着色器定义了在 2D 或者 3D 场景中几何图形是如何处理的。一个顶点指的是 2D 或者 3D 空间中的一个点。在图像处理中,有 4 个顶点:每一个顶点代表图像的一个角。顶点着色器设置顶点的位置,并且把位置和纹理坐标这样的参数发送到片段着色器。

然后 GPU 使用片段着色器在对象或者图片的每一个像素上进行计算,最终计算出每个像素的最终颜色。图片,归根结底,实际上仅仅是数据的集合。图片的文档包含每一个像素的各个颜色分量和像素透明度的值。因为对每一个像素,算式是相同的,GPU 可以流水线作业这个过程,从而更加有效的进行处理。使用正确优化过的着色器,在 GPU 上进行处理,将使你获得百倍于在 CPU 上用同样的过程进行图像处理的效率。

我们的第一个着色器的例子

顶点着色器

好吧,关于着色器我们说的足够多了。我们来看一个实践中真实的着色器程序。这里是一个 GPUImage 中一个基础的顶点着色器:

NSString *const kGPUImageVertexShaderString = SHADER_STRING

(

attribute vec4 position;

attribute vec4 inputTextureCoordinate;

varying vec2 textureCoordinate;

void main()

{

gl_Position = position;

textureCoordinate = inputTextureCoordinate.xy;

}

);

我们一句一句的来看:

attribute vec4 position;

像所有的语言一样,着色器语言的设计者也为常用的类型创造了特殊的数据类型,例如 2D 和 3D 坐标。这些类型是向量,稍后我们会深入更多。回到我们的应用程序的代码,我们创建了一系列顶点,我们为每个顶点提供的参数里的其中一个是顶点在画布中的位置。然后我们必须告诉我们的顶点着色器它需要接收这个参数,我们稍后会将它用在某些事情上。

attribute vec4 inputTextureCoordinate;

现在你或许很奇怪,为什么我们需要一个纹理坐标。我们不是刚刚得到了我们的顶点位置了吗?难道它们不是同样的东西吗?

其实它们并非一定是同样的东西。纹理坐标是纹理映射的一部分。这意味着你想要对你的纹理进行某种滤镜操作的时候会用到它。左上角坐标是 (0,0)。右上角的坐标是 (1,0)。如果我们需要在图片内部而不是边缘选择一个纹理坐标,我们需要在我们的应用中设定的纹理坐标就会与此不同,像是 (.25, .25) 是在图片左上角向右向下各图片高宽 1/4 的位置。在我们当前的图像处理应用里,我们希望纹理坐标和顶点位置一致,因为我们想覆盖到图片的整个长度和宽度。有时候你或许会希望这些坐标是不同的,所以需要记住它们未必是相同的坐标。在这个例子中,顶点坐标空间从 -1.0 延展到 1.0,而纹理坐标是从 0.0 到 1.0。

varying vec2 textureCoordinate;

因为顶点着色器负责和片段着色器交流,所以我们需要创建一个变量和它共享相关的信息。在图像处理中,片段着色器需要的唯一相关信息就是顶点着色器现在正在处理哪个像素。

gl_Position = position;

gl_Position 是一个内建的变量。GLSL 有一些内建的变量,在片段着色器的例子中我们将看到其中的一个。这些特殊的变量是可编程管道的一部分,API 会去寻找它们,并且知道如何和它们关联上。在这个例子中,我们指定了顶点的位置,并且把它从我们的程序中反馈给渲染管线。

textureCoordinate = inputTextureCoordinate.xy;

最后,我们取出这个顶点中纹理坐标的 X 和 Y 的位置。我们只关心 inputTextureCoordinate 中的前两个参数,X 和 Y。这个坐标最开始是通过 4 个属性存在顶点着色器里的,但我们只需要其中的两个。我们拿出需要的属性,然后赋值给一个将要和片段着色器通信的变量,而不是把更多的属性反馈给片段着色器。

在大多数图像处理程序中,顶点着色器都差不多,所以,这篇文章接下来的部分,我们将集中讨论片段着色器。

片段着色器

看过了我们简单的顶点着色器后,我们再来看一个可以实现的最简单的片段着色器:一个直通滤镜:

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;

void main()

{

gl_FragColor = texture2D(inputImageTexture, textureCoordinate);

}

这个着色器实际上不会改变图像中的任何东西。它是一个直通着色器,意味着我们输入每一个像素,然后输出完全相同的像素。我们来一句句的看:

varying highp vec2 textureCoordinate;

因为片段着色器作用在每一个像素上,我们需要一个方法来确定我们当前在分析哪一个像素/片段。它需要存储像素的 X 和 Y 坐标。我们接收到的是当前在顶点着色器被设置好的纹理坐标。

uniform sampler2D inputImageTexture;

为了处理图像,我们从应用中接收一个图片的引用,我们把它当做一个 2D 的纹理。这个数据类型被叫做 sampler2D ,这是因为我们要从这个 2D 纹理中采样出一个点来进行处理。

gl_FragColor = texture2D(inputImageTexture, textureCoordinate);

这是我们碰到的第一个 GLSL 特有的方法:texture2D,顾名思义,创建一个 2D 的纹理。它采用我们之前声明过的属性作为参数来决定被处理的像素的颜色。这个颜色然后被设置给另外一个内建变量,gl_FragColor。因为片段着色器的唯一目的就是确定一个像素的颜色,gl_FragColor 本质上就是我们片段着色器的返回语句。一旦这个片段的颜色被设置,接下来片段着色器就不需要再做其他任何事情了,所以你在这之后写任何的语句,都不会被执行。

就像你看到的那样,写着色器很大一部分就是了解着色语言。即使着色语言是基于 C 语言的,依然有很多怪异和细微的差别让它和普通的 C 语言有不同。

输入,输出,以及精度修饰 (Precision Qualifiers)

看一看我们的直通着色器,你会注意到有一个属性被标记为 “varying”,另一个属性被标记为 “uniform”。

这些变量是 GLSL 中的输入和输出。它允许从我们应用的输入,以及在顶点着色器和片段着色器之间进行交流。

在 GLSL 中,实际有三种标签可以赋值给我们的变量:

Uniforms

Attributes

Varyings

Uniforms 是一种外界和你的着色器交流的方式。Uniforms 是为在一个渲染循环里不变的输入值设计的。如果你正在应用茶色滤镜,并且你已经指定了滤镜的强度,那么这些就是在渲染过程中不需要改变的事情,你可以把它作为 Uniform 输入。 Uniform 在顶点着色器和片段着色器里都可以被访问到。

Attributes 仅仅可以在顶点着色器中被访问。Attribute 是在随着每一个顶点不同而会发生变动的输入值,例如顶点的位置和纹理坐标等。顶点着色器利用这些变量来计算位置,以它们为基础计算一些值,然后把这些值以 varyings 的方式传到片段着色器。

最后,但同样重要的,是 varyings 标签。Varying 在顶点着色器和片段着色器都会出现。Varying 是用来在顶点着色器和片段着色器传递信息的,并且在顶点着色器和片段着色器中必须有匹配的名字。数值在顶点着色器被写入到 varying ,然后在片段着色器被读出。被写入 varying 中的值,在片段着色器中会被以插值的形式插入到两个顶点直接的各个像素中去。

回看我们之前写的简单的着色器的例子,在顶点着色器和片段着色器中都用 varying 声明了 textureCoordinate。我们在顶点着色器中写入 varying 的值。然后我们把它传入片段着色器,并在片段着色器中读取和处理。

在我们继续之前,最后一件要注意的事。看看创建的这些变量。你会注意到纹理坐标有一个叫做 highp 的属性。这个属性负责设置你需要的变量精度。因为 OpenGL ES 被设计为在处理能力有限的系统中使用,精度限制被加入进来可以提高效率。

如果不需要非常高的精度,你可以进行设定,这或许会允许在一个时钟循环内处理更多的值。相反的,在纹理坐标中,我们需要尽可能的确保精确,所以我们具体说明确实需要额外的精度。

精度修饰存在于 OpenGL ES 中,因为它是被设计用在移动设备中的。但是,在老版本的桌面版的 OpenGL 中则没有。因为 OpenGL ES 实际上是 OpenGL 的子集,你几乎总是可以直接把 OpenGL ES 的项目移植到 OpenGL。如果你这样做,记住一定要在你的桌面版着色器中去掉精度修饰。这是很重要的一件事,尤其是当你计划在 iOS 和 OS X 之间移植项目时。

向量

在 GLSL 中,你会用到很多向量和向量类型。向量是一个很棘手的话题,它们表面上看起来很直观,但是因为它们有很多用途,这使我们在使用它们时常常会感到迷惑。

在 GLSL 环境中,向量是一个类似数组的特殊的数据类型。每一种类型都有固定的可以保存的元素。深入研究一下,你甚至可以获得数组可以存储的数值的精确的类型。但是在大多数情况下,只要使用通用的向量类型就足够了。

有三种向量类型你会经常看到:

vec2

vec3

vec4

这些向量类型包含特定数量的浮点数:vec2 包含两个浮点数,vec3 包含三个浮点数,vec4 包含四个浮点数。

这些类型可以被用在着色器中可能被改变或者持有的多种数据类型中。在片段着色器中,很明显 X 和 Y 坐标是的你想保存的信息。 (X,Y) 存储在 vec2 中就很合适。

在图像处理过程中,另一个你可能想持续追踪的事情就是每个像素的 R,G,B,A 值。这些可以被存储在 vec4 中。

矩阵

现在我们已经了解了向量,接下来继续了解矩阵。矩阵和向量很相似,但是它们添加了额外一层的复杂度。矩阵是一个浮点数数组的数组,而不是单个的简单浮点数数组。

类似于向量,你将会经常处理的矩阵对象是:

mat2

mat3

mat4

vec2 保存两个浮点数,mat 保存相当于两个 vec2 对象的值。将向量对象传递到矩阵对象并不是必须的,只需要有足够填充矩阵的浮点数即可。在 mat2 中,你需要传入两个 vec2 或者四个浮点数。因为你可以给向量命名,而且相比于直接传浮点数,你只需要负责两个对象,而不是四个,所以非常推荐使用封装好的值来存储你的数字,这样更利于追踪。对于 mat4 会更复杂一些,因为你要负责 16 个数字,而不是 4 个。

在我们 mat2 的例子中,我们有两个 vec2 对象。每个 vec2 对象代表一行。每个 vec2 对象的第一个元素代表一列。构建你的矩阵对象的时候,确保每个值都放在了正确的行和列上是很重要的,否则使用它们进行运算肯定得不到正确的结果。

既然我们有了矩阵也有了填充矩阵的向量,问题来了:“我们要用它们做什么呢?“ 我们可以存储点和颜色或者其他的一些的信息,但是要如果通过修改它们来做一些很酷的事情呢?

向量和矩阵运算,也就是初等线性代数

我找到的最好的关于线性代数和矩阵是如何工作的资源是这个网站的更好的解释。我从这个网站偷来借鉴的一句引述就是:

线性代数课程的幸存者都成为了物理学家,图形程序员或者其他的受虐狂。

矩阵操作总体来说并不“难”;只不过它们没有被任何上下文解释,所以很难概念化地理解究竟为什么会有人想要和它们打交道。我希望能在给出一些它们在图形编程中的应用背景后,我们可以了解它们怎样帮助我们实现不可思议的东西。

线性代数允许你一次在很多值上进行操作。假想你有一组数,你想要每一个数乘以 2。你一般会一个个地顺次计算数值。但是因为对每一个数都进行的是同样的操作,所以你完全可以并行地实现这个操作。

我们举一个看起来可怕的例子,CGAffineTransforms。仿射转化是很简单的操作,它可以改变具有平行边的形状 (比如正方形或者矩形) 的大小,位置,或者旋转角度。

在这种时候你当然可以坐下来拿出笔和纸,自己去计算这些转化,但这么做其实没什么意义。GLSL 有很多内建的函数来进行这些庞杂的用来计算转换的函数。了解这些函数背后的思想才是最重要的。

GLSL 特有函数

这篇文章中,我们不会把所有的 GLSL 内建的函数都过一遍,不过你可以在 Shaderific 上找到很好的相关资源。很多 GLSL 函数都是从 C 语言数学库中的基本的数学运算导出的,所以解释 sin 函数是做什么的真的是浪费时间。我们将集中阐释一些更深奥的函数,从而达到这篇文章的目的,解释怎样才能充分利用 GPU 的性能的一些细节。

step(): GPU 有一个局限性,它并不能很好的处理条件逻辑。GPU 喜欢做的事情是接受一系列的操作,并将它们作用在所有的东西上。分支会在片段着色器上导致明显的性能下降,在移动设备上尤其明显。step() 通过允许在不产生分支的前提下实现条件逻辑,从而在某种程度上可以缓解这种局限性。如果传进 step() 函数的值小于阈值,step() 会返回 0.0。如果大于或等于阈值,则会返回 1.0。通过把这个结果和你的着色器的值相乘,着色器的值就可以被使用或者忽略,而不用使用 if() 语句。

mix(): mix 函数将两个值 (例如颜色值) 混合为一个变量。如果我们有红和绿两个颜色,我们可以用 mix() 函数线性插值。这在图像处理中很常用,比如在应用程序中通过一组独特的设定来控制效果的强度等。

*clamp(): GLSL 中一个比较一致的方面就是它喜欢使用归一化的坐标。它希望收到的颜色分量或者纹理坐标的值在 0.0 和 1.0 之间。为了保证我们的值不会超出这个非常窄的区域,我们可以使用 clamp() 函数。 clamp() 会检查并确保你的值在 0.0 和 1.0 之间。如果你的值小于 0.0,它会把值设为 0.0。这样做是为了防止一些常见的错误,例如当你进行计算时意外的传入了一个负数,或者其他的完全超出了算式范围的值。

更复杂的着色器的例子

我知道数学的洪水一定让你快被淹没了。如果你还能跟上我,我想举几个优美的着色器的例子,这会更有意义,这样你又有机会淹没在 GLSL 的潮水中。

饱和度调整

这是一个做饱和度调节的片段着色器。这个着色器出自 《图形着色器:理论和实践》一书,我强烈推荐整本书给所有对着色器感兴趣的人。

饱和度是用来表示颜色的亮度和强度的术语。一件亮红色的毛衣的饱和度要远比北京雾霾时灰色的天空的饱和度高得多。

在这个着色器上,参照人类对颜色和亮度的感知过程,我们有一些优化可以使用。一般而言,人类对亮度要比对颜色敏感的多。这么多年来,压缩软件体积的一个优化方式就是减少存储颜色所用的内存。

人类不仅对亮度比颜色要敏感,同样亮度下,我们对某些特定的颜色反应也更加灵敏,尤其是绿色。这意味着,当你寻找压缩图片的方式,或者以某种方式改变它们的亮度和颜色的时候,多放一些注意力在绿色光谱上是很重要的,因为我们对它最为敏感。

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;

uniform lowp float saturation;

// Values from "Graphics Shaders: Theory and Practice" by Bailey and Cunningham

const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);

void main()

{

lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);

lowp float luminance = dot(textureColor.rgb, luminanceWeighting);

lowp vec3 greyScaleColor = vec3(luminance);

gl_FragColor = vec4(mix(greyScaleColor, textureColor.rgb, saturation), textureColor.w);

}

我们一行行的看这个片段着色器的代码:

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;

uniform lowp float saturation;

再一次,因为这是一个要和基础的顶点着色器通信的片段着色器,我们需要为输入纹理坐标和输入图片纹理声明一个 varyings 变量,这样才能接收到我们需要的信息,并进行过滤处理。这个例子中我们有一个新的 uniform 的变量需要处理,那就是饱和度。饱和度的数值是一个我们从用户界面设置的参数。我们需要知道用户需要多少饱和度,从而展示正确的颜色数量。

const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);

这就是我们设置三个元素的向量,为我们的亮度来保存颜色比重的地方。这三个值加起来要为 1,这样我们才能把亮度计算为 0.0 – 1.0 之间的值。注意中间的值,就是表示绿色的值,用了 70% 的颜色比重,而蓝色只用了它的 10%。蓝色对我们的展示不是很好,把更多权重放在绿色上是很有意义的。

lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);

我们需要取样特定像素在我们图片/纹理中的具体坐标来获取颜色信息。我们将会改变它一点点,而不是想直通滤镜那样直接返回。

lowp float luminance = dot(textureColor.rgb, luminanceWeighting);

这行代码会让那些没有学过线性代数或者很早以前在学校学过但是很少用过的人看起来不那么熟悉。我们是在使用 GLSL 中的点乘运算。如果你记得在学校里曾用过点运算符来相乘两个数字的话,那么你就能明白是什么回事儿了。点乘计算以包含纹理颜色信息的vec4 为参数,舍弃 vec4 的最后一个不需要的元素,将它和相对应的亮度权重相乘。然后取出所有的三个值把它们加在一起,计算出这个像素综合的亮度值。

lowp vec3 greyScaleColor = vec3(luminance);

最后,我们把所有的片段组合起来。为了确定每个新的颜色是什么,我们使用刚刚学过的很好用的 mix 函数。mix 函数会把我们刚刚计算的灰度值和初始的纹理颜色以及我们得到的饱和度的信息相结合。

这就是一个很棒的,好用的着色器,它让你用主函数里的四行代码就可以把图片从彩色变到灰色,或者从灰色变到彩色。还不错,不是吗?

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

推荐阅读更多精彩内容