原文地址:
http://archive.cnblogs.com/a/2429576/
今天我们来实现一个功能,我们来给精灵加一个遮罩,不过略微有点不同的是,我们的精灵的遮罩不是固定的,可以随着手指的移动,实现动态的遮挡精灵的不同部分。像上一篇教程一样,我们还是简陋的实现我们的主要功能,只给出解决问题的主要思路和方法,更丰富出彩的功能,需要你自己开动脑筋去实现。
我们这篇教程所涉及的知识,基本上都来自子龙山人译:的怎么用cocos2d 2.0实现精灵的遮罩和raywenderlich博客团队成员的另一篇文章,我们所做的功能,只不过是调整一些方法而已。再次感谢子龙山人,帮我们翻译这么好的文章,同样也感谢ray wenderlich的团队,写这么好的文章分享给我们,两位都是我们ios程序员的福音呀哈哈!!
介绍
首先,我们这篇文章需要用到cocos2d 2.x(就是我们所说的cocos2d 2.0的一个更新版),这是因为cocos2d 2.x是不同于cocos2d 1.0的,1.0版的cocos2d用的是openGL-ES1.0,而2.0版本的用的则是openGL-ES2.0,而我们这篇文章实现的根基就是openGL-ES2.0,在openGL-ES2.0是可以使用自己的着色器的(shader),我们的核心内容就是和shader打交道。如果你还没有安装cocos2d 2.x的话,你可以到cocos2d-iphone的官方网站去下载最新的版本,然后解压下载下来的压缩包,然后打开mac里的finder(相当于windows的我的电脑),按苹果的徽标键+shift+U来打开常用工具目录,然后打开我们的终端,在终端我们敲入字符cd,空格,然后把我们解压好的文件夹拖到终端上来,它会自动为我们填上这个文件夹的具体目录,回车,这样我们就已经进入了我们的cocos2d 2.x的安装源的根目录,再敲入以下命令:./install-templates.sh -f -u,回车,很快我们的cocos2d 2.x就装好了,这时你打开xcode,会发现多了一个cocos2d 2.x的模板选项,这就是我们想要的。(其实对于安装来说,个人更推荐用git的方式,具体方式参考怎么用cocos2d 2.0实现精灵的遮罩这篇文章,我就不再赘述了)。
开始
好了,如果你已经安装好了cocos2d 2.0的话,我们就可以开始了。打开xcode,选择new project,选择我们新添加的cocos2d 2.x模板,在右侧的面板里选择cocos2d ios
点击下一步,把我们的项目命名为:MoveMask,然后选择一个地方保存你的项目,最后点创建。
接下来,我们要使用ARC(自动引用记数)功能,使用ARC可以让我们不用过多的操心内存管理的问题,并且基本上不会出现内存泄漏等我们程序员最头疼的内存问题。虽然cocos2d是不支持ARC功能的,但是实际上你是可以为你的cocos2d项目使用这个功能的:
在xcode的菜单栏选择Edit\Refactor\Convert to Objective-C ARC,在打开的对话框中,确保展开MoveMask.app左边的小三角
如上图,把其它文件前面的勾都取消掉,只留下main.m,Appdelegate.m和HelloWorldLayer.m,点击check,然后在后面的几个对话框中都选择确定,这其中你可能会被问到是否为你的代码创建一个快照,你选创建和不创建都行,不过一般情况下还是创建一个比较好,这样当你的代码在遇到不可逆转的错误的时候可以恢复。好了,现在你就可以在你的cocos2d程序里用ARC了,是不是很简单。
现在编译运行我们的项目,你会发现在一个错误:(我后来又试了几次,它又不出错了,呵呵,总之如果有你在试的时候有同样的错误的话,可以和我用同样的方式解决它,如果没出错更好,直接进行后面的操作)
错误提示告诉我们,我们的window这个属性是一个需要被保持的对象,可是我们却可能会释放它,换句话说,我们的ARC系统没有为我们做好这个window属性的管理工作,它应该是一个强引用的,可是我们却没有给它分配strong属性。(我并不知道为什么造成了这个错误,如果你知道,请留言告诉我,谢谢!),不过管它呢,我们来修正这个错误,在appDelegate.h文件中,为我们的windown属性加上strong声明。好了,再次编译我们的项目,如果一切正常的话,你会看到下面的画面:
进入正题
一切都准备就绪了,我们来实现我们的功能吧。
哦,对不起,在我们还没有写我们的代码之前,我需要确定一件事,你知道shader是什么吗?shader(着色器)是一个用来执行渲染效果的小程序。它是由图形处理单元执行的,在移动设备上,有两种着色器:
1.vertex shader,这个叫顶点着色器,当每一个顶点被渲染的时候,都会调用这个着色器,所以当我们渲染一个精灵的时候,由于我们的精灵都是四个顶点的,所以我们的顶点着色器会被调用四次,它被用来计算我们的精灵每一个顶点的颜色和一些其它属性。
2.fragment shader,我们也可以叫它片源着色器(也可以说是像素着色器),这个着色器会在每个像素被渲染的时候被调用,也就是说,如果我们在iphone上显示一个全屏的图片的话,这个着色器会被执行480*320次。
这两个着色器不能单独使用,它们必须成对的使用,一对儿顶点和片源着色器叫做一个着色程序,它们通常是按下面的方式工作的:
顶点着色器首先确定每一个显示到屏幕上的顶点的属性,然后这些顶点组成的区域被化分成一系列的像素,这些像素的每一个都会调用一次片源着色器,最后这些经过处理的像素显示在屏幕上。
让我们先看一对简单的着色程序吧,首先是顶点着色器:
//1
attribute vec4 a_position;
attribute vec2 a_texCoord;
//2
uniform mat4 u_MVPMatrix;
//3
#ifdef GL_ES
varying mediump vec2 v_texCoord;
#else
varying vec2 v_texCoord;
#endif
//4
void main()
{
//5
gl_Position = u_MVPMatrix * a_position;
//6
v_texCoord = a_texCoord;
}
每一个着色程序都必需接受输入数据,并产生输出数据。在这个顶点着色器里,我们有三个输入数据,一个是保存每个顶点的位置,一个是保存每个顶点对应的纹理坐标,最后还有一个是用于整个精灵的位置、缩放、旋转的矩阵,这些数据都会在这个着色器被调用之前被传进来。我们还有两个输出数据在这个着色器程序里,一个决定了每个顶点的最终屏幕坐标,一个决定了每个顶点的最终纹理坐标。下面我们会讲到,这些输出数据是被片源着色器怎么使用的。
现在我们先来一点点地认真看一下这个顶点着色器:
注释1,我们定义这个着色器的输入数据,我们定义了两个数据结构变量。一个vec4类型,表明这个结构有4个float型数据组成,它用来存储顶点的位置。(你可能会奇怪,我们的2维世界的坐标怎么要用4个float来存呢?两个不就够了吗?这个牵扯的数学问题就比较复杂了,简单地说,是因为我们如果要对我们的精灵进行缩放、旋转、移动等操作的话,从图形学上来说的话,是要做矩阵的乘法的,保存我们转换信息的这些矩阵都是4*4的,所以我们的位置也要是4位的才能进行操作,具体4元数了什么的,那都太高级了,我也不清楚,如果你感兴趣的话,可以自己查这方面的资料)另一个是vec2类型,表明它有两个float型数据组成,它用来存储我们的纹理坐标。很重要的一点是,这个attribute关键字,它告诉编译器,被这个关键字修饰的变量是一个输入变量,这个变量的值要从每个顶点的数据结构中获得。(具体获得的地方在后面的代码中用到时我们会指出)
注释2,当你需要一些额外的数据传入到着色器的话,你需要把你的变量用uniform关键字声明。(注意uniform和attribute的区别,虽然都是输入数据,但是它们不同,attribute修饰的变量的值,是通过顶点数据结构传入的,而uniform修饰的变量的值,是我们在代码里传入的)这里我们声明了一个mat4类型,这是一个4*4的矩阵,如果你线性代数一窍不通的话,你只要记住这个矩阵是我们用来移动、缩放和旋转的就行了。(说到线性代数了我就不得不说下我的大学里的高数课了,在讲线性代数的时候,我们的教育只告诉我们这些知识的用法,就是只告诉你怎么进行运算,完全不告诉你它能干什么,所以学的时候,觉得这是什么玩意儿呀,有什么用呀,根本就不知道它能干什么,我怎么可能会去好好学它?!结果我的线性代数在考试的时候差点就挂科了,而且,我现在完全不记得它的用法了……。如果我在学的时候就能知道它是干什么的,结合它的实际用途去学习理解,我相信我会学好它的,不过貌似这是我们的教育的通病……)
注释3,这顶点着色器需要输出一些数据到片源着色器,这样它们才能协同操作,要实现输出数据到片源着色器,你需要用varying关键字来声明你的变量,这个最酷的事情是,varying变量的值是插值的,就是说如果,顶点A的坐标是定义成varying的,并设置值为0,顶点B坐标也是varying的,并被设置值为1的话,在片源着色器里,会自动把顶点A和顶点B中间的那个像素的坐标设置为0.5,每一个片源都是从顶点数据插值计算得到的。我们还看到在条件编译里有一个mediump 前缀,这个是用来决定计算的精度的,可用的前缀还有highp和lowp.它们分别代表高、中、低三个级别的精度,精度越高,数据越精确,不过计算起来也会更慢。
注释4,每个着色器都要有一个main函数
注释5,gl_Position是顶点着色器的内置变量,决定每一个顶点的最终位置,这个着色器里,是把这个输入的顶点坐标a_psoition乘以这个输入的变形矩阵u_MVPMatrix(这个矩阵是由cocos2d框架自动传进来的,我们不用操心这个,它决定了精灵的移动、缩放和旋转) 。
注释6,我们把输入的纹理坐标a_texCoord不做任何改变赋值给varying变量V_texCoord,以便传递给片源着色器。
下面我们接着看和这个顶点着色器配合工作的片源着色器的代码:
//1
#ifdef GL_ES
precision mediump float;
#endif
//2
varying vec2 v_texCoord;
//3
uniform sampler2D u_texture;
void main()
{
//4
gl_FragColor = texture2D(u_texture, v_texCoord);
}
注释1,我们强制设置在这个片源着色器里对float数的计算精度为中等精度。
注释2,任何在顶点着色器里的输出变量在片源着色器里都要被定义为输入变量,所以在这里再次定义这个纹理坐标变量
注释3,片源着色器也可以有自己的uniform变量,这种变量是从代码里传过来的常量,cocos2d会传入纹理到一个uniform变量,所以这里定义了一个用uniform声明的sampler2D类型的变量,它只是一个正常的纹理。
注释4,像顶点着色器的gl_Position一样,这个gl_FragColor也是一个内置变量,它决定了每一个像素的最终的颜色。这里只是通过顶点着色器得到的纹理坐标,获得纹理的相应坐标的正常颜色。
如果你还想了解它们是怎么一起工作的话,这个着色器在CCGrid.m里被调用,你可以打开这个文件去看看,在初始化方法里有下面代码:
self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];
这句代码从着色器缓存中加载这个着色程序。如果你好奇,你可以看看CCShaderCache文件,看看这个着色器是怎么编译和存储的。然后再blit方法里,它传递变量到shader,并运行这个着色程序。
-(void)blit
{
//1
NSInteger n = gridSize_.x * gridSize_.y;
//2 Enable the vertex shader's "input variables" (attributes)
ccGLEnableVertexAttribs( kCCVertexAttribFlag_Position | kCCVertexAttribFlag_TexCoords );
// 3 Tell Cocos2D to use the shader we loaded earlier
[shaderProgram_ use];
//4 Tell Cocos2D to pass the CCNode's position/scale/rotation matrix to the shader
[shaderProgram_ setUniformForModelViewProjectionMatrix];
//5 Pass vertex positions
glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, 0, vertices);
//6 Pass texture coordinates
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, texCoordinates);
//7 Draw the geometry to the screen (this actually runs the vertex and fragment shaders at this point)
glDrawElements(GL_TRIANGLES, (GLsizei) n*6, GL_UNSIGNED_SHORT, indices);
//8 Just stat keeping here
CC_INCREMENT_GL_DRAWS(1);
}
注释1,这个是特定于这个CCGrid.m类的,计算网格数的,我们不讨论它
注释2,这个是启用顶点着色器的输入变量的,这时我们启用了两个kCCVertexAttribFlag_Position和kCCVertexAttribFlag_TexCoords,实际上就是我们的顶点着色器里的a_Position和a_texCoord。这样我们能才使用这两个输入变量。
注释3,是告诉cocos2d我们要使用我们刚才在init方法里面加载的着色程序。
注释4,告诉cocos2d传入我们的CCNode的移动、缩放、旋转矩阵给shader,实际上就是为我们的顶点着色器里的输入变量u_MVPMatrix赋值。
注释5,传递顶点数据结构的位置数据给顶点着色器,实际上就是为顶点着色器里的输入变量a_Posigion赋值。
注释6,和注释5差不多,它是给顶点着色器里的a_texCoord赋值的
注释7,它是真正的openGL_ES绘制方法,把我们的精灵进行绘制。这个方法的第一个参数是GL_TRIANGLES表明,我们要绘制的图元是三角形,第二个参数是要绘制的图元的数量乘以每个图元的顶点数,这里是6表明,我们要绘制2个三角形,每个三角形有3个顶点,很显然两个三角形可以合成一个矩形,这个矩形就是我们的精灵,第三个参数是顶点索引数据的类型,最后一个参数就是指向索引存储位置的指针。
注释8,这个我们不讨论了。(因为我也不知道它是干什么的,好像是保持递增式的绘图的,我个人猜测有可能跟精灵的zOrder属性有关,我没有看这它的代码,对它的看法都是瞎猜的,不过这个真的不影响我们使用,所以我才没看它的代码的,只要保持这句话是这个样子就行了哈哈)
还没有开始真正的写我们的代码呢,就先灌输了这么一大堆东西给你,真的不好意思,不过这个对于理解我们的这个项目来说是真的很有必要的,所以我才对这些知识进行了解释,其实这些知识点的内容基本上就是直接译自raywenderlich博客团队的这篇:How To Create Cool Effects with Custom Shaders in OpenGL ES 2.0 and Cocos2D 2.X了,我本来想以自己的观点表达这些知识内容的,可是,原文表达的实在是太好了,所以不自觉的基本上就变成这个原文的译文了呵呵
好了,让我们干自己的活儿吧
现在你已经具备了编写sahder的能力,那我可以说你拥有了你对你自己的精灵的完全的控制权,你想呀,你的精灵的每一个像素你都能控制,还有什么是你不能做的效果呢?(当然说着简单,真正好的效果是需要扎实的图形学知识的)
下面我们创建一个ccsprite类的子类,命名为MaskedSprite.
选择File\New\New File,然后选择iOS\Cocoa Touch\Objective-C class,再点击Next,然后输入CCSprite为subclass,接着,点Next,把新的文件命名为MaskedSprite.m,最后点击Save。
然后把MaskedSprite.h替换成下面的内容:
#import "cocos2d.h"
@interface MaskedSprite : CCSprite {
CCTexture2D * _maskTexture;
GLuint _maskLocation;
CGPoint offsetWithPosition;
GLuint _offsetLocation;
}
-(void) postOffsetToShader:(CGPoint)aOffset;
@end
这里我们定义了一个变量_maskTexture来跟踪我们的遮罩纹理,一个变量_maskLocation来追踪遮罩纹理的uniform位置(稍后会在我们的着色器中看到),一个变量offsetWithPosition来存储我们的坐标的偏差,最后一个变量_offsetLocation来跟踪v_offset(稍后会在我们的着色器中看到)的uniform位置 ,然后我们声明了一个方法来给我们的offsetWithPosition赋值。
下面我们打开MaskedSprite.m,并替换它的内容如下:
#import "MaskedSprite.h"
@implementation MaskedSprite
- (id)initWithFile:(NSString *)file
{
self = [super initWithFile:file];
if (self) {
// 1
_maskTexture = [[[CCTextureCache sharedTextureCache] addImage:@"yuan.png"] retain];
[_maskTexture setAliasTexParameters];
// 2
const GLchar * fragmentSource = (GLchar*) [[NSString stringWithContentsOfFile:[CCFileUtils fullPathFromRelativePath:@"Mask.fsh"] encoding:NSUTF8StringEncoding error:nil] UTF8String];
self.shaderProgram = [[CCGLProgram alloc] initWithVertexShaderByteArray:ccPositionTextureA8Color_vert
fragmentShaderByteArray:fragmentSource];
// 3
[shaderProgram_ addAttribute:kCCAttributeNamePosition index:kCCVertexAttrib_Position];
[shaderProgram_ addAttribute:kCCAttributeNameColor index:kCCVertexAttrib_Color];
[shaderProgram_ addAttribute:kCCAttributeNameTexCoord index:kCCVertexAttrib_TexCoords];
// 4
[shaderProgram_ link];
// 5
[shaderProgram_ updateUniforms];
// 6
_maskLocation = glGetUniformLocation( shaderProgram_->program_, "u_mask");
_offsetLocation = glGetUniformLocation(shaderProgram_->program_, "v_offset");
//7
offsetWithPosition = ccp(-1000,-1000);
}
return self;
}
@end
我们重写这个initWithFile:方法
注释1,我们加载我们的遮罩纹理(这里我们的遮罩纹理是一个480*320的图片,但是它只有中间一个小圆是有颜色的,其它地方都是透明的),并且取消这个纹理的线性插值,我们要用它的真实颜色。
注释2,我们先加载我们自己的片源着色器(稍后会展示),然后用我们的片源着色器和一个cocos2d自带的顶点着色器,合成一个着色程序赋给我们的shaderProgram属性,这个属性指示我们的着色程序
注释3,和前面讲CCGrid类用的着色程序时的一样,这里只启用顶点着色器的attribute变量的,这里是坐标,纹理坐标,和颜色三个。
注释4,是对我们的这个自定义着色程序进行链接。
注释5,这个是cocos2d帮我们设置我们的移动、缩放、旋转的矩阵的
注释6,我们得到我们自己写的片源着色器里的两个uniform变量的位置,一个是U_mask,一个是V_offset,这样我们就可以在我们的代码里通过这两个位置为我们的这两个变量赋值了。
注释7,为我们的offsetWithPosition赋一个初值。
下面我们该实现我们的postOffsetToShader:方法了,在initWithFile:方法下面添加如下实现:
-(void) postOffsetToShader:(CGPoint)aOffset
{
offsetWithPosition = aOffset;
}
呵呵,就一句话,给我们的offsetWithPosition赋值为我们传过来的参数aOffset,实在没什么可说的这个方法,就是我们用来随时改变offsetWithPositon用的。
下一步,我们要做的就很重要了,我们要重写ccsprite的draw方法:
-(void) draw
{
CC_PROFILER_START_CATEGORY(kCCProfilerCategorySprite, @"CCSprite - draw");
NSAssert(!batchNode_, @"If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");
CC_NODE_DRAW_SETUP();
ccGLBlendFunc( blendFunc_.src, blendFunc_.dst );
ccGLBindTexture2D( [texture_ name] ); //1
glActiveTexture(GL_TEXTURE1); //2
glBindTexture( GL_TEXTURE_2D, [_maskTexture name] );
glUniform1i(_maskLocation, 1);
glUniform2f(_offsetLocation, offsetWithPosition.x, offsetWithPosition.y); //3
//
// Attributes
//
ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );
#define kQuadSize sizeof(quad_.bl)
long offset = (long)&quad_;
// vertex
NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, kQuadSize, (void*) (offset + diff));
// texCoods
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, kQuadSize, (void*)(offset + diff));
// color
diff = offsetof( ccV3F_C4B_T2F, colors);
glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (void*)(offset + diff));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
CHECK_GL_ERROR_DEBUG();
#if CC_SPRITE_DEBUG_DRAW == 1
// draw bounding box
CGPoint vertices[4]={
ccp(quad_.tl.vertices.x,quad_.tl.vertices.y),
ccp(quad_.bl.vertices.x,quad_.bl.vertices.y),
ccp(quad_.br.vertices.x,quad_.br.vertices.y),
ccp(quad_.tr.vertices.x,quad_.tr.vertices.y),
};
ccDrawPoly(vertices, 4, YES);
#elif CC_SPRITE_DEBUG_DRAW == 2
// draw texture box
CGSize s = self.textureRect.size;
CGPoint offsetPix = self.offsetPosition;
CGPoint vertices[4] = {
ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
};
ccDrawPoly(vertices, 4, YES);
#endif // CC_SPRITE_DEBUG_DRAW
CC_INCREMENT_GL_DRAWS(1);
CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite, @"CCSprite - draw");
glActiveTexture(GL_TEXTURE0); //4
}
如果你自己写这个darw方法的时候,如果不知道该写些什么,我建议你直接去ccsprite.m里copy它的draw方法出来,然后再根据你自己的需要进行改动,我就是这样做的哈哈。
draw方法里的代码好多,有的我们已经在前面讲CCGrid的着色器时讲到了,有的没讲,这些没讲到的我们也不用深究,对于我们自己定制这个draw方法来说,也就是这四个注释值得说一下:
注释1,这是一个纹理绑定动作,绑定我们的精灵纹理到纹理槽0,这里其实我们是偷了巧的,我们只写了一句话,其实它和注释2里的代码是同样的效果,在默认情况下,是纹理槽0处于激活状态,我们直接进行绑定操作,就是将我们的纹理绑定到当前激活的纹理槽上,所以是绑到纹理槽0上了,在我们自己的片源着色器里有u_texture,它是指向纹理槽的,而默认情况下它是0,这样我们的u_texture就代表纹理槽0内的纹理,也就是我们的精灵纹理,所以我们这样一句代码其实完成了三句代码的功能。
注释2,这是注释1的完整版代码,不同时的是,它是把我们的遮罩纹理绑定到纹理槽1了,并通过glUniform1i(_maskLocation, 1)这个方法来把1传递给我们的着色器里的U_mask,使其指向纹理槽1。这样u_mask就指向我们的遮罩纹理了。glUniform**()这个方法,是向第一个参数指向的位置(这个位置在前面我们已经获得了)代表的uniform变量赋值,方法时的数字数代表一次要传递几个数值,这时是1,代表我们要传递一个数值;数字后边的i代表我们要传的是整数,如果是f的话,就代表我们要传的是float数。
注释3,我们为我们的着色器里的v_offset赋值,它是一个vec2类型的变量,所以我们是传两个float给我们的v_offset方法。
注释4,这个注释在draw方法的最后一行,千万不要漏掉。这是我们重新激活纹理槽0为当前活动纹理槽,因为我们之前在注释2的时候激活了纹理槽1,所以还原这个当前活动的纹理槽是很重要的。
事实上这时候我们的这个MaskedSprite类已经完成了,只要实现我们自己的片源着色器就能用了。
现在,我们来写一个我们自己的片源着色器吧
回到Xcode,选择File\New\New file,再选择 iOS\Other\Empty,点击Next。然后命名为Mask.fsh并点击Save。然后我们的工程文件,MoveMask target,然后选择build phase标签,然后把我们的Mask.fsh从compile source里拖到copy bundle source里,这样它就会变成资源文件,而不是和我们的代码一起编译了。
然后把Mask.fsh的代码替换成下面的样子,然后我们会一步步讲解我们的mask.fsh里的具体代码:
#ifdef GL_ES
precision lowp float;
#endif
varying vec4 v_fragmentColor; //1
varying vec2 v_texCoord;
uniform sampler2D u_texture;//2
uniform sampler2D u_mask;
uniform vec2 v_offset;//3
void main()
{
vec2 onePixel = vec2(1.0 / 480.0, 1.0 / 320.0);//4
vec2 texCoord = v_texCoord;//5
vec2 finCoord = vec2(texCoord.x - onePixel.x*v_offset.x, texCoord.y + onePixel.y*v_offset.y);
vec4 texColor = texture2D(u_texture, v_texCoord);//6
vec4 maskColor = texture2D(u_mask, finCoord);
vec4 finalColor = vec4(texColor.r, texColor.g, texColor.b, maskColor.a * texColor.a);//7
gl_FragColor = v_fragmentColor * finalColor;//8
}
这段代码的最前面是我们设置这个段着色器对于float数据计算的精度为低。
注释1,这是两个从顶点着色器传过来的两个变量,一个是每个片源的颜色,一个是每个片源的纹理坐标
注释2,这是我们定义了两个纹理,它是被声明为uniform的,所以我们需要在代码里传入这两个纹理。一个是我们的精灵的正常的纹理,一个是我们的精灵的遮罩的纹理。
注释3,我们定义了一个vec2变量v_offset,用来存储我们的精灵的纹理和我们的遮罩的纹理之间的位置偏差(我们就是通过这个偏差的变化来实现遮罩的移动的)。同样的,我们也把它声明为uniform的,这样,它也需要我们在代码里来给它赋值。
注释4,定义了一个vec2类,它用来存储一个像素的宽和高,这样我们在移动的时候就可以通过,移动n倍的这个变量,来达到移动多少个像素的目的。
注释5,我们先把由顶点着色器传进来的纹理坐标赋给一个新的vec2变量,然后通过一系列的计算,得到在v_offset这个偏差存在的情况下,一个我们的精灵的纹理坐标的位置,对应的精灵的遮罩的纹理坐标。
我们来详细地说一下这个计算。首先明确一个事情,我们的精灵的纹理和我们的精灵的遮罩的纹理是一样大的,在本例中,它们都是480*320大小的,我们就拿两张大小一样的扑克牌来想像一下具体的情况,下面的牌相当于精灵纹理,上面的相当于遮罩纹理,这样,在v_offset这个变量是(0,0)的时候,就相当于是这两张牌完全合在一起,上面的盖着下面的,下面的完全看不到。当你把这两张牌的上面一张移动了一定的距离使 这两张牌叠在一起,但不完全重合的时候,我们可以看到,在这两张牌叠在一起的部分,现在的下面的牌的纹理坐标的位置,和叠在它上面的牌的纹理坐标的位置不再是一样的了,它们之间有了偏差。而这个偏差的值就是我们上面的牌移动的距离,其实也就是我们的v_offset此刻的值,图片应该比文字更能说明问题:
这个图片代表两个纹理完全重合的时候,可以看到它们重合的位置纹理坐标是一致的,再看它们不重合的时候:
图中我用蓝色框代表精灵纹理,绿色框代表遮罩纹理,图中的黄色区域就是在有了偏移后,两个纹理的重合部分,在图中我们可以清楚的看到,在重合的部分,现在的精灵的纹理坐标(0,0)对应的位置,不再是遮罩纹理的(0,0)坐标,而是图中两个红线的交叉点,遮罩纹理的大概(0.3,0.2)坐标的样子,这个变化的数就是我们的偏差值,也就是我们的v_offset的值。因此精灵纹理的纹理坐标,加上这个V_offset就可以得到在它的纹理坐标位置上相应的遮罩纹理的纹理坐标。好的,知道了这个v_offset是干什么的,我们就可以很简单的看明白这个计算做了什么操作了。因为这个v_offset的值是计算的坐标的差(下面的代码中会有介绍),因些在我们的着色器里,要得到纹理坐标的偏移量,我们需要用我们传进来的v_offset.x乘以一个像素的宽(即onePixel .x)来得到精灵纹理坐标与遮罩纹理坐标在x方向上的偏移量,同样的v_offset.y*onePixel.y就可以得到在y方向上的偏移量了。
那么有这个偏移量,我们应该怎么得到这个偏移后的遮罩纹理的纹理坐标呢?这个其实是要具体情况具体分析的,(你计算偏移的方式不同,这里计算的方式也会不同,你计算偏移的时候的减数和被减数的位置会决定你这里是应该用加法还是减法,你后面会看到我们计算偏移的方式是用我们的触摸点的位置减去我们的精灵的位置的),这里我们用减法,因为如果我们的触摸点和在精灵的位置的左边的话,情况就和上图中的情况差不多,这时候,按我在后面计算偏移的方式的话,我得到的偏移量应该是负的,这而这时候我们看图片就能看出来,精灵纹理的纹理坐标位置对应的偏移后的遮罩纹理的纹理坐标应该是增大了,所以这里我们要用减法,所以(texCoord.x - onePixel.x*v_offset.x),这个式子得到偏移后的精灵纹理的纹理坐标位置对应的偏移后的遮罩纹理的纹理坐标的x值,注意,这里有一个大陷井,不要觉得x是用减法了,那么求偏移后的y也应该用减法,不要忘了一件事情,iPhone中用于Core Graphics的图像坐标系统和我们用的opengl_es的坐标系统的y轴方向是相反的,opengl_es是从下向上增大的,而core graphics上从下向上减小的,所以我们得到的y的偏移量其实是相反的,所以这里得到偏移的y,我们需要用加法,(texCoord.y + onePixel.y*v_offset.y),这样这个式子就得到了实际的偏移后相对应的遮罩纹理的纹理坐标。
然后我们把这个求得的坐标赋值给finCoord 。
注释6,我们根据我们的精灵纹理的纹理坐标,和偏移的的遮罩纹理的纹理坐标,得到屏幕上同一点对应的,这两个不同纹理的的具体的颜色,分别赋值给texcolor和maskcolor。
注释7,我们用我们的精灵纹理的rgb颜色做为一个新的颜色的rgb值,但是我们用我们刚刚得到的两个texcolor和maskcolor的透明度值相乘做为这个新的颜色的透明度值,这样主是在同一点上,如果遮罩纹理上的透明度为0的话,这个新的颜色的透明度就是0,那么在这个点上这个颜色就不可见了,如果遮罩上不是零的话,而那么相乘的透明度就不是零,那么这个颜色是可见的。
注释8,我们用注释7得到的颜色乘以我们从顶点着色器得到的颜色,赋给我们的最终每个像素的颜色。
哦,好累呀,这一段东西实在是太绕口了,我自己都感觉说不清楚了,还是对着图片多想想的话容易明白吧。吼吼……
希望就在前方
最后,我们应该使用我们自己的这个MaskedSprite类了,打开HelloWorldLayer.h,在文件的顶部包含我们的类:
接着在interface里加入我们的变量:
让我们打开HelloWorldLayer.m文件,替换它的init方法如下:
-(id) init
{
if( (self=[super init]))
{
hello = [MaskedSprite spriteWithFile:@"ground.jpg"];
CGSize size = [[CCDirector sharedDirector] winSize];
hello.position = ccp( size.width /2 , size.height/2 );
[self addChild: hello];
self.isTouchEnabled = YES;
}
return self;
}
很简单的,我们初始化我们自己的MaskedSprite类的一个对象,把它加入到屏幕中央的位置,然后加入到我们的层,最后我们启用这个层的isTouchEnabled方法,这样我们就可以让这个层响应我们的触摸事件了。
在init方法下面实现下面方法:
- (void)registerWithTouchDispatcher {
[[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self
priority:0 swallowsTouches:YES];
}
这个方法重新注册我们对touch事件的响应方式,默认的话cclayer是响应standard touch delegate的,现在我们让它响应targeted touch delegate。(两种不同的响应方式请参考CCTouchDelegateProtocol),priority是响应的优先级,这是高为0,swallowsTouches设为Yes,这样只我们的touch处理方法响应并处理了这个touch事件,这个touch事件就不再继续分发了。
下面主就是实现我们的touch响应方法了:
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint tempPoint = [self convertTouchToNodeSpace: touch];
tempPoint = ccpSub(tempPoint, ccp(240,160));
[hello postOffsetToShader: tempPoint];
return TRUE;
}
-(void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint tempPoint = [self convertTouchToNodeSpace: touch];
tempPoint = ccpSub(tempPoint, ccp(240,160));
[hello postOffsetToShader: tempPoint];
}
-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
[hello postOffsetToShader:ccp(-1000,-1000)];
}
在touchBegan方法中,我们得到我们的触摸点在layer中的坐标,用这个坐标减去我们的MaskedSprite的实际位置,这样就得到了触摸点和我们的精灵的位置这间的偏差,把这个偏差传递给我们的精灵的postOffsetToShader:方法,这样这个偏差值就传给了我们的精灵了,它把这个值赋给精灵的offsetWithPositon变量,而这个变量又会在我们的精灵的draw方法里通过注释3的那句代码传递给,我们自己写的片源着色器里的被uniform声明的v_offset变量。然后经过我们的着色器的计算,就得到了我们的移动的遮罩的效果。
我们返回YES,表明,只要接收到touch事件,我们就处理。
在touchMoved方法中,我们重复在began方法里的计算
最后,在touchend方法里,我们传递给我们的精灵一个极大的负坐标,这个值和在我们的MaskedSprite类的init方法里,给offsetWithPosition赋的值是一样的,这样做的目的是,在没有touch事件的时候,让我们的遮罩纹理和精灵纹理完全不重叠,这样精灵纹理所处的位置的遮罩纹理的透明度值肯定是0,这样在没有触摸的情况下,我们的精灵就不可见了。可是只要你在屏幕点一触摸你就会发现在,有一个经过遮罩处理的精灵显示在你的触摸的位置,你移动你的手指,这个遮罩的位置也随着你移动。
利用这个功能我们能做什么
这个功能可能会在塔防类的游戏里会有点用,就像本例中的一样,如果在我们的HelloWorldLayer里先加一个背景,然后再加一个我们的MaskerSprite精灵的话,这个就可以实现,动态地只有在触摸发生的情况下才在我们的游戏背景上显示一个只在自己的遮罩范围(遮罩应该是一个塔的攻击范围的图片)内可见的一个位置网格是不是很不错?像这样:
如果你有多个塔,每个塔的攻击范围是不一样的,你只需要在我们的MaskedSprite类里多加几个遮罩纹理就好了,在拖动不同的塔的时候,根据塔的类型来绑定不同的遮罩纹理到纹理槽1,就可以了吧似乎,具体行不行,你试试吧呵呵,我也不确定能不能行呵呵。
这个demo虽然笨拙,但是它还是能工作的,如果你有更好的实现的方法,留言告诉我,我会好好学习的,谢谢。能力有限,文中可能会用不对的地方,希望大家指教。谢谢!!
参考文章:
dingwenjie博客 http://www.cnblogs.com/dingwenjie/archive/2012/04/02/2429576.html
子龙山人博客http://www.cnblogs.com/andyque/archive/2011/09/16/2155068.html