为新手准备的 Codea 着色器(Shader)教程

为新手准备的 Codea 着色器(Shader) 教程

原文标题:《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

顶点着色器Vertex Shader 容许你一次修改一个顶点(一个顶点就是一个三角形的一个角,记住在计算机中全部的图形都是由三角形组成的)app

译者注:以下所示:dom

3D网格模型

片断着色器 --Fragment shaders

片断着色器Fragment shaders 容许你一次修改一个像素点的颜色(以及纹理贴图的坐标位置)。ide

这些听起来都很复杂,不过别被吓跑。函数

着色器Shader 听起来很是神秘和困难,可是实际上它们并不是那样的。

这个教程尽可能不使用专业术语,也尽可能不使用矩阵。不须要太多的数学。大多数是一些简单的例子。

不过有一个警告:它并非为全部的初学者准备的。

若是你不知足下面这些描述就别再日后看了:

  • 编程让你感受很轻松舒服

  • 熟悉 Codea 中的 mesh(画刷) 和 image texture(图形的纹理贴图),而且-->

  • 准备好学习一丁点 C 语言(我保证只要一丁点!)

  • 返回目录

着色器是什么 --What are shaders?

我读过一些关于着色器是什么的解释,它们谈到了 pipelines(管道)、vectors(向量)、rasterisation(图形栅格化)、scissor tests(剪切测试),以及一些复杂的示意图。这种讨论直接让我远离了对着色器 的学习好几个月。

在此输入图片描述

我确信,像我同样,你也会喜欢真正简单明了的解释,没有任何上述的险恶术语。

管道 --Pipes

OpenGL 就像一个长长的管道。你从这一头把你的绘图指令(如sprite,mesh等)放进去,像素点会从另外一头出来,这些像素点会在你的屏幕上造成一幅 3D 图像。在管道内部是一系列复杂处理,咱们不会伪装理解。

所以让咱们聚焦到管道外。

在管道上有两个位置被挖了两个洞,所以你能经过这两个洞看到管道里流过的信息,而且你还能够进到里面去修改这些信息,这些信息处于整个体系的最底层,下图是细节:

在此输入图片描述

OpenGL 管道上的两个洞容许你改变里面的信息流,不过它们假定你知道本身在作什么,而且在这里写的代码不像 Codea 中那么简单和宽容--任何错误均可能会简单地致使你的屏幕一片空白。你没法作出任何打断。

不管如何咱们都会大胆地偷窥这两个洞。不过首先,咱们得了解更多关于这两个洞里的信息流在干什么,这样咱们才能理解咱们在洞里看到的。

从顶点到像素点 --Vertexes to pixels

在制做动画片时,熟练的艺术家不会画出每个单独帧。他们绘制关键帧,而后让其余人(或者如今是计算机)去填充位于关键帧之间的中间帧,这个处理被称为 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 接着会经过在顶点(顶角)值之间插值的办法 在每一个三角形的内部绘制出全部的点。

回到那个管道的洞上:

在此输入图片描述

顶点着色器 --Vertex Shader

管道上的一个洞位于信息流中 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(着色器实验室),它会显示语法错误信息,做为一点帮助。

在此输入图片描述

不过咱们随后将会返回上述所有这些,我刚刚意识到每同样仍然有些混淆。

先在这里挂起,不久将会更清楚。

片断着色器 --Fragment Shaders

管道上的第二个洞位于这个位置,在这里 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 shaderfragment-片断 只是像素点-pixels 的另外一个叫法)。

所以:

  • 顶点着色器-Vertex Shader 影响独立的顶点们
  • 片断着色器-Fragment Shader 影响独立的像素点们

在关于它们是如何作的这一点上,将仍然是一个彻底的秘密,不过我会给你一些例程来帮助你理解。

顶点着色器 --Vertex Shaders

如今咱们看看基本的 顶点着色器-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),还有 vec3vec4
  • bool = boolean(true 或 false) 布尔型,真值或假值
  • int = integer 整型
  • float = 带小数的数 浮点型
  • sampler2D = 一个 2D 图像
  • mat2 = 2*2 的表(mat3mat4 分别是 3*34*4 的表)

所以你必须在你添加的任何变量前面包括其中任意一种类型

三、这些变量用来作什么?

OpenGL 须要知道你这些变量是拿来作什么用的。一个出如今这个着色器中的变量有三个可能的缘由。

(a)attribute - 是一个输入,提供关于这个特定顶点的信息,好比,它的值对于每一个顶点都是不一样的。明显的例子就是位置坐标,颜色和纹理坐标,而且你将会在上述代码中看到它们所有。这些输入由 Codea 自动为每个顶点提供。

