原文标题:《Shaders for dummies》
作者:Ignatz
译者:FreeBlues
译文链接:http://my.oschina.net/freeblues/blog/336055
PDF链接:http://pan.baidu.com/s/1c0xTUzI
译者注:
1、Codea
是一款可以在 Ipad
上直接编写游戏的 APP
应用软件,它使用 Lua
的语法和库,并针对 iPad
提供了一系列函数,诸如绘图、触摸、摄像头、重力感应乃至网络等等,Codea
支持 OpenGL ES
,它编写的程序可以直接在 iPad
上运行,也可以导出为 Xcode
项目,再用 Xcode
编译为可发布在 App Store
的应用程序。
2、本教程讲述的内容适用于 Codea
环境,跟 OpenGL ES
在其他开发环境的使用会有一些不同。
Codea
建构于 OpenGL ES Shading Language
(开放图形库 嵌入式系统 渲染语言)之上,它(OpenGL
)提供了非常复杂的工具,以及一连串复杂处理,用来把像素绘制到屏幕上。
在这一系列处理中有两个步骤:Vertex Shader
(顶点着色) 和 Fragment Shader
(片段着色),Codea
允许我们把这两者混合在一起来使用。为什么应该为此而兴奋?因为它给了你访问底层图形的能力,允许你创建非常强有力的视觉效果。
顶点着色器Vertex Shader
允许你一次修改一个顶点(一个顶点就是一个三角形的一个角,记住在计算机中所有的图形都是由三角形组成的)
译者注:如下所示:
片段着色器Fragment shaders
允许你一次修改一个像素点的颜色(以及纹理贴图的坐标位置)。
这些听起来都很复杂,不过别被吓跑。
着色器Shader
听起来非常神秘和困难,但是实际上它们并非那样的。
这个教程尽量不使用专业术语,也尽量不使用矩阵。不需要太多的数学。大多数是一些简单的例子。
不过有一个警告:它并不是为所有的初学者准备的。
如果你不满足下面这些描述就别再往后看了:
编程让你感觉很轻松舒服
熟悉 Codea
中的 mesh
(画刷) 和 image texture
(图形的纹理贴图),并且–>
准备好学习一丁点 C
语言(我保证只要一丁点!)
返回目录
我读过一些关于着色器
是什么的解释,它们谈到了 pipelines(管道)、vectors(向量)、rasterisation(图形栅格化)、scissor tests(剪切测试)
,以及一些复杂的示意图。这种讨论直接让我远离了对着色器
的学习好几个月。
我确信,像我一样,你也会喜欢真正简单明了的解释,没有任何上述的险恶术语。
OpenGL
就像一个长长的管道。你从这一头把你的绘图指令(如sprite,mesh
等)放进去,像素点会从另一头出来,这些像素点会在你的屏幕上形成一幅 3D 图像。在管道内部是一系列复杂处理,我们不会假装理解。
因此让我们聚焦到管道外。
在管道上有两个位置被挖了两个洞,因此你能通过这两个洞看到管道里流过的信息,并且你还可以进到里面去修改这些信息,这些信息处于整个体系的最底层,下图是细节:
在 OpenGL
管道上的两个洞允许你改变里面的信息流,不过它们假定你知道自己在做什么,并且在这里写的代码不像 Codea
中那么简单和宽容–任何错误都可能会简单地导致你的屏幕一片空白。你无法做出任何打断。
无论如何我们都会大胆地偷窥这两个洞。不过首先,我们得了解更多关于这两个洞里的信息流在干什么,这样我们才能理解我们在洞里看到的。
在制作动画片时,熟练的艺术家不会画出每一个单独帧。他们绘制关键帧,然后让其他人(或者现在是计算机)去填充位于关键帧之间的中间帧,这个处理被称为 tweening
。
类似地,在 2D 和 3D 图形处理中,我们必须指出我们所画的位置和颜色,不过我们只需要对一些点的样本集合(译者注:类似于关键帧)进行操作,这些点被称为 vertex(顶点)
。实际上,我们创造了一个线框模型。
OpenGL
接着会添加插入这些样本点之间的所有点。具体的方法是:把这些点组成三角形-因为这是最简单的形状,因此它用三个角的顶点值来计算三角形里所有点的值。
就像上图一样。看看红色角、绿色角和蓝色角的颜色是如何在三角形内部混合起来的。它确实很简单。
并且这种方法不仅被应用在 3D 上,也被应用在 2D 上,并且不仅被用于 mesh,也被用于 sprite
,因为 sprite
实际是以 mesh
为基础的。
因此,Codea
中所有的一切都是由 mesh、三角形、顶点
绘制而成的。
OpenGL
需要知道每个顶点的 x,y,z
位置坐标,以及它的颜色
- 或者,假如你把一个纹理图形
粘贴在线框模型上时,图形的哪一部分会被绘制在这个顶点上。
所以,每个顶点都有三条关键信息:
x,y,z
位置坐标颜色
(如果你设置过)纹理映射
(例如纹理贴图中的哪一个 x,y
点被用于这个顶点) OpenGL
然后就能插入这些信息用来计算三角形内部的每一个点的位置和颜色。
OpenGL
做了其他一大堆非常复杂、名字很长的事情,当然,我们所关注的仅仅是我们所提供的顶点集合的信息,以及 OpenGL
在屏幕上向这些顶点中插入的像素点和像素点的颜色。
因此,继续:
OpenGL
要你为你的 mesh
定义一组三角形mesh
上面)一个(x,y)
值,用来描述纹理贴图的哪一部分将会被绘制在这个顶点上OpenGL
接着会通过在顶点(顶角)值之间插值的办法 在每个三角形的内部绘制出所有的点。回到那个管道的洞上:
管道上的一个洞位于信息流中 mesh
被分离为独立顶点的地方,并且每个顶点的全部信息都被收集在一起。OpenGL
正要插入位于三角形顶点之间的所有像素点(译者注:也就是在几个顶点坐标值的区间内进行插值)。
不过首先,我们获得一次跟这些顶点玩耍的机会。
当我们通过这个洞向管道里看时,我们仅仅看到一个单独的顶点。正如我所说,我们在这里工作于一个系统底层。顶点知道它的 x,y,z
位置坐标值,一个颜色值(如果你已经设置了一个),以及它在纹理贴图上的位置坐标,除了这些就没有更多的了。
我也说过我们只看到一个 vertex
(顶点)。其他所有的顶点到哪里去了?好了,备份管道的某些地方是一个循环处理,一次只让全部顶点的一个通过,并且把一个顶点发送到管道里去。因此 vertex
代码将会为每个顶点独立运行。(译者注:也就是说处理 vertex
的代码一次只处理一个顶点,处理所有顶点的循环由整个管道来实现,我们在写代码时按照一个顶点的处理逻辑写就可以了)。
在这个洞中已经有了一些代码,不过所有这些代码看起来好像只是取得这些信息的一部分,并把它们不做任何改变地传递给其他变量,这些看起来都是相当不得要领的(译者注:不容易理解)。
事实上,这些代码正如下面所写:
vColor = color;
vTexCoord = texCoord;
gl_Position = modelViewProjection * position;
这句代码 vColor = color;
是什么意思?
我猜测软件开发者在说:
我们将会在一个名为 color
的输入变量中,给你们每个顶点的颜色,你们可以对它做任何事情,然后把结果放在一个名为vColor
的输出变量中,如果你们不打算改变这个顶点的颜色,那就让那行代码呆着别动好了。
同样的事情发生在顶点位置和纹理映射坐标上。因此你能取得顶点数据(译者注:包括颜色、顶点位置坐标、纹理映射坐标),编写代码篡改它们,然后把结果传递出去。
译者注:简单说就是,上述代码中赋值号 =
右侧的部分是由 Codea
自动传递进来到这个处理阶段的输入变量, color
是顶点颜色, position
是顶点位置坐标,texCoord
是纹理映射坐标;赋值号左侧的部分就是准备由这个处理阶段传递给下一道工序的输出变量。
你放在这里的代码就被称为一个 vertex shader
(顶点着色器)。
你打算如何来改变一个顶点?好,一个顶点主要跟位置坐标相关,因此,例如你可以制作一幅镜像图形(比如在 x
轴上翻转)通过把 x
坐标翻过来(译者注:上下翻转,想象一下水中的倒影),这样图形的右手侧就会被画到左手侧,反之亦然。或者你也可以创造一个爆炸物体,通过让 x,y,z
坐标以一种特定路径在一系列帧上飞离。
限制:
当你从事编写 顶点着色器-vertex shader
代码时,有很多限制:
你的代码一次处理一个顶点,并且它没有太多相关信息,仅仅只能影响到这个顶点。所以它不知道相邻顶点的任何信息,例如–除非你传递额外的信息进去,它才可能知道(我们很快就会提到)。
这些代码用它们自己的语言来编写,基于 C
,没有很多函数可供使用。
如果有一个错误,你很可能会得到一块空白的屏幕 – 或者混乱的屏幕,这会给调试工作带来一些阻挠(尽管至少你无法破坏掉些什么,失败是完全安全的)。Codea
有一个内建的 Shader Lab(着色器实验室)
,它会显示语法错误信息,作为一点帮助。
不过我们随后将会返回上述全部这些,我刚刚意识到每一样仍然有些混淆。
先在这里挂起,不久将会更清楚。
管道上的第二个洞位于这个位置,在这里 mesh
中的每个顶点的顶点信息已经被完成插值。
因此,所有这些在到达我们这个洞之前都已经发生了。向里看,我们看到一个单独的像素点,比如,不论我们在这里放什么代码,它都会为 mesh
中的每一个像素点而运行。
再一次,这里已经有代码存在。并且所有这些代码所做的,是取得插值颜色和纹理坐标位置,并且用它们指出应用到像素点上的颜色。这只会带来两行代码。
lowp vec4 col = texture2D(texture, vTexCoord) * vColor ;
gl_FragColor = col;
乍看起来有点奇怪,不过看啊看啊你就习惯了。
命令 texture2D
相当于 Codea 中的 myImage:get(x,y)
,它取得纹理贴图上面一个像素点的颜色,这个像素点位于x,y
,由 vTexCoord
指定,最后把这个像素点的颜色放到一个名为 col
的变量中。
而且,如果你已经为顶点设置了颜色,它将会在这里应用那个插值颜色(vColor
)。至于现在,还不必去担心为什么会用颜色来相差。
第二行简单地把颜色 col
赋值给一个名为 gl_FragColor
的东西。
因此再一次地,这段代码没有干太多事。不过,正如 顶点着色器-vertax shader
一样,如果我们想,我们可以对像素点的颜色进行混合。于是结果就是我们可以通过各种有趣的方式来做这件事。事实上,几乎所有 Codea
内建的着色器都是这种类型。
接着我们为这个洞编写的任何代码都被称为 片段着色器-fragment shader
(fragment-片段
只是像素点-pixels
的另一个叫法)。
因此:
顶点着色器-Vertex Shader
影响独立的顶点们片段着色器-Fragment Shader
影响独立的像素点们在关于它们是如何做的这一点上,将仍然是一个完全的秘密,不过我会给你一些例程来帮助你理解。
现在我们看看基本的 顶点着色器-Vertex shader
,并且学习一点 shader language(着色语言)
。
我不能一直谈论管道。某些时候,我不得不给你看一些真正的代码并且解释它们。不过我不会给出一个关于着色语言的课程。我只会简单地解释说那是什么,仅仅是你工作所需要知道的最少的那些原材料。
我想要从 shader lab
里开始。想找到它,进入你选择项目的 Codea
主界面,点击屏幕左上角的那个方形和箭头的图标,你就会发现 shader lab
。选择它,并且点击 Documents
,然后选择 Create New Shader
,给它起个名字。
现在你就可以看这些代码了,在标签页 vertex
:
//
// A basic vertex shader
//
//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;
//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
void main()
{
//Pass the mesh color to the fragment shader vColor = color;
vTexCoord = texCoord;
//Multiply the vertex position by our combined transform
gl_Position = modelViewProjection * position;
}
这里有很多代码,不过只有它们中的三部分完成所有工作!
因此现在,我将快速解释你在哪里看到的一些奇怪的东西(译者注:这些只是 C
语言的基本语法)。
//
为前缀,而不是 Codea 中的 --
;
结束main
函数,由 {}
包围着,就像 Codea
中的 setup
函数main
函数不像 Codea
一样以 function
为前缀main
前面的 void
仅仅意味着当它执行时不会返回任何值 如果我们在 顶点着色器-vertex shader
中改动任何地方,它将大半落在 main
函数里。
现在如果你回顾上述 main
函数中的所有代码行,(你会发现)这些代码行定义了我们将会在代码中用到的全部的输入和输出变量。你必须在使用它们之前先定义它们。
每一行最后一个词是变量名,那么所有这些前缀–uniform, attributes, varying, lowp, highp, mat4, vec2, and vec4
又是什么呢?
不必担心,它们都是合乎逻辑的。这些前缀告诉 OpenGL
三件事:
1、Precision
(小数的位数)– 精度
有三个选项,highp, mediump, lowp
,你可以猜测它们的含义,如果你没有指定一个,默认值是 highp
。就现在而言,所有这些你都可以完全忽略,因为我们要做的任何事都不需要特别的精度。
2、变量的数据类型
如果你编写过其他程序,你会习惯于指出一个变量是否是一个整数,一个带有小数的数,一个字符串,一个数组等等。Codea
自己计算出绝大部分数据类型而断送掉我们亲自计算的机会。OpenGL
需要我们确切地定义变量,不过它们都是相当明显的。
vec2
= Codea
中的 vec2
,如 vec2(3,4)
,还有 vec3
和 vec4
bool
= boolean
(true 或 false) 布尔型,真值或假值int
= integer
整型float
= 带小数的数 浮点型sampler2D
= 一个 2D 图像mat2
= 2*2
的表(mat3
和 mat4
分别是 3*3
和 4*4
的表)因此你必须在你添加的任何变量前面包括其中任意一种类型
3、这些变量用来做什么?
OpenGL
需要知道你这些变量是拿来做什么用的。一个出现在这个着色器中的变量有三个可能的原因。
(a)attribute
- 是一个输入,提供关于这个特定顶点的信息,比如,它的值对于每个顶点都是不同的。明显的例子就是位置坐标,颜色和纹理坐标,并且你将会在上述代码中看到它们全部。这些输入由 Codea
自动为每一个顶点提供。
(b)uniform
- 也是一个输入,不过对于每个顶点来说没有变化。例如,Codea
中的 blend shader
(译者注:可以在着色实验室找到这个着色器)定义了第二幅图像用来跟通常的 mesh
纹理贴图图像进行混合,并且这幅相同的图像将会被用于所有的顶点,因此它是 uniform
-统一的。在标准代码中只有一个 uniform
- modelViewProjection
- 而且我们现在不会讨论它,因为它是 3D 黑盒子的一部分。
(c)varying
- 这些是输出,将被用于插值独立像素点,还将会在 片段着色器-fragment shader
中可用。这里是它们中的两个 vColor
和 vTexCoord
,你可以添加更多的。
让我们再次总结一下:
attribute
- 输入 为每一个顶点输入一个不同的值,如 position
uniform
- 输入 对于所有顶点都是相同的,如第二幅图像varying
- 输出 将会被提供给 片段着色器-fragment shader
使用因此,让我们看看下面这一些变量,看看能否指出它们的定义。
attribute vec4 color;
变量 color
是一个 vec4(r,g,b,a)
(译者注:红,绿,蓝,透明率
) 和一个 attribute
,这意味着它是一个输入,并且对于每个顶点都不同,这正是我们所期待的。
attribute vec2 texCoord;
变量 texCoord
是一个 vec2
以及一个 attribute
(因此它对于每个顶点都不同),我们可以根据它的名字来猜测:它保留了应用于这个点的纹理贴图的坐标位置。
varying highp vec2 vTexCoord;
变量 vTexCoord
是一个高精度的 vec2
,它还是一个 varying
,这意味着它是一个输出,因此它将会被插值到每个像素点,并且发送给 片段着色器-fragment shader
。你可以从 main 函数中的代码看到,vTexCoord = texCoord
,因此所有这些代码所做的就是传递贴图的位置坐标给 片段着色器-fragment shader
。
因此我们回到所有这个着色器所做的事实,它取得位置坐标,纹理和颜色信息(来自 attribute
输入变量),然后把它们未做改动地赋值给输出(varying
)变量.
基本上,它什么也没做(除了一个神秘的矩阵相乘)。
现在该由我们来改变它了。
是时候来改变那个 顶点着色器-vertex shader
了。这也正是它存在的意义。
首先,我想分享关于用一种你一无所知的语言编写代码时的我的规则
不论何地,尽可能地,窃取一行已经存在的能工作的代码
(译者注:大意是,对于一门陌生的语言,尽量参考引用别人写好的完善的代码)
这将会有点困难,当我们被给了这么少的几行代码作为开始时,不过 Shader Lab
包含了大约 15 个着色器的代码,并且其中不少代码我们都可以偷来(以及研究)用。
首先,让我们试着翻转一幅图像,这样我们就会得到一个镜像图像。在 Shader Lab
中你自己定制的 着色器中尝试。我们的目标是把 Codea
的 Logo
变成一个镜像图像。
翻转图像最简单的办法是改变纹理贴图的所有坐标,这样 OpenGL
就会由从右到左绘制换成从左到右绘制。你应该记得纹理贴图的位置坐标是介于 0 到 1
之间的分数,0 位于左边(或者底部),1 位于右边(或者顶部)。如果我们用 1 减去x
值,我们将会得到想要的结果,因为位置(0,0)
(左下角)会被改变为(1,0)
(右下角),反之亦然。
因此,让我们看看 顶点着色器-vertex shader
中 main
的内部,这就是我们要改的那一行
vTexCoord=texCoord;
我们只想翻转 x
值,因此改动如下:
texCoord.x = 1 - texCoord.x; //change the x value vTexCoord = texCoord;
好了,你已经犯了两个错误。一个是 texCoord
是一个输入,它不能被改动。另一个是 texCoord
包含分数(浮点)值,不能跟整数混合使用,因此你应该用 1.0 或 1
. 而不是 1
这是一个真正的”我抓到你了“的小圈套来愚弄你(它仍然得到我的一丝不苟),所以,尽量记住这个玩笑中的两个错误。
任何定义为 float
的变量在跟其他数字一起计算时,该数字必须包含一个小数点,所以换掉 d = 1
,你应该说 d = 1.0
或者仅仅是 d = 1.
,否则它就不会工作。
所以我们换一个:
vTexCoord = vec2(1.-texCoord.x,texCoord.y);
这句代码定义了一个新的 vec2
(正是 vTexCoord
想要的),并且把它赋值为 1-x
的值和 y
的值。
它生效了,并且应该在 Shader Lab
把 Logo
翻转为一个镜像图像。
现在来看看你能否用相同的方法翻转 y
值。。。
你能用它来做什么?假定你有一个图像来指向一条路,而且你希望能用它指向另一条路。现在你只需要一个图像就可以实现了。
我们如何为用户提供翻转图像的可选项?这将会是一个对于所有定点都相同的输入,因此,它将会是 uniform
,对不对?
它也是 true
或 false
,所以它是 boolean
,或者着色器语言中的 bool
那么我们只有当收到要求翻转的请求时,才需要让纹理贴图的 x
值翻转。下面是新的 顶点着色器-vertex shader
,修改部分用红色,我去掉了注释语句以便节省空间。
uniform mat4 modelViewProjection;
uniform bool flip; // 红色
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
lowp vec4 vColor;
varying highp vec2 vTexCoord;
void main()
{
vColor = color;
if (flip) vTexCoord = vec2(1.0-texCoord.x,texCoord.y); //红色
else vTexCoord = texCoord; //红色
gl_Position = modelViewProjection * position;
}
C
中的 if
判断跟 Codea 中的相似,除了判断条件被放在圆括号中,并且,如果 if
或 else
代码超过1行,你需要用大括号 {}
把它们包围起来。
如果你用上面这些替换了 Shader Lab
里的 vertex
标签页的代码,什么也不会发生,因为 flip
的默认值是 false
。不过如果你到了 binding
标签页(在这里你可以设置测试值),你将会看到一个项目 flip
已经被添加,并且如果你把它设置为 true
,Codea Logo
将会翻转。
这个例子展示给我们的是我们可以通过非常少的代码来实现很酷的效果,而且我们可以通过命令来让 着色器去做各种不同的事情。当然了,我意识到你想知道如何在 Codea
代码中设置 flip
的值。我们很快就会讲到这一点。
下一步我们会去看 片段着色器-fragment shader
,拥有多得多的用于改造的潜力。
现在我们会去查看 `片段着色器-fragment shader- 的更多细节。
如果你查看了 Shader Lab
中你定制的着色器中的 片段着色器-fragment shader
标签页,你将会看到这些代码:
//
// A basic fragment shader
//
//Default precision qualifier
precision highp float;
//This represents the current texture on the mesh
uniform lowp sampler2D texture;
//The interpolated vertex color for this fragment
varying lowp vec4 vColor;
//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;
void main()
{
//Sample the texture at the interpolated coordinate
lowp vec4 col = texture2D( texture, vTexCoord ) ;
gl_FragColor = col;
}
这些看起来跟 顶点着色器-vertex shader
的代码没有太多不同,并且如果你看了上述 main
函数中的变量的话,你就会看到一些老朋友,vColor
,vTexCoord
,而且它们确实用了相同的方式来定义。
不论如何,它们确实不一样,因为在 顶点着色器-vertex shader
,它们为一个特定的顶点给出一个值,然而在这里,他们为一个像素点给出一些值(插值)。而且,你可能只有 10
个使用 顶点着色器-vertex shader
的顶点,但是你可能会有 1000
个像素点来使用 片段着色器-fragment shader
。
这里有一个新变量,定义为 uniform
(因此它被应用于所有的像素点)和 sampler2D
(比如在 Codea
中一个 2D 图像之类的东西)。这是将被用于为像素点选择颜色的纹理贴图图像。(它没有在 顶点着色器-vertex shader
中被提及,因为那里不需要它)
我曾经解释过一次那些代码,不过现在我要再做一次。
lowp vec4 col = texture2D( texture, vTexCoord ) ;
main
中的第一行代码定义了一个名为 col
的新变量,它是一个带有低精度的 vec4(这些不是我们的关注点)。注意你不需要为它出现在那里而给 OpenGL
一个理由(例如 attribute
, varying
或 uniform
),因为对 main
函数而言,它是一个纯粹的局部变量。
函数 texture2D
就像 Codea
中的 myImage:Get(i,j)
。它取得纹理贴图图像
中位于 x,y
处的颜色
,x,y
的取值范围是 0~1
gl_FragColor = col;
第二行简单地把它传递给用于输出的变量 gl_FragColor
。
这是相当无聊的,所以让我们试着改动它。
在你的 Shader Lab
的例子里,在这两行之间添加一行,如下:
lowp vec4 col = texture2D( texture, vTexCoord );
col.g=1.0-col.g; // <===== 新加的行
gl_FragColor = col;
接着你会看到这个:
我们所做的是把绿色翻转,因此如果它原来是低的,现在变成了高的,反之亦然。
你可能会疑惑为什么我们会用 1
去减,与此同时,颜色值的范围应该在 0 到 255
之间。好吧,不在 OpenGL
中时它们不是那样。它们被转换为数值范围位于 0 到 1(=255)
之间的小数
。
这就是为什么,如果我们为顶点设置颜色,像一个纹理贴图一样,使用:
mesh:setColors(color(255)) --set to white
的原因。它将会被转换为 0 到 1
之间的数字,例如淡黄色(255,255,0,128)
将会在 片段着色器-fragment shader
中变为 (1.0, 1.0, 0.0, 0.5)
。
我们可以把这个颜色应用到我们的像素点上,通过如下乘法:
gl_FragColor = col * vColor;
译者注:这里的 vColor
的值就是上一句中通过 setColor(color(255))
设置好的。
相乘的结果为:
col * vColor = vec4(col.r * vColor.r, col.g * vColor.g,...等等)
例如 col
的 r,g,b,a 的值会跟对应的 vColor
的 r,g,b,a
的值相乘。
你能通过一个简单的实验来理解这些。我们将会使 Logo
变黑。
把最后一行改为:
gl_FragColor = col * 0.2; //把所有值的亮度都降到 20%
这会有效果,因为 0.2
会跟 col
中的每个 r,g,b,a
相乘。
现在,能从 Codea
去做这些将会真的很酷,比如让你的景色从亮变暗。
那么,这一次就让我们从 Codea
来做这些吧,OK?
你大概一直跟我在 Shader Lab
流连,并且现在你已经有了一个你自己的改变了一些东西(顶点或片段)的着色器。
你可以很容易地试验它。返回到 Codea
程序的主界面,并且调用那个着色器
示例项目。在第 20 行有一个着色器被命名为Effects:Ripple
。点击这个名字,并且从弹出菜单的 Documents
区域选择你的着色器来代替。然后运行,你就会在屏幕上看到你的着色器做出的改变。
这意味着对一个普通的着色器做出简单的改变是相当容易的,立刻在你的代码中使用你的着色器版本。事实上,仅仅需要一行代码来把你的着色器
和 mesh
关联起来。
myMesh.shader=('Documents:MyCoolShader')
让我们更进一步,创建一个着色器,在我们画面中实时改变亮度。
首先,回到 Shader Lab
,在 Documents
增加一个新着色器,我把它叫做我的 lighting
到 片段-fragment
标签页,在 main
之前,把这行代码加入到定义中去。
uniform float lighting;
通过把 lighting
定义为 uniform
,我们告诉 OpenGL
这个值由管道外部来提供(比如,来自 Codea
),并且应用到全部像素点。因此我们将需要从 Codea
来设置 lighting
(它是一个 0~1
之间的分数)这个值。
现在,在 main
函数中,改动最后一行为:
gl_FragColor = col*lighting;
位于右侧的小测试屏幕将会变黑,因为我们的新变量 lighting
默认为 0
,意味着所有的像素点都会被设置为黑色。
为了测试我们的着色器是否起作用,转到 Binding
标签页,你将会看到 lighting
的一个条目,值为 0.0
。让它变大一些,如 0.6
,然后测试的图像会再次出现。值 1.0
会让它完全变亮。这说明我们的着色器正常工作。
所以,为了告诉 OpenGL
我们想从 Codea
提供一个值,我们在着色器中把它定义为 uniform
,并且标签页 Binding
为我们提供了一个测试它的方法,在我们在 Codea
中实际使用它之前。
不过现在让我们返回到 Codea
并且尝试它。下面是一些代码用来调用资源库里的一个图像,并且为我们提供一个参数用来调节亮度。我已经把我的着色器叫做 lighting
,因此,只要改为任何你用过的着色器的名字就可以了。
function setup()
img=readImage('Small World:House White')
m=mesh()
m.texture=img
--double size image so we can see it clearly
u=m:addRect(0,0,img.width*2,img.height*2)
m:setRectTex(u,0,0,1,1)
--assign our shader to this mesh (use your own shader name)
m.shader=shader('Documents:Lighting')
--allow user to set lighting level
parameter.integer('Light',0,255,255)
end
function draw()
background(200)
perspective()
camera(0,50,200,0,0,0)
pushMatrix()
translate(0,0,-100)
--here we set lighting as a fraction 0-1
m.shader.lighting=Light/255
m:draw()
popMatrix() end
特别注意这些:
1、在 draw
函数中,恰好在绘制 mesh
之前,我基于 parameter
的值设置了 lighting
变量,把它当做一个除以 255
的分数
2、你需要把变量 lighting
关联到 m.shader
(比如一个实际的着色器)上,而不是 m
(mesh)。
当我们运行它同时改变 light
参数时,图像慢慢地如下图所示般变淡,你可以写一个循环让它平滑地去做。
因为我们创造了一个淡入淡出的着色器,或者叫雾化。非常简洁。
你还能用一个我们的着色器里已有的变量-不过该变量还没有使用过-来尝试,就是 color
(或者 vColor
,片段着色器-fragment Shader
知道它)。Codea
有一个专有的函数用于这个 - 既然我们使用了 setRect
创建了 mesh
,那么我们需要使用 setRectColor
,如下:
:setRectColor(u,color(Light))
但是好像没效果。
图像没有淡化,而是变黑了。发生了什么?
实际上,一切都很好并且工作正常。发生现在这种情况是因为 alpha
(控制颜色的透明率) 值在这两种场景下是不一样的。我们使用 color(Light)
来设置 setRectColor
,当我们只为 color
函数提供一个值时,它把这个值用于前三个参数 r,g,b
,但是让第四个参数 a = 255
。所以,当你减少 light
值时,它们每一个都变黑了,而不是透明(译者注:alpha=0
是全部透明,alpha =255
是全部不透明)。
如果你想要得到淡化/雾化效果,你需要让 alpha
值跟着一起变化,通过设置全部的 r,g,b,a
m:setRectColor(u,color(Light,Light,Light,Light))
你可以使用这个经验来实现翻转,回到上述的着色器代码即可,并且由白天变为黑夜,而不是雾化。所有需要我们做的只是通过 light
把 r,g,b
的值乘起来,不过不包括 a
所以我们的 main
函数变为:
owp vec4 col = texture2D( texture, vTexCoord ) * vColor;
col.rgb=col.rgb*lighting; //新行 - 或者, 用 C, 可以写成 col.rgb *= lighting;
gl_FragColor = col;
想一想上面我们如何能只选择改变 r,g,b
的值,而保持 a
不变。这就是我期望 Codea
能做的事。
现在当 light
减少时图像变黑了(如果你想让你的背景同时变黑,只要在 Codea
的 background
函数中改变颜色就可以了)。
因此你现在应该明白如何新建一个着色器,它可以制造雾化效果,淡化你的图像,或者让你的图像变黑。你可以通过内建的color
变量来实现,也可以使用你自己新建的变量来实现。这种效果对于仅用几行代码来说是相当强大的。
如果你给着色器两个 uniform
变量,你就能实现雾化、暗化。
不过我猜你也能看到这些都花费了一些时间去习惯和实践。不过我向你保证,我也没有一两次就把所有代码写对。(译者注:第一句感觉含义不大清楚,结合上下文,大概就是说上面的例子都经过反复调试,不影响理解)
我想开始给你很多例子,不过首先,我想向你演示如何把着色器代码包含在你的 Codea
代码中。这是因为尽管 Shader Lab
很有用,它也是保存在你的 iPad
中以致你的着色器不能分享给其他人。
把着色器代码嵌入到你的代码中是相当容易的。
--this is how you attach your shader to a mesh
MyMesh.shader=shader(MyShader.vertexShader, MyShader.fragmentShader)
--and this is how you "wrap" your shader (in a table) so Codea can read it
--this can go anywhere in your code. Choose any name you like.
MyShader = {
vertexShader = [[ //vertex shader code here ]],
fragmentShader = [[ //fragment shader code here ]]}
你把你的 顶点着色器-vertex shader
和 片段着色器-fragment shader
代码放到一个文本字符串中(两对方括号[[]]
只是一种书写多行文本字符串的方式),并且接着把它们保存到一个表中(译者注:就是再用大括号 {}
包起来)。最后,你告诉 Codea
到哪里去找你的着色器 – 注意你给 顶点着色器-vertex shader
和 片段着色器-fragment shader
都起了名字。
你可以在多个 mesh
中使用相同的着色器,你也可以在同一个 mesh
中使用不同的着色器(当然是在不同的时间)。
我通常把着色器嵌入我的代码中,因此它们是可移植的。不过如果你有了一个错误,你不得不自己去找,然而,如果你在 Shader Lab
中创建了着色器,它会对语法错误做出警告,这很有帮助。所以一切取决于你。你可以先从 Shader Lab
起步,后面代码没问题了再把它们拷贝到 Codea
中嵌入。
我现在准备给你相当一些着色器例子。因为它们中的很多都很简单,并且只涉及 顶点着色器-vertex shader
或者 片段着色器-fragment shader
中的一种,- 而不是同时包括两者 - 我觉得没有改变的代码没必要重复。
所以我准备从那些我建议你拷贝到 Codea
的标准代码开始,然后是每一种着色器(译者注:就是先 顶点-vertex
再 片段-fragment
)。我会演示给你看,在标准代码中改变哪些内容,来让着色器生效。我将会把着色器嵌入到 Codea
代码中。
接下来就是起步的代码,包括一个仍然什么都不做的着色器。我们主要目标是把颜色改为红色。
你可以为每个着色器起一个不同的名字,不过也别忘了同时在 setup
中修改把 shader
和 mesh
关联起来的那行代码。
译者注:就是这个:
MyMesh.shader=shader(MyShader.vertexShader, MyShader.fragmentShader)
我的建议是保持这些位于 Codea
左手边标签页的代码不要改变。当我们试验每一个新例程时,在右边新建一个标签页并把所有标准代码都拷贝进去,然后在那里修改它们。这意味着你将建立自己的着色器库,当你摸爬滚打在不同的例程中。
注意 - 如果你终止于 8 个标签页时(最多使用 8 个时),每个标签页都有自己的 setup
和 draw
,没什么关系。当 LUA
在运行前编译,它会从左到右执行,并且如果它找到重复的函数,它仅仅替换掉它们。因此位于右侧标签页的代码是最终被执行的那个 - 你也可以把任何一个标签页拖到最右侧来让它执行。
译者注:Codea
有一个使用技巧,它在拷贝/粘贴到项目时可以把位于同一个文件中的不同标签分开,只要你在每个标签页的代码最前面用 --#标签页1
来标识即可
请注意另外一些事情。在下面提到的任何着色器例程中,我会把着色器用到的变量放在 draw
函数中,例如:
m.shader.visibility=0.5
唯一的理由是我要使用参数来改变设置,在任何时候用户都要能设置,因此 draw
函数需要始终获得最新值。然而,如果设置一直都不变,例如,如果你正使用雾化/暗化化着色器,并且你只需要雾化,那么你就可以在你第一次把 shader
和 mesh
关联时就把设置好的值发送给着色器,你就不需要在 draw
里去做这些(一旦你设置好了,它会一直保持同一个值,直到你再次改变)。
最后一句,你会很惊讶这些解释和 Codea
代码某种程度上比任何实际着色器代码的改动都要长。不会一直是这样的,当然了,这样会确保你能够理解这些例程。
为了更容易一些,在写这份教程时,我已经完成了全部的例程代码,而且你可以在这个项目里找到它们:
https://gist.github.com/dermotbalson/7443057
不过如果你用的是 iPad 1
,那就用这个:
https://gist.github.com/dermotbalson/7443577
直接选择你想要运行的着色器然后运行它。它们中的每一个都位于自己的代码标签页内,并且可以被拷贝到其他项目,不需要任何改动就可以运行。
function setup() m=mesh()
img=readImage("Small World:Icon") --Choose another if you prefer
m:addRect(WIDTH/2,HEIGHT/2,img.width*3,img.height*3) -- I tripled its size
m:setColors(color(255))
m.texture=img
m.shader=shader(DefaultShader.vertexShader,DefaultShader.fragmentShader)
end
function draw()
background(40, 40, 50)
m:draw()
end
DefaultShader = { vertexShader = [[ uniform mat4 modelViewProjection; attribute vec4 position; attribute vec4 color; attribute vec2 texCoord; varying lowp vec4 vColor; varying highp vec2 vTexCoord; void main() { vColor=color; vTexCoord = texCoord; gl_Position = modelViewProjection * position; } ]],
fragmentShader = [[ precision highp float; uniform lowp sampler2D texture; varying lowp vec4 vColor; varying highp vec2 vTexCoord; void main() { lowp vec4 col = texture2D( texture, vTexCoord) * vColor; gl_FragColor = col; } ]]}
让我们从我们做过的开始。我们会让图像在朦胧不清的雾中淡入淡出。
我打算把我们的着色器叫做 FogShader
,而且我准备使用一个参数,让我们设置能见度,位于 0
(什么也看不到)到1
(全部都能清晰看到) 之间的一个颜色值。
因此,这就是我需要在 setup
中修改的内容:
m.shader=shader(FogShader.vertexShader,FogShader.fragmentShader)
parameter.number("visibility",0,1,1)
在 draw
中也有一点小改变。我把背景设置为跟朦胧不清一样的颜色,把能见度系数发送给着色器
background(220)
m.shader.visibility = visibility
在 顶点着色器-vertex shader
中我改了两行代码。加入了能见度
系数,通过跟这个系数相乘来调整颜色。
//put this with the other uniform item(s) above main
uniform float visibility;
//replace the line that sets vColor, with this
vColor=vec4( color.rgb, color.a ) * visibility;
就是它了,你现在可以跟这个能见度
参数小伙伴一起好好玩耍了。
我们已经到了这里,让我们制作一个能把一幅图像变亮、变暗的版本。这跟雾化着色器很相似,除了我们没有调整像素点颜色的 alpha
值。
因此我们可以使用雾化着色器的代码,只改变其中一行:
vColor=vec4( color.rgb * visibility, color.a );
让我们勇敢地把它们结合起来,既然它们如此相似。
我会在 Codea
的 setup
中放入一个参数,这样我们就可以在它们之间切换,如果没错,我们的 着色器将会绘制雾,或者它会把一幅图像亮化或暗化。
parameter.boolean("Fog",true)
把它放到 draw
中:
m.shader.fog=Fog
再把它作为另一个 uniform
变量放到 顶点着色器-vertex shader
中:
uniform bool fog;
接着改变 顶点着色器-vertex shader
中 main
函数中的代码,这样它要么用能见度系数乘以整个颜色(译者注:即r,g,b,a
),要么只乘以 r,g,b
:
if (fog) vColor=vec4( color.rgb, color.a ) * visibility;
else vColor=vec4( color.rgb * visibility, color.a );
这样是不是很酷,当物体远去时雾会变得更浓(在一个 3D 画面里)?或者如果你模拟一个火把或者灯笼,它们会随着远去而光亮被遮住直到变黑?
好了,我们可以用我们已有的东西来实现这种效果,不用改动着色器。我们可以绘制一些物体在 3D 场景中,然后让我们的能见度由距离来决定,就像这样。
在 setup
中,我会加入一个距离参数,它让我们指定物体在变得完全透明(或者黑暗)之前需要多远(用像素点计算)。我会让我们的图像在 100 到 1000
的距离之间重复地前进和后退,使用一个 tween
动画,这样我们就可以看到效果了。
parameter.integer("distance",0,2000,1000)
parameter.boolean("Fog",true)
dist={z=200} --we have to use a table of pairs with tweens
tween(10, dist, {z=1500}, { easing = tween.easing.linear, loop = tween.loop.pingpong } )
我删掉了之前的能见度
参数,因为我们打算自己来计算它。
我替换掉了全部的 draw
代码,因为我需要在 3D 中绘制(需要 perspective
和 camera
命令),我还想让背景的明暗由是否使用雾化来决定。我还需要在当前距离绘制一个照片(由 tween
设置,在 dist.z
中)
function draw()
if Fog then background(220) else background(0) end
perspective()
camera(0,0,0,0,0,-1000)
m.shader.visibility = 1 - math.min(1,dist.z/distance)
m.shader.fog=Fog
pushMatrix()
translate(0,0,-dist.z)
m:draw()
popMatrix()
end
我们最开始的第一个着色器,翻转一幅图像来制作镜像。我们也可以把它包含进来,通过标准代码来实现。
我们将会在 setup
中新建 2
个参数由你操作,这样你就能翻转 x 或 y
,或者两者同时。
parameter.boolean("Flip_X",false)
parameter.boolean("Flip_Y",false)
我们将会在 draw
中把它们发送给着色器
m.shader.flipX=Flip_X
m.shader.flipY=Flip_Y
同时要在 顶点着色器-vertex shader
代码的顶部加入我们的新变量:
uniform bool flipX;
uniform bool flipY;
并且调整纹理贴图的坐标,如下:
vec2 t = texCoord;
if (flipX) t.x = 1.0 - t.x;
if (flipY) t.y = 1.0 - t.y;
vTexCoord = t;
是不是觉得变得更容易了?因为我们做了更多的练习。
或许,现在是做一些 片段着色器-fragment shader
的时候了。
这是一个极其有用的着色器,有很多用途 - 并且相当简单!
我第一次需要它是在绘制一个大型 3D 场景时,尝试把像草、砖块、栅栏等纹理贴图覆盖到不同的物体上。在互联网上很容易找到合适的纹理图像,但是它们通常都是错误的比例(例如放大太多或缩小太多),和尺寸。太大了还好说,但是太小了就意味着你需要用纹理贴图像马赛克一样贴满你的图像(就像一堆瓷砖)。
例如,假设你想要画一个巨大的 2D 草地,有 2000 * 1000
个像素点,而你有一个大小为 400 * 300
的草的图像, 这就需要被一个大概 10
倍的系数来进行比例缩放(例如草的叶子会非常巨大)。怎么做?
困难的方法是把你的地面分割成多个跟草的图像大小一样的矩形,再把每一个矩形加入你的 mesh
中,用草的图像作为它们的纹理贴图。然而,如果我用系数 10
把草的图像缩放为 40 * 30
像素点,我就需要准备一个数目巨大的矩形集来覆盖2000 * 1000
的区域。
假设我可以用下面这么实现:
Codea
最大的图像尺寸限制,2048
个像素点)结果如此令人惊讶,甚至让我钦佩。
它基于一个简单的技巧。你知道纹理贴图被映射到每个顶点,用一对介于 0~1
之间的 x,y
值(例如,0,0
是左下角,1,1
是右上角)。
假定我们用两个三角形新建了一个矩形,生成了整个地面,我们用纹理贴图做了映射,这样四个角的 x,y
位置为(使用上面那个例子):
左下角 x = 0, y = 0
右下角 x = 50, y = 0
左上角 x = 0, y = 33.33
右上角 x = 50, y = 33.33
x
值为 50
,是由 地面宽度/贴图宽度 = 2000/40
计算得到的,y
值采用相似的计算 1000/30
。因此我的 x 和 y
的最大值就是我的贴图的重复次数。
如果只完成上述工作,我们的片段着色器将会变得混乱,因为它期待介于 0~1
之间的值。不过我们还有更多的事情要做。
在片段着色器中,改动 main
中的第一行代码如下:
lowp vec4 col = texture2D( texture, vec2(mod(vTexCoord.x,1.0), mod(vTexCoord.y,1.0)));
它做了什么?它对每个 x 和 y
的纹理值
用了一个 mod
函数,计算小数部分,忽略掉整数。所以值 23.45
会变为 .45
如果你好好想想,这将确实是最合适的方法,我们想把小的纹理图像贴到地面上。
下面的代码示范了怎么做。我把创建 mesh
的代码放到一个独立的函数中,这样你就能使用参数改变比例同时看看它的样子。(你也可以试着下载一个草或砖的图像作为纹理贴图来玩玩)。
现在我意识到我说过只有两行代码被改动,我已经增加了更多的代码来创建 mesh
,因为 addRect
无法设置纹理映射,除了 1
之外,因此我不得不“手动”创建 mesh
。不过在大多数项目中,你将至少用这种方式制造你的 mesh
。
下面的代码包括了所有的 Codea
代码,不过没有对着色器进行任何修改。需要你自己亲自去做修改:
function setup() parameter.number("Scale",0.01,1,.5) parameter.action("Apply change",CreateMesh) CreateMesh() end function CreateMesh() m=mesh() img=readImage("Cargo Bot:Starry Background") --create mesh to cover the whole screen local v,t={},{} meshWidth,meshHeight=WIDTH,HEIGHT --whole screen imgScale=Scale --use the image at this fraction of its normal size, ie reduce it --now calculate how many times the image is used along the x and z axes --use these as the maximum texture settings --the shader will just use the fractional part of the texture mapping --(the shader only requires one line to change, to do this) local tilesWide=WIDTH/(img.width*imgScale) local tilesHigh=HEIGHT/img.height/imgScale local x1,x2,y1,y2=0,WIDTH,0,HEIGHT local tx1,tx2,tz1,tz2=0,tilesWide,0,tilesHigh v[1]=vec3(x1,y1,0) t[1]=vec2(tx1,tz1) v[2]=vec3(x2,y1,0) t[2]=vec2(tx2,tz1) v[3]=vec3(x2,y2,0) t[3]=vec2(tx2,tz2) v[4]=vec3(x1,y2,0) t[4]=vec2(tx1,tz2) v[5]=vec3(x1,y1,0) t[5]=vec2(tx1,tz1) v[6]=vec3(x2,y2,0) t[6]=vec2(tx2,tz2) m.vertices=v m.texCoords=t m:setColors(color(255))
m.texture=img
m.shader=shader(TileShader.vertexShader,TileShader.fragmentShader)
end
function draw() background(40, 40, 50) m:draw()
end
我们可以在更多的场合使用拼贴着色器,而不仅仅用来拼贴巨大的表面。假定你正在制作一个平台游戏,你想要让一个背景连续卷动,产生移动着的视觉暗示(译者注:比如横版卷轴游戏)。你的背景图像需要自己重复自己,比如当你走到头时再次开始动,这跟把一个图像拼贴满一个大型区域非常相似。
所以这段 Codea
代码创建了一个被称为舞台布景的图像,通过一个使用灰色矩形的简单城市的轮廓,把它加入到一个 mesh
中。
然后,在 draw
中,我们有一个计数器告诉我们以多快的速度卷动。我们计算了需要卷动的图像的小数
(= 被卷动的像素点/图像的宽度)并且把它发给着色器。
function setup()
--create background scenery image
--make it a little wider than the screen so it doesn't start repeating too soon
scenery=image(WIDTH*1.2,150)
--draw some stuff on it
setContext(scenery)
pushStyle()
strokeWidth(1)
stroke(75)
fill(150)
local x=0
rectMode(CORNER)
while x<scenery.width do
local w=math.random(25,100)
local h=math.random(50,150) rect(x,0,w,h)
x=x+w
end
popStyle()
setContext()
--create mesh
m=mesh()
m:addRect(scenery.width/2,scenery.height/2,scenery.width,scenery.height)
m:setColors(color(255))
m.texture=scenery
m.shader=shader(TileShader.vertexShader,TileShader.fragmentShader)
--initialise offset
offset=0
end
function draw()
background(40, 40, 50)
offset=offset+1
m.shader.offset=offset/scenery.width
m:draw() --sprite(scenery,WIDTH/2,100)
end
在着色器中,我们在顶点着色器代码顶部加入 offset
uniform float offset;
并且改变了 vTexCoord
的计算,让它加上了 offset
的小数值
vTexCoord = vec2(texCoord.x+offset,texCoord.y);
当偏移量 offset
增加时,纹理贴图的 x
的值将会比 1
大,不过我们在片段着色器中的 mod
函数只会保留小数,因此图像会被拼贴,从而给出一个很平滑的连续不断的城市背景。
一旦你开始使用多幅图像,一个常见的问题是 OpenGL
不认识透明像素点。我的意思是,如果你先在屏幕上创建了一个完全空白的图像,接着在它后面绘制了另一个图像,你希望看到那个图像 - 但是你看不到。OpenGL
知道它已经在前面画了些什么(哪怕什么内容也没有),同时错误地假定在它的后面一个点也不画,因为你看不到它。(译者注:这种处理是为了减少不必要的计算量)。
当然,这只是 3D 中的一个问题,因为在 2D 中你无法在其他图像后面画图。
对此有不少解决方案,一个是通过距离为你的图像 mesh
排序,然后按照先远后近的顺序来绘制它们(这样你就绝不会在其他图像后面绘制任何图像)。
另一个办法是让 OpenGL
停止绘制那些空白像素点。有一个着色器命令 discard
告诉它不要画某个像素点,如果你使用它,OpenGL
将会随后在那些被丢弃掉的像素点后面绘制另外的图像。
所以我们的透明着色器将会丢弃掉那些 alpha
值低于一个由用户设置的数字的像素点。我打算把这个数字命名为 minAlpha
(范围 0~1
),并且把它包含到着色器中,如下:
uniform float minAlpha; //把这个放在片段着色器中, main 之前
//替换掉 gl_FragColor = col; 用这两行
if ( col.a < minAlpha ) discard;
else gl_FragColor = col;
为了测试它,我打算在一个蓝色星球前面绘制一艘火箭船。我先画火箭船,然后画星球。如果透明阀值被设置为 1
,我不会丢弃任何东西,这样你就会看到这个问题了 - 火箭图像挡住了后面的星球。当你降低阀值时,着色器开始丢弃像素点 - 大概设置为 0.75
看起来效果最好。
function setup() m=mesh() img=readImage("SpaceCute:Rocketship") m:addRect(0,0,img.width,img.height) m:setColors(color(255)) m.texture=img m.shader=shader(DefaultShader.vertexShader,DefaultShader.fragmentShader) parameter.number("Transparency",0,1,1) end function draw() background(40, 40, 50) perspective() camera(0,0,0,0,0,-1000) pushMatrix() translate(0,0,-400) --rocketship first m.shader.minAlpha = 1 - Transparency m:draw() translate(0,0,-400) --draw the planet further away fill(140, 188, 211, 255) ellipse(0,0,500) popMatrix() end
假定你想让一幅图像像面具一样半遮半掩在另一幅图像上面,例如你想从一幅图像里剪切出一个形状来,或者可能仅仅画一幅图像的一部分来覆盖到第二幅图像上。
看看下图的例子:
在第一幅图像中,一个小公主的形状被用于从图像上剪切了一个剪影洞。
在第二幅图像中,一个小公主的形状用一个红色五星图像画了出来。
译者注:小公主形状来自 Codea
素材库里的小公主图像。
正如前一个例程一样,大多数代码改动都在 Codea
里,我们从读入两幅图像,并用五星状背景创建 mesh
开始。这里有三个参数 - Invert
让我们在上述两类蒙版之间选择,Offset_X
和 Offset_Y
让我们把蒙版准确地放置到你想要放置的地方(好好跟它们玩玩看看它们怎么做)。
function setup() img=readImage("Cargo Bot:Starry Background") stencilImg=readImage("Planet Cute:Character Princess Girl") m=mesh() u=m:addRect(0,0,img.width,img.height) m.texture=img m.shader = shader(stencilShader.vertexShader, stencilShader.fragmentShader) m.shader.texture2=stencilImg parameter.boolean("Invert",false) parameter.number("Offset_X",-.5,.5,0) parameter.number("Offset_Y",-.5,.5,0) end function draw() background(200) pushMatrix() translate(300,300) m.shader.negative=Invert m.shader.offset=vec2(Offset_X,Offset_Y) m:draw() popMatrix() end
片段着色器需要定义额外的图像,和变量,这个变量告诉它通过什么方式去应用蒙版,以及蒙版的偏移位置。
蒙版本身是很简单的。你将会看到我们首先从两幅图中读入两个像素点颜色(涉及第二幅图像时使用 offset
),然后我们或者
或者
代码:
uniform lowp sampler2D texture2;
uniform bool negative;
uniform vec2 offset;
lowp vec4 col1 = texture2D( texture, vTexCoord );
lowp vec4 col2 = texture2D( texture2, vec2(vTexCoord.x-offset.x,vTexCoord.y-offset.y));
if (negative)
{if (col2.a>0.) gl_FragColor = col1; else discard;}
else if (col2.a==0.) gl_FragColor = col1; else discard;
由 Codea
提供的着色器非常值得一看,看看你是否能学到些什么。它们有些充满数学,不过其他的非常有趣。
打开积木着色器,例如,它没有使用任何纹理贴图画了一个砖墙图案。
顶点着色器非常普通,除了:
vTexCoord
被遗忘了main
中有一行额外代码代码:
vPos = position;
我们能够理解为什么 vTexCoord
会缺少(这里没有纹理贴图图像),不过即使这样仍然很有趣,因为它展示了你仅须传递片段着色器需要的变量。
额外的一行传递顶点位置坐标的代码,更有趣。通常它不会被传递给片段着色器,不过很明显的,在这个例子里我们需它。OpenGL
将会对每个像素点进行插值,所以片段着色器会知道每个像素点的确切位置。
片段着色器有 4
个来自 Codea
的输入 - 砖块颜色,灰泥(水泥)颜色,砖块的尺寸(xyz,所以它可以是 2D 或 3D),以及砖块在整体规模中的比例(剩下的是水泥)。
uniform vec4 brickColor;
uniform vec4 mortarColor;
uniform vec3 brickSize;
uniform vec3 brickPct;
main
函数如下:
void main() {
vec3 color;
vec3 position, useBrick;
我们计算了砖块上的像素点的位置。这将是一个像是 0.43
或者 5.36
的数字(如果我们在第六块砖块上),以此类推。
position = vPos.xyz / brickSize.xyz;
如果砖块数目是偶数,它就以半块砖为单位来移动 x
和 z
(深度)的位置,所以砖块的间隔行的偏移以半块砖为单位。
if( fract(position.y * 0.5) > 0.5 )
{
position.x += 0.5;
position.z += 0.5;
}
接下来我们决定如果我们位于砖块或者水泥上。C
里的函数 step
返回 0 如果 position < brickPct.xyz
,否则返回1
(例如,它一直只是 0 或 1
)。这看起来跟下面这句一样:
if position < brickPct.xyz, useBrick = 0 else useBrick=1
但是要注意,对于每个 x,y 和 z
,它都会分别进行计算,例如 useBrick
是一个 vec3
position = fract(position);
useBrick = step(position, brickPct.xyz);
现在我们使用 mix
函数来把水泥和砖块的颜色组合起来,应用 useBrick
。我们对 useBrick
里的 x,y 和 z
的值进行相乘,因为我们只想绘制砖块的颜色当我们在 3
个方向上都位于砖块区域内时。命令 mix
等价于 Codea 中的 color:mix
。
结果被用来跟为 mesh
设置的全局颜色(vColor
)相乘。
color = mix(mortarColor.rgb, brickColor.rgb, useBrick.x * useBrick.y * useBrick.z);
color *= vColor.rgb;
//Set the output color to the texture color
gl_FragColor = vec4(color, 1.0);
}
我发现这个着色器有趣的地方是如何把你不想要的东西扔出去,而把你想要的其他东西包括进来 – 只要你足够小心!!!
没有比阅读更多例程代码更好的办法来学习着色器了。Codea
有一批内建的着色器可供你把玩,而且在互联网上有更多的,尽管它可能会引起混淆因为我们使用的是一种叫做 GLSL
的特殊的 OpenGL
着色器语言,所以最好把它们加入搜索关键词。
我也用一种方便的关于 GLSL
暗化 可用命令的概要参考,来自这里:
http://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf
只用最后两页。
全文结束 – End