原文标题:《Shaders for dummies》 做者:Ignatz 译者:FreeBlues 译文连接:http://my.oschina.net/freeblues/blog/336055 PDF连接:http://pan.baidu.com/s/1c0xTUzIgit
译者注:github
一、Codea
是一款能够在 Ipad
上直接编写游戏的 APP
应用软件,它使用 Lua
的语法和库,并针对 iPad
提供了一系列函数,诸如绘图、触摸、摄像头、重力感应乃至网络等等,Codea
支持 OpenGL ES
,它编写的程序能够直接在 iPad
上运行,也能够导出为 Xcode
项目,再用 Xcode
编译为可发布在 App Store
的应用程序。编程
二、本教程讲述的内容适用于 Codea
环境,跟 OpenGL ES
在其余开发环境的使用会有一些不一样。数组
Codea
建构于 OpenGL ES Shading Language
(开放图形库 嵌入式系统 渲染语言)之上,它(OpenGL
)提供了很是复杂的工具,以及一连串复杂处理,用来把像素绘制到屏幕上。安全
在这一系列处理中有两个步骤:Vertex Shader
(顶点着色) 和 Fragment Shader
(片断着色),Codea
容许咱们把这二者混合在一块儿来使用。为何应该为此而兴奋?由于它给了你访问底层图形的能力,容许你建立很是强有力的视觉效果。网络
顶点着色器Vertex Shader
容许你一次修改一个顶点(一个顶点就是一个三角形的一个角,记住在计算机中全部的图形都是由三角形组成的)app
译者注:以下所示:dom
片断着色器Fragment shaders
容许你一次修改一个像素点的颜色(以及纹理贴图的坐标位置)。ide
这些听起来都很复杂,不过别被吓跑。函数
着色器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
三件事:
一、Precision
(小数的位数)-- 精度
有三个选项,highp, mediump, lowp
,你能够猜想它们的含义,若是你没有指定一个,默认值是 highp
。就如今而言,全部这些你均可以彻底忽略,由于咱们要作的任何事都不须要特别的精度。
二、变量的数据类型
若是你编写过其余程序,你会习惯于指出一个变量是不是一个整数,一个带有小数的数,一个字符串,一个数组等等。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
的表)所以你必须在你添加的任何变量前面包括其中任意一种类型
三、这些变量用来作什么?
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
特别注意这些:
一、在 draw
函数中,刚好在绘制 mesh
以前,我基于 parameter
的值设置了 lighting
变量,把它当作一个除以 255
的分数
二、你须要把变量 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