(b)uniform - 也是一个输入,不过对于每一个顶点来讲没有变化。例如,Codea 中的 blend shader(译者注:能够在着色实验室找到这个着色器)定义了第二幅图像用来跟一般的 mesh 纹理贴图图像进行混合,而且这幅相同的图像将会被用于全部的顶点,所以它是 uniform-统一的。在标准代码中只有一个 uniform - modelViewProjection - 并且咱们如今不会讨论它,由于它是 3D 黑盒子的一部分。

(c)varying - 这些是输出,将被用于插值独立像素点,还将会在 片断着色器-fragment shader 中可用。这里是它们中的两个 vColorvTexCoord,你能够添加更多的。

让咱们再次总结一下:

  • 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)变量.

基本上,它什么也没作(除了一个神秘的矩阵相乘)。

如今该由咱们来改变它了。

改变顶点着色器 --Changing the vertex shader

是时候来改变那个 顶点着色器-vertex shader 了。这也正是它存在的意义。

首先,我想分享关于用一种你一无所知的语言编写代码时的个人规则

不论何地,尽量地,窃取一行已经存在的能工做的代码

(译者注:大意是,对于一门陌生的语言,尽可能参考引用别人写好的完善的代码)

这将会有点困难,当咱们被给了这么少的几行代码做为开始时,不过 Shader Lab 包含了大约 15 个着色器的代码,而且其中很多代码咱们均可以偷来(以及研究)用。

翻转图像 --Flipping a image

首先,让咱们试着翻转一幅图像,这样咱们就会获得一个镜像图像。在 Shader Lab 中你本身定制的 着色器中尝试。咱们的目标是把 CodeaLogo 变成一个镜像图像。

翻转图像最简单的办法是改变纹理贴图的全部坐标,这样 OpenGL 就会由从右到左绘制换成从左到右绘制。你应该记得纹理贴图的位置坐标是介于 0 到 1 之间的分数,0 位于左边(或者底部),1 位于右边(或者顶部)。若是咱们用 1 减去 x 值,咱们将会获得想要的结果,由于位置(0,0)(左下角)会被改变为(1,0)(右下角),反之亦然。

所以,让咱们看看 顶点着色器-vertex shadermain 的内部,这就是咱们要改的那一行

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 LabLogo 翻转为一个镜像图像。

如今来看看你可否用相同的方法翻转 y 值。。。

你能用它来作什么?假定你有一个图像来指向一条路,并且你但愿能用它指向另外一条路。如今你只须要一个图像就能够实现了。

给用户提供翻转图像的可选项 --Giving the user the option to flip the image

咱们如何为用户提供翻转图像的可选项?这将会是一个对于全部定点都相同的输入,所以,它将会是 uniform ,对不对?

它也是 truefalse,因此它是 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 中的类似,除了判断条件被放在圆括号中,而且,若是 ifelse 代码超过1行,你须要用大括号 {} 把它们包围起来。

若是你用上面这些替换了 Shader Lab 里的 vertex 标签页的代码,什么也不会发生,由于 flip 的默认值是 false。不过若是你到了 binding 标签页(在这里你能够设置测试值),你将会看到一个项目 flip 已经被添加,而且若是你把它设置为 trueCodea Logo 将会翻转。

这个例子展现给咱们的是咱们能够经过很是少的代码来实现很酷的效果,并且咱们能够经过命令来让 着色器去作各类不一样的事情。固然了,我意识到你想知道如何在 Codea 代码中设置 flip 的值。咱们很快就会讲到这一点。

下一步咱们会去看 片断着色器-fragment shader,拥有多得多的用于改造的潜力。

片断着色器 --Fragment Shaders

