原文地址:
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