如今咱们会去查看 `片断着色器-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 函数中的变量的话,你就会看到一些老朋友,vColorvTexCoord,并且它们确实用了相同的方式来定义。

不论如何,它们确实不同,由于在 顶点着色器-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, varyinguniform),由于对 main 函数而言,它是一个纯粹的局部变量。

函数 texture2D 就像 Codea 中的 myImage:Get(i,j)。它取得纹理贴图图像中位于 x,y 处的颜色x,y 的取值范围是 0~1

gl_FragColor = col;

第二行简单地把它传递给用于输出的变量 gl_FragColor

这是至关无聊的,因此让咱们试着改动它。

改变颜色 --Making a colour change

在你的 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 的值会跟对应的 vColorr,g,b,a 的值相乘。

你能经过一个简单的实验来理解这些。咱们将会使 Logo 变黑。

把最后一行改成:

gl_FragColor = col * 0.2; //把全部值的亮度都降到 20%

这会有效果,由于 0.2 会跟 col 中的每一个 r,g,b,a 相乘。

如今,能从 Codea 去作这些将会真的很酷,好比让你的景色从亮变暗。

那么,这一次就让咱们从 Codea 来作这些吧,OK?

在 Codea 代码中使用着色器 –Using your shader from Codea

你大概一直跟我在 Shader Lab 流连,而且如今你已经有了一个你本身的改变了一些东西(顶点或片断)的着色器。

使用你本身的着色器 –Using your own shader

你能够很容易地试验它。返回到 Codea 程序的主界面,而且调用那个着色器示例项目。在第 20 行有一个着色器被命名为 Effects:Ripple。点击这个名字,而且从弹出菜单的 Documents 区域选择你的着色器来代替。而后运行,你就会在屏幕上看到你的着色器作出的改变。

这意味着对一个普通的着色器作出简单的改变是至关容易的,马上在你的代码中使用你的着色器版本。事实上,仅仅须要一行代码来把你的着色器mesh 关联起来。

myMesh.shader=('Documents:MyCoolShader')

在 Codea 中设置变量 –Setting variables from Codea

让咱们更进一步,建立一个着色器,在咱们画面中实时改变亮度。

首先,回到 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 参数时,图像慢慢地以下图所示般变淡,你能够写一个循环让它平滑地去作。

由于咱们创造了一个淡入淡出的着色器,或者叫雾化。很是简洁。

一个替代选择 --An alternative

你还能用一个咱们的着色器里已有的变量-不过该变量尚未使用过-来尝试,就是 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))

你可使用这个经验来实现翻转,回到上述的着色器代码便可,而且由白天变为黑夜,而不是雾化。全部须要咱们作的只是经过 lightr,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 减小时图像变黑了(若是你想让你的背景同时变黑,只要在 Codeabackground 函数中改变颜色就能够了)。

所以你如今应该明白如何新建一个着色器,它能够制造雾化效果,淡化你的图像,或者让你的图像变黑。你能够经过内建的 color 变量来实现,也可使用你本身新建的变量来实现。这种效果对于仅用几行代码来讲是至关强大的。

若是你给着色器两个 uniform 变量,你就能实现雾化、暗化。

不过我猜你也能看到这些都花费了一些时间去习惯和实践。不过我向你保证,我也没有一两次就把全部代码写对。(译者注:第一句感受含义不大清楚,结合上下文,大概就是说上面的例子都通过反复调试,不影响理解)

把着色器代码嵌入你的代码中 –Embedding Shaders in your code

我想开始给你不少例子,不过首先,我想向你演示如何把着色器代码包含在你的 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 中使用不一样的着色器(固然是在不一样的时间)。

哪一种方式更好? –Which way is better?

我一般把着色器嵌入个人代码中,所以它们是可移植的。不过若是你有了一个错误,你不得不本身去找,然而,若是你在 Shader Lab 中建立了着色器,它会对语法错误作出警告,这颇有帮助。因此一切取决于你。你能够先从 Shader Lab 起步,后面代码没问题了再把它们拷贝到 Codea 中嵌入。

着色器例程 –Examples of shaders

我如今准备给你至关一些着色器例子。由于它们中的不少都很简单,而且只涉及 顶点着色器-vertex shader 或者 片断着色器-fragment shader 中的一种,- 而不是同时包括二者 - 我以为没有改变的代码不必重复。

因此我准备从那些我建议你拷贝到 Codea 的标准代码开始,而后是每一种着色器(译者注:就是先 顶点-vertex片断-fragment)。我会演示给你看,在标准代码中改变哪些内容,来让着色器生效。我将会把着色器嵌入到 Codea 代码中。

接下来就是起步的代码,包括一个仍然什么都不作的着色器。咱们主要目标是把颜色改成红色。

建议 –Suggestions

你能够为每一个着色器起一个不一样的名字,不过也别忘了同时在 setup 中修改把 shadermesh 关联起来的那行代码。

译者注:就是这个:

MyMesh.shader=shader(MyShader.vertexShader, MyShader.fragmentShader)

个人建议是保持这些位于 Codea 左手边标签页的代码不要改变。当咱们试验每个新例程时,在右边新建一个标签页并把全部标准代码都拷贝进去,而后在那里修改它们。这意味着你将创建本身的着色器库,当你摸爬滚打在不一样的例程中。

注意 - 若是你终止于 8 个标签页时(最多使用 8 个时),每一个标签页都有本身的 setupdraw,没什么关系。当 LUA 在运行前编译,它会从左到右执行,而且若是它找到重复的函数,它仅仅替换掉它们。所以位于右侧标签页的代码是最终被执行的那个 - 你也能够把任何一个标签页拖到最右侧来让它执行。

译者注:Codea 有一个使用技巧,它在拷贝/粘贴到项目时能够把位于同一个文件中的不一样标签分开,只要你在每一个标签页的代码最前面用 --#标签页1 来标识便可

请注意另一些事情。在下面提到的任何着色器例程中,我会把着色器用到的变量放在 draw 函数中,例如:

m.shader.visibility=0.5

惟一的理由是我要使用参数来改变设置,在任什么时候候用户都要能设置,所以 draw 函数须要始终得到最新值。然而,若是设置一直都不变,例如,若是你正使用雾化/暗化化着色器,而且你只须要雾化,那么你就能够在你第一次把 shadermesh 关联时就把设置好的值发送给着色器,你就不须要在 draw 里去作这些(一旦你设置好了,它会一直保持同一个值,直到你再次改变)。

最后一句,你会很惊讶这些解释和 Codea 代码某种程度上比任何实际着色器代码的改动都要长。不会一直是这样的,固然了,这样会确保你可以理解这些例程。

为了更容易一些,在写这份教程时,我已经完成了所有的例程代码,并且你能够在这个项目里找到它们:

https://gist.github.com/dermotbalson/7443057

不过若是你用的是 iPad 1,那就用这个:

https://gist.github.com/dermotbalson/7443577

直接选择你想要运行的着色器而后运行它。它们中的每个都位于本身的代码标签页内,而且能够被拷贝到其余项目,不须要任何改动就能够运行。

标准代码 –Standard Code

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; 
	}
]]}

雾化/模糊 --Fog/mist

让咱们从咱们作过的开始。咱们会让图像在朦胧不清的雾中淡入淡出。

我打算把咱们的着色器叫作 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;

就是它了,你如今能够跟这个能见度参数小伙伴一块儿好好玩耍了。

明暗 --Light/dark

咱们已经到了这里,让咱们制做一个能把一幅图像变亮、变暗的版本。这跟雾化着色器很类似,除了咱们没有调整像素点颜色的 alpha 值。

所以咱们可使用雾化着色器的代码,只改变其中一行:

vColor=vec4( color.rgb * visibility, color.a );

让咱们勇敢地把它们结合起来,既然它们如此类似。

我会在 Codeasetup 中放入一个参数,这样咱们就能够在它们之间切换,若是没错,咱们的 着色器将会绘制雾,或者它会把一幅图像亮化或暗化。

parameter.boolean("Fog",true)

把它放到 draw 中:

m.shader.fog=Fog

再把它做为另外一个 uniform 变量放到 顶点着色器-vertex shader 中:

uniform bool fog;

接着改变 顶点着色器-vertex shadermain 函数中的代码,这样它要么用能见度系数乘以整个颜色(译者注:即 r,g,b,a),要么只乘以 r,g,b

if (fog) vColor=vec4( color.rgb, color.a ) * visibility; 
else vColor=vec4( color.rgb * visibility, color.a );

基于雾或黑暗的距离 --Distance based fog or dark

这样是否是很酷,当物体远去时雾会变得更浓(在一个 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 中绘制(须要 perspectivecamera 命令),我还想让背景的明暗由是否使用雾化来决定。我还须要在当前距离绘制一个照片(由 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

翻转着色器 --Flip shader

咱们最开始的第一个着色器,翻转一幅图像来制做镜像。咱们也能够把它包含进来,经过标准代码来实现。

咱们将会在 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 的时候了。

拼贴着色器 --Tile 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

轮廓着色器 --Panorama shader

咱们能够在更多的场合使用拼贴着色器,而不只仅用来拼贴巨大的表面。假定你正在制做一个平台游戏,你想要让一个背景连续卷动,产生移动着的视觉暗示(译者注:好比横版卷轴游戏)。你的背景图像须要本身重复本身,好比当你走到头时再次开始动,这跟把一个图像拼贴满一个大型区域很是类似。

因此这段 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 函数只会保留小数,所以图像会被拼贴,从而给出一个很平滑的接二连三的城市背景。

透明着色器 --Transparency shader

一旦你开始使用多幅图像,一个常见的问题是 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

蒙版着色器 --Stencil shader

假定你想让一幅图像像面具同样半遮半掩在另外一幅图像上面,例如你想从一幅图像里剪切出一个形状来,或者可能仅仅画一幅图像的一部分来覆盖到第二幅图像上。

看看下图的例子:

在此输入图片描述

在此输入图片描述

在第一幅图像中,一个小公主的形状被用于从图像上剪切了一个剪影洞。

在第二幅图像中,一个小公主的形状用一个红色五星图像画了出来。

译者注:小公主形状来自 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内建) --Brick shader (built into Codea)

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;

若是砖块数目是偶数,它就以半块砖为单位来移动 xz(深度)的位置,因此砖块的间隔行的偏移以半块砖为单位。

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

相关文章
相关标签/搜索