原文地址:
http://archive.cnblogs.com/a/2429576/
今天我們來實(shí)現(xiàn)一個(gè)功能,我們來給精靈加一個(gè)遮罩,不過略微有點(diǎn)不同的是,我們的精靈的遮罩不是固定的,可以隨著手指的移動(dòng),實(shí)現(xiàn)動(dòng)態(tài)的遮擋精靈的不同部分。像上一篇教程一樣,我們還是簡(jiǎn)陋的實(shí)現(xiàn)我們的主要功能,只給出解決問題的主要思路和方法,更豐富出彩的功能,需要你自己開動(dòng)腦筋去實(shí)現(xiàn)。
我們這篇教程所涉及的知識(shí),基本上都來自子龍山人譯:的怎么用cocos2d 2.0實(shí)現(xiàn)精靈的遮罩和raywenderlich博客團(tuán)隊(duì)成員的另一篇文章,我們所做的功能,只不過是調(diào)整一些方法而已。再次感謝子龍山人,幫我們翻譯這么好的文章,同樣也感謝ray wenderlich的團(tuán)隊(duì),寫這么好的文章分享給我們,兩位都是我們ios程序員的福音呀哈哈!!
介紹
首先,我們這篇文章需要用到cocos2d 2.x(就是我們所說的cocos2d 2.0的一個(gè)更新版),這是因?yàn)閏ocos2d 2.x是不同于cocos2d 1.0的,1.0版的cocos2d用的是openGL-ES1.0,而2.0版本的用的則是openGL-ES2.0,而我們這篇文章實(shí)現(xiàn)的根基就是openGL-ES2.0,在openGL-ES2.0是可以使用自己的著色器的(shader),我們的核心內(nèi)容就是和shader打交道。如果你還沒有安裝cocos2d 2.x的話,你可以到cocos2d-iphone的官方網(wǎng)站去下載最新的版本,然后解壓下載下來的壓縮包,然后打開mac里的finder(相當(dāng)于windows的我的電腦),按蘋果的徽標(biāo)鍵+shift+U來打開常用工具目錄,然后打開我們的終端,在終端我們敲入字符cd,空格,然后把我們解壓好的文件夾拖到終端上來,它會(huì)自動(dòng)為我們填上這個(gè)文件夾的具體目錄,回車,這樣我們就已經(jīng)進(jìn)入了我們的cocos2d 2.x的安裝源的根目錄,再敲入以下命令:./install-templates.sh -f -u,回車,很快我們的cocos2d 2.x就裝好了,這時(shí)你打開xcode,會(huì)發(fā)現(xiàn)多了一個(gè)cocos2d 2.x的模板選項(xiàng),這就是我們想要的。(其實(shí)對(duì)于安裝來說,個(gè)人更推薦用git的方式,具體方式參考怎么用cocos2d 2.0實(shí)現(xiàn)精靈的遮罩這篇文章,我就不再贅述了)。
開始
好了,如果你已經(jīng)安裝好了cocos2d 2.0的話,我們就可以開始了。打開xcode,選擇new project,選擇我們新添加的cocos2d 2.x模板,在右側(cè)的面板里選擇cocos2d ios

點(diǎn)擊下一步,把我們的項(xiàng)目命名為:MoveMask,然后選擇一個(gè)地方保存你的項(xiàng)目,最后點(diǎn)創(chuàng)建。
接下來,我們要使用ARC(自動(dòng)引用記數(shù))功能,使用ARC可以讓我們不用過多的操心內(nèi)存管理的問題,并且基本上不會(huì)出現(xiàn)內(nèi)存泄漏等我們程序員最頭疼的內(nèi)存問題。雖然cocos2d是不支持ARC功能的,但是實(shí)際上你是可以為你的cocos2d項(xiàng)目使用這個(gè)功能的:

在xcode的菜單欄選擇Edit\Refactor\Convert to Objective-C ARC,在打開的對(duì)話框中,確保展開MoveMask.app左邊的小三角

如上圖,把其它文件前面的勾都取消掉,只留下main.m,Appdelegate.m和HelloWorldLayer.m,點(diǎn)擊check,然后在后面的幾個(gè)對(duì)話框中都選擇確定,這其中你可能會(huì)被問到是否為你的代碼創(chuàng)建一個(gè)快照,你選創(chuàng)建和不創(chuàng)建都行,不過一般情況下還是創(chuàng)建一個(gè)比較好,這樣當(dāng)你的代碼在遇到不可逆轉(zhuǎn)的錯(cuò)誤的時(shí)候可以恢復(fù)。好了,現(xiàn)在你就可以在你的cocos2d程序里用ARC了,是不是很簡(jiǎn)單。
現(xiàn)在編譯運(yùn)行我們的項(xiàng)目,你會(huì)發(fā)現(xiàn)在一個(gè)錯(cuò)誤:(我后來又試了幾次,它又不出錯(cuò)了,呵呵,總之如果有你在試的時(shí)候有同樣的錯(cuò)誤的話,可以和我用同樣的方式解決它,如果沒出錯(cuò)更好,直接進(jìn)行后面的操作)

錯(cuò)誤提示告訴我們,我們的window這個(gè)屬性是一個(gè)需要被保持的對(duì)象,可是我們卻可能會(huì)釋放它,換句話說,我們的ARC系統(tǒng)沒有為我們做好這個(gè)window屬性的管理工作,它應(yīng)該是一個(gè)強(qiáng)引用的,可是我們卻沒有給它分配strong屬性。(我并不知道為什么造成了這個(gè)錯(cuò)誤,如果你知道,請(qǐng)留言告訴我,謝謝!),不過管它呢,我們來修正這個(gè)錯(cuò)誤,在appDelegate.h文件中,為我們的windown屬性加上strong聲明。好了,再次編譯我們的項(xiàng)目,如果一切正常的話,你會(huì)看到下面的畫面:

進(jìn)入正題
一切都準(zhǔn)備就緒了,我們來實(shí)現(xiàn)我們的功能吧。
哦,對(duì)不起,在我們還沒有寫我們的代碼之前,我需要確定一件事,你知道shader是什么嗎?shader(著色器)是一個(gè)用來執(zhí)行渲染效果的小程序。它是由圖形處理單元執(zhí)行的,在移動(dòng)設(shè)備上,有兩種著色器:
1.vertex shader,這個(gè)叫頂點(diǎn)著色器,當(dāng)每一個(gè)頂點(diǎn)被渲染的時(shí)候,都會(huì)調(diào)用這個(gè)著色器,所以當(dāng)我們渲染一個(gè)精靈的時(shí)候,由于我們的精靈都是四個(gè)頂點(diǎn)的,所以我們的頂點(diǎn)著色器會(huì)被調(diào)用四次,它被用來計(jì)算我們的精靈每一個(gè)頂點(diǎn)的顏色和一些其它屬性。
2.fragment shader,我們也可以叫它片源著色器(也可以說是像素著色器),這個(gè)著色器會(huì)在每個(gè)像素被渲染的時(shí)候被調(diào)用,也就是說,如果我們?cè)趇phone上顯示一個(gè)全屏的圖片的話,這個(gè)著色器會(huì)被執(zhí)行480*320次。
這兩個(gè)著色器不能單獨(dú)使用,它們必須成對(duì)的使用,一對(duì)兒頂點(diǎn)和片源著色器叫做一個(gè)著色程序,它們通常是按下面的方式工作的:
頂點(diǎn)著色器首先確定每一個(gè)顯示到屏幕上的頂點(diǎn)的屬性,然后這些頂點(diǎn)組成的區(qū)域被化分成一系列的像素,這些像素的每一個(gè)都會(huì)調(diào)用一次片源著色器,最后這些經(jīng)過處理的像素顯示在屏幕上。
讓我們先看一對(duì)簡(jiǎn)單的著色程序吧,首先是頂點(diǎn)著色器:
//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;
}
每一個(gè)著色程序都必需接受輸入數(shù)據(jù),并產(chǎn)生輸出數(shù)據(jù)。在這個(gè)頂點(diǎn)著色器里,我們有三個(gè)輸入數(shù)據(jù),一個(gè)是保存每個(gè)頂點(diǎn)的位置,一個(gè)是保存每個(gè)頂點(diǎn)對(duì)應(yīng)的紋理坐標(biāo),最后還有一個(gè)是用于整個(gè)精靈的位置、縮放、旋轉(zhuǎn)的矩陣,這些數(shù)據(jù)都會(huì)在這個(gè)著色器被調(diào)用之前被傳進(jìn)來。我們還有兩個(gè)輸出數(shù)據(jù)在這個(gè)著色器程序里,一個(gè)決定了每個(gè)頂點(diǎn)的最終屏幕坐標(biāo),一個(gè)決定了每個(gè)頂點(diǎn)的最終紋理坐標(biāo)。下面我們會(huì)講到,這些輸出數(shù)據(jù)是被片源著色器怎么使用的。
現(xiàn)在我們先來一點(diǎn)點(diǎn)地認(rèn)真看一下這個(gè)頂點(diǎn)著色器:
注釋1,我們定義這個(gè)著色器的輸入數(shù)據(jù),我們定義了兩個(gè)數(shù)據(jù)結(jié)構(gòu)變量。一個(gè)vec4類型,表明這個(gè)結(jié)構(gòu)有4個(gè)float型數(shù)據(jù)組成,它用來存儲(chǔ)頂點(diǎn)的位置。(你可能會(huì)奇怪,我們的2維世界的坐標(biāo)怎么要用4個(gè)float來存呢?兩個(gè)不就夠了嗎?這個(gè)牽扯的數(shù)學(xué)問題就比較復(fù)雜了,簡(jiǎn)單地說,是因?yàn)槲覀內(nèi)绻獙?duì)我們的精靈進(jìn)行縮放、旋轉(zhuǎn)、移動(dòng)等操作的話,從圖形學(xué)上來說的話,是要做矩陣的乘法的,保存我們轉(zhuǎn)換信息的這些矩陣都是4*4的,所以我們的位置也要是4位的才能進(jìn)行操作,具體4元數(shù)了什么的,那都太高級(jí)了,我也不清楚,如果你感興趣的話,可以自己查這方面的資料)另一個(gè)是vec2類型,表明它有兩個(gè)float型數(shù)據(jù)組成,它用來存儲(chǔ)我們的紋理坐標(biāo)。很重要的一點(diǎn)是,這個(gè)attribute關(guān)鍵字,它告訴編譯器,被這個(gè)關(guān)鍵字修飾的變量是一個(gè)輸入變量,這個(gè)變量的值要從每個(gè)頂點(diǎn)的數(shù)據(jù)結(jié)構(gòu)中獲得。(具體獲得的地方在后面的代碼中用到時(shí)我們會(huì)指出)
注釋2,當(dāng)你需要一些額外的數(shù)據(jù)傳入到著色器的話,你需要把你的變量用uniform關(guān)鍵字聲明。(注意uniform和attribute的區(qū)別,雖然都是輸入數(shù)據(jù),但是它們不同,attribute修飾的變量的值,是通過頂點(diǎn)數(shù)據(jù)結(jié)構(gòu)傳入的,而uniform修飾的變量的值,是我們?cè)诖a里傳入的)這里我們聲明了一個(gè)mat4類型,這是一個(gè)4*4的矩陣,如果你線性代數(shù)一竅不通的話,你只要記住這個(gè)矩陣是我們用來移動(dòng)、縮放和旋轉(zhuǎn)的就行了。(說到線性代數(shù)了我就不得不說下我的大學(xué)里的高數(shù)課了,在講線性代數(shù)的時(shí)候,我們的教育只告訴我們這些知識(shí)的用法,就是只告訴你怎么進(jìn)行運(yùn)算,完全不告訴你它能干什么,所以學(xué)的時(shí)候,覺得這是什么玩意兒呀,有什么用呀,根本就不知道它能干什么,我怎么可能會(huì)去好好學(xué)它?!結(jié)果我的線性代數(shù)在考試的時(shí)候差點(diǎn)就掛科了,而且,我現(xiàn)在完全不記得它的用法了……。如果我在學(xué)的時(shí)候就能知道它是干什么的,結(jié)合它的實(shí)際用途去學(xué)習(xí)理解,我相信我會(huì)學(xué)好它的,不過貌似這是我們的教育的通病……)
注釋3,這頂點(diǎn)著色器需要輸出一些數(shù)據(jù)到片源著色器,這樣它們才能協(xié)同操作,要實(shí)現(xiàn)輸出數(shù)據(jù)到片源著色器,你需要用varying關(guān)鍵字來聲明你的變量,這個(gè)最酷的事情是,varying變量的值是插值的,就是說如果,頂點(diǎn)A的坐標(biāo)是定義成varying的,并設(shè)置值為0,頂點(diǎn)B坐標(biāo)也是varying的,并被設(shè)置值為1的話,在片源著色器里,會(huì)自動(dòng)把頂點(diǎn)A和頂點(diǎn)B中間的那個(gè)像素的坐標(biāo)設(shè)置為0.5,每一個(gè)片源都是從頂點(diǎn)數(shù)據(jù)插值計(jì)算得到的。我們還看到在條件編譯里有一個(gè)mediump 前綴,這個(gè)是用來決定計(jì)算的精度的,可用的前綴還有highp和lowp.它們分別代表高、中、低三個(gè)級(jí)別的精度,精度越高,數(shù)據(jù)越精確,不過計(jì)算起來也會(huì)更慢。
注釋4,每個(gè)著色器都要有一個(gè)main函數(shù)
注釋5,gl_Position是頂點(diǎn)著色器的內(nèi)置變量,決定每一個(gè)頂點(diǎn)的最終位置,這個(gè)著色器里,是把這個(gè)輸入的頂點(diǎn)坐標(biāo)a_psoition乘以這個(gè)輸入的變形矩陣u_MVPMatrix(這個(gè)矩陣是由cocos2d框架自動(dòng)傳進(jìn)來的,我們不用操心這個(gè),它決定了精靈的移動(dòng)、縮放和旋轉(zhuǎn)) 。
注釋6,我們把輸入的紋理坐標(biāo)a_texCoord不做任何改變賦值給varying變量V_texCoord,以便傳遞給片源著色器。
下面我們接著看和這個(gè)頂點(diǎn)著色器配合工作的片源著色器的代碼:
//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,我們強(qiáng)制設(shè)置在這個(gè)片源著色器里對(duì)float數(shù)的計(jì)算精度為中等精度。
注釋2,任何在頂點(diǎn)著色器里的輸出變量在片源著色器里都要被定義為輸入變量,所以在這里再次定義這個(gè)紋理坐標(biāo)變量
注釋3,片源著色器也可以有自己的uniform變量,這種變量是從代碼里傳過來的常量,cocos2d會(huì)傳入紋理到一個(gè)uniform變量,所以這里定義了一個(gè)用uniform聲明的sampler2D類型的變量,它只是一個(gè)正常的紋理。
注釋4,像頂點(diǎn)著色器的gl_Position一樣,這個(gè)gl_FragColor也是一個(gè)內(nèi)置變量,它決定了每一個(gè)像素的最終的顏色。這里只是通過頂點(diǎn)著色器得到的紋理坐標(biāo),獲得紋理的相應(yīng)坐標(biāo)的正常顏色。
如果你還想了解它們是怎么一起工作的話,這個(gè)著色器在CCGrid.m里被調(diào)用,你可以打開這個(gè)文件去看看,在初始化方法里有下面代碼:
self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];
這句代碼從著色器緩存中加載這個(gè)著色程序。如果你好奇,你可以看看CCShaderCache文件,看看這個(gè)著色器是怎么編譯和存儲(chǔ)的。然后再blit方法里,它傳遞變量到shader,并運(yùn)行這個(gè)著色程序。
-(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,這個(gè)是特定于這個(gè)CCGrid.m類的,計(jì)算網(wǎng)格數(shù)的,我們不討論它
注釋2,這個(gè)是啟用頂點(diǎn)著色器的輸入變量的,這時(shí)我們啟用了兩個(gè)kCCVertexAttribFlag_Position和kCCVertexAttribFlag_TexCoords,實(shí)際上就是我們的頂點(diǎn)著色器里的a_Position和a_texCoord。這樣我們能才使用這兩個(gè)輸入變量。
注釋3,是告訴cocos2d我們要使用我們剛才在init方法里面加載的著色程序。
注釋4,告訴cocos2d傳入我們的CCNode的移動(dòng)、縮放、旋轉(zhuǎn)矩陣給shader,實(shí)際上就是為我們的頂點(diǎn)著色器里的輸入變量u_MVPMatrix賦值。
注釋5,傳遞頂點(diǎn)數(shù)據(jù)結(jié)構(gòu)的位置數(shù)據(jù)給頂點(diǎn)著色器,實(shí)際上就是為頂點(diǎn)著色器里的輸入變量a_Posigion賦值。
注釋6,和注釋5差不多,它是給頂點(diǎn)著色器里的a_texCoord賦值的
注釋7,它是真正的openGL_ES繪制方法,把我們的精靈進(jìn)行繪制。這個(gè)方法的第一個(gè)參數(shù)是GL_TRIANGLES表明,我們要繪制的圖元是三角形,第二個(gè)參數(shù)是要繪制的圖元的數(shù)量乘以每個(gè)圖元的頂點(diǎn)數(shù),這里是6表明,我們要繪制2個(gè)三角形,每個(gè)三角形有3個(gè)頂點(diǎn),很顯然兩個(gè)三角形可以合成一個(gè)矩形,這個(gè)矩形就是我們的精靈,第三個(gè)參數(shù)是頂點(diǎn)索引數(shù)據(jù)的類型,最后一個(gè)參數(shù)就是指向索引存儲(chǔ)位置的指針。
注釋8,這個(gè)我們不討論了。(因?yàn)槲乙膊恢浪歉墒裁吹模孟袷潜3诌f增式的繪圖的,我個(gè)人猜測(cè)有可能跟精靈的zOrder屬性有關(guān),我沒有看這它的代碼,對(duì)它的看法都是瞎猜的,不過這個(gè)真的不影響我們使用,所以我才沒看它的代碼的,只要保持這句話是這個(gè)樣子就行了哈哈)
還沒有開始真正的寫我們的代碼呢,就先灌輸了這么一大堆東西給你,真的不好意思,不過這個(gè)對(duì)于理解我們的這個(gè)項(xiàng)目來說是真的很有必要的,所以我才對(duì)這些知識(shí)進(jìn)行了解釋,其實(shí)這些知識(shí)點(diǎn)的內(nèi)容基本上就是直接譯自raywenderlich博客團(tuán)隊(duì)的這篇:How To Create Cool Effects with Custom Shaders in OpenGL ES 2.0 and Cocos2D 2.X了,我本來想以自己的觀點(diǎn)表達(dá)這些知識(shí)內(nèi)容的,可是,原文表達(dá)的實(shí)在是太好了,所以不自覺的基本上就變成這個(gè)原文的譯文了呵呵
好了,讓我們干自己的活兒吧
現(xiàn)在你已經(jīng)具備了編寫sahder的能力,那我可以說你擁有了你對(duì)你自己的精靈的完全的控制權(quán),你想呀,你的精靈的每一個(gè)像素你都能控制,還有什么是你不能做的效果呢?(當(dāng)然說著簡(jiǎn)單,真正好的效果是需要扎實(shí)的圖形學(xué)知識(shí)的)
下面我們創(chuàng)建一個(gè)ccsprite類的子類,命名為MaskedSprite.
選擇File\New\New File,然后選擇iOS\Cocoa Touch\Objective-C class,再點(diǎn)擊Next,然后輸入CCSprite為subclass,接著,點(diǎn)Next,把新的文件命名為MaskedSprite.m,最后點(diǎn)擊Save。
然后把MaskedSprite.h替換成下面的內(nèi)容:
#import "cocos2d.h"
@interface MaskedSprite : CCSprite {
CCTexture2D * _maskTexture;
GLuint _maskLocation;
CGPoint offsetWithPosition;
GLuint _offsetLocation;
}
-(void) postOffsetToShader:(CGPoint)aOffset;
@end
這里我們定義了一個(gè)變量_maskTexture來跟蹤我們的遮罩紋理,一個(gè)變量_maskLocation來追蹤遮罩紋理的uniform位置(稍后會(huì)在我們的著色器中看到),一個(gè)變量offsetWithPosition來存儲(chǔ)我們的坐標(biāo)的偏差,最后一個(gè)變量_offsetLocation來跟蹤v_offset(稍后會(huì)在我們的著色器中看到)的uniform位置 ,然后我們聲明了一個(gè)方法來給我們的offsetWithPosition賦值。
下面我們打開MaskedSprite.m,并替換它的內(nèi)容如下:
#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
我們重寫這個(gè)initWithFile:方法
注釋1,我們加載我們的遮罩紋理(這里我們的遮罩紋理是一個(gè)480*320的圖片,但是它只有中間一個(gè)小圓是有顏色的,其它地方都是透明的),并且取消這個(gè)紋理的線性插值,我們要用它的真實(shí)顏色。
注釋2,我們先加載我們自己的片源著色器(稍后會(huì)展示),然后用我們的片源著色器和一個(gè)cocos2d自帶的頂點(diǎn)著色器,合成一個(gè)著色程序賦給我們的shaderProgram屬性,這個(gè)屬性指示我們的著色程序
注釋3,和前面講CCGrid類用的著色程序時(shí)的一樣,這里只啟用頂點(diǎn)著色器的attribute變量的,這里是坐標(biāo),紋理坐標(biāo),和顏色三個(gè)。
注釋4,是對(duì)我們的這個(gè)自定義著色程序進(jìn)行鏈接。
注釋5,這個(gè)是cocos2d幫我們?cè)O(shè)置我們的移動(dòng)、縮放、旋轉(zhuǎn)的矩陣的
注釋6,我們得到我們自己寫的片源著色器里的兩個(gè)uniform變量的位置,一個(gè)是U_mask,一個(gè)是V_offset,這樣我們就可以在我們的代碼里通過這兩個(gè)位置為我們的這兩個(gè)變量賦值了。
注釋7,為我們的offsetWithPosition賦一個(gè)初值。
下面我們?cè)搶?shí)現(xiàn)我們的postOffsetToShader:方法了,在initWithFile:方法下面添加如下實(shí)現(xiàn):
-(void) postOffsetToShader:(CGPoint)aOffset
{
offsetWithPosition = aOffset;
}
呵呵,就一句話,給我們的offsetWithPosition賦值為我們傳過來的參數(shù)aOffset,實(shí)在沒什么可說的這個(gè)方法,就是我們用來隨時(shí)改變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
}
如果你自己寫這個(gè)darw方法的時(shí)候,如果不知道該寫些什么,我建議你直接去ccsprite.m里copy它的draw方法出來,然后再根據(jù)你自己的需要進(jìn)行改動(dòng),我就是這樣做的哈哈。
draw方法里的代碼好多,有的我們已經(jīng)在前面講CCGrid的著色器時(shí)講到了,有的沒講,這些沒講到的我們也不用深究,對(duì)于我們自己定制這個(gè)draw方法來說,也就是這四個(gè)注釋值得說一下:
注釋1,這是一個(gè)紋理綁定動(dòng)作,綁定我們的精靈紋理到紋理槽0,這里其實(shí)我們是偷了巧的,我們只寫了一句話,其實(shí)它和注釋2里的代碼是同樣的效果,在默認(rèn)情況下,是紋理槽0處于激活狀態(tài),我們直接進(jìn)行綁定操作,就是將我們的紋理綁定到當(dāng)前激活的紋理槽上,所以是綁到紋理槽0上了,在我們自己的片源著色器里有u_texture,它是指向紋理槽的,而默認(rèn)情況下它是0,這樣我們的u_texture就代表紋理槽0內(nèi)的紋理,也就是我們的精靈紋理,所以我們這樣一句代碼其實(shí)完成了三句代碼的功能。
注釋2,這是注釋1的完整版代碼,不同時(shí)的是,它是把我們的遮罩紋理綁定到紋理槽1了,并通過glUniform1i(_maskLocation, 1)這個(gè)方法來把1傳遞給我們的著色器里的U_mask,使其指向紋理槽1。這樣u_mask就指向我們的遮罩紋理了。glUniform**()這個(gè)方法,是向第一個(gè)參數(shù)指向的位置(這個(gè)位置在前面我們已經(jīng)獲得了)代表的uniform變量賦值,方法時(shí)的數(shù)字?jǐn)?shù)代表一次要傳遞幾個(gè)數(shù)值,這時(shí)是1,代表我們要傳遞一個(gè)數(shù)值;數(shù)字后邊的i代表我們要傳的是整數(shù),如果是f的話,就代表我們要傳的是float數(shù)。
注釋3,我們?yōu)槲覀兊闹骼锏膙_offset賦值,它是一個(gè)vec2類型的變量,所以我們是傳兩個(gè)float給我們的v_offset方法。
注釋4,這個(gè)注釋在draw方法的最后一行,千萬不要漏掉。這是我們重新激活紋理槽0為當(dāng)前活動(dòng)紋理槽,因?yàn)槲覀冎霸谧⑨?的時(shí)候激活了紋理槽1,所以還原這個(gè)當(dāng)前活動(dòng)的紋理槽是很重要的。
事實(shí)上這時(shí)候我們的這個(gè)MaskedSprite類已經(jīng)完成了,只要實(shí)現(xiàn)我們自己的片源著色器就能用了。
現(xiàn)在,我們來寫一個(gè)我們自己的片源著色器吧
回到Xcode,選擇File\New\New file,再選擇 iOS\Other\Empty,點(diǎn)擊Next。然后命名為Mask.fsh并點(diǎn)擊Save。然后我們的工程文件,MoveMask target,然后選擇build phase標(biāo)簽,然后把我們的Mask.fsh從compile source里拖到copy bundle source里,這樣它就會(huì)變成資源文件,而不是和我們的代碼一起編譯了。

然后把Mask.fsh的代碼替換成下面的樣子,然后我們會(huì)一步步講解我們的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
}
這段代碼的最前面是我們?cè)O(shè)置這個(gè)段著色器對(duì)于float數(shù)據(jù)計(jì)算的精度為低。
注釋1,這是兩個(gè)從頂點(diǎn)著色器傳過來的兩個(gè)變量,一個(gè)是每個(gè)片源的顏色,一個(gè)是每個(gè)片源的紋理坐標(biāo)
注釋2,這是我們定義了兩個(gè)紋理,它是被聲明為uniform的,所以我們需要在代碼里傳入這兩個(gè)紋理。一個(gè)是我們的精靈的正常的紋理,一個(gè)是我們的精靈的遮罩的紋理。
注釋3,我們定義了一個(gè)vec2變量v_offset,用來存儲(chǔ)我們的精靈的紋理和我們的遮罩的紋理之間的位置偏差(我們就是通過這個(gè)偏差的變化來實(shí)現(xiàn)遮罩的移動(dòng)的)。同樣的,我們也把它聲明為uniform的,這樣,它也需要我們?cè)诖a里來給它賦值。
注釋4,定義了一個(gè)vec2類,它用來存儲(chǔ)一個(gè)像素的寬和高,這樣我們?cè)谝苿?dòng)的時(shí)候就可以通過,移動(dòng)n倍的這個(gè)變量,來達(dá)到移動(dòng)多少個(gè)像素的目的。
注釋5,我們先把由頂點(diǎn)著色器傳進(jìn)來的紋理坐標(biāo)賦給一個(gè)新的vec2變量,然后通過一系列的計(jì)算,得到在v_offset這個(gè)偏差存在的情況下,一個(gè)我們的精靈的紋理坐標(biāo)的位置,對(duì)應(yīng)的精靈的遮罩的紋理坐標(biāo)。
我們來詳細(xì)地說一下這個(gè)計(jì)算。首先明確一個(gè)事情,我們的精靈的紋理和我們的精靈的遮罩的紋理是一樣大的,在本例中,它們都是480*320大小的,我們就拿兩張大小一樣的撲克牌來想像一下具體的情況,下面的牌相當(dāng)于精靈紋理,上面的相當(dāng)于遮罩紋理,這樣,在v_offset這個(gè)變量是(0,0)的時(shí)候,就相當(dāng)于是這兩張牌完全合在一起,上面的蓋著下面的,下面的完全看不到。當(dāng)你把這兩張牌的上面一張移動(dòng)了一定的距離使 這兩張牌疊在一起,但不完全重合的時(shí)候,我們可以看到,在這兩張牌疊在一起的部分,現(xiàn)在的下面的牌的紋理坐標(biāo)的位置,和疊在它上面的牌的紋理坐標(biāo)的位置不再是一樣的了,它們之間有了偏差。而這個(gè)偏差的值就是我們上面的牌移動(dòng)的距離,其實(shí)也就是我們的v_offset此刻的值,圖片應(yīng)該比文字更能說明問題:

這個(gè)圖片代表兩個(gè)紋理完全重合的時(shí)候,可以看到它們重合的位置紋理坐標(biāo)是一致的,再看它們不重合的時(shí)候:

圖中我用藍(lán)色框代表精靈紋理,綠色框代表遮罩紋理,圖中的黃色區(qū)域就是在有了偏移后,兩個(gè)紋理的重合部分,在圖中我們可以清楚的看到,在重合的部分,現(xiàn)在的精靈的紋理坐標(biāo)(0,0)對(duì)應(yīng)的位置,不再是遮罩紋理的(0,0)坐標(biāo),而是圖中兩個(gè)紅線的交叉點(diǎn),遮罩紋理的大概(0.3,0.2)坐標(biāo)的樣子,這個(gè)變化的數(shù)就是我們的偏差值,也就是我們的v_offset的值。因此精靈紋理的紋理坐標(biāo),加上這個(gè)V_offset就可以得到在它的紋理坐標(biāo)位置上相應(yīng)的遮罩紋理的紋理坐標(biāo)。好的,知道了這個(gè)v_offset是干什么的,我們就可以很簡(jiǎn)單的看明白這個(gè)計(jì)算做了什么操作了。因?yàn)檫@個(gè)v_offset的值是計(jì)算的坐標(biāo)的差(下面的代碼中會(huì)有介紹),因些在我們的著色器里,要得到紋理坐標(biāo)的偏移量,我們需要用我們傳進(jìn)來的v_offset.x乘以一個(gè)像素的寬(即onePixel .x)來得到精靈紋理坐標(biāo)與遮罩紋理坐標(biāo)在x方向上的偏移量,同樣的v_offset.y*onePixel.y就可以得到在y方向上的偏移量了。
那么有這個(gè)偏移量,我們應(yīng)該怎么得到這個(gè)偏移后的遮罩紋理的紋理坐標(biāo)呢?這個(gè)其實(shí)是要具體情況具體分析的,(你計(jì)算偏移的方式不同,這里計(jì)算的方式也會(huì)不同,你計(jì)算偏移的時(shí)候的減數(shù)和被減數(shù)的位置會(huì)決定你這里是應(yīng)該用加法還是減法,你后面會(huì)看到我們計(jì)算偏移的方式是用我們的觸摸點(diǎn)的位置減去我們的精靈的位置的),這里我們用減法,因?yàn)槿绻覀兊挠|摸點(diǎn)和在精靈的位置的左邊的話,情況就和上圖中的情況差不多,這時(shí)候,按我在后面計(jì)算偏移的方式的話,我得到的偏移量應(yīng)該是負(fù)的,這而這時(shí)候我們看圖片就能看出來,精靈紋理的紋理坐標(biāo)位置對(duì)應(yīng)的偏移后的遮罩紋理的紋理坐標(biāo)應(yīng)該是增大了,所以這里我們要用減法,所以(texCoord.x - onePixel.x*v_offset.x),這個(gè)式子得到偏移后的精靈紋理的紋理坐標(biāo)位置對(duì)應(yīng)的偏移后的遮罩紋理的紋理坐標(biāo)的x值,注意,這里有一個(gè)大陷井,不要覺得x是用減法了,那么求偏移后的y也應(yīng)該用減法,不要忘了一件事情,iPhone中用于Core Graphics的圖像坐標(biāo)系統(tǒng)和我們用的opengl_es的坐標(biāo)系統(tǒng)的y軸方向是相反的,opengl_es是從下向上增大的,而core graphics上從下向上減小的,所以我們得到的y的偏移量其實(shí)是相反的,所以這里得到偏移的y,我們需要用加法,(texCoord.y + onePixel.y*v_offset.y),這樣這個(gè)式子就得到了實(shí)際的偏移后相對(duì)應(yīng)的遮罩紋理的紋理坐標(biāo)。
然后我們把這個(gè)求得的坐標(biāo)賦值給finCoord 。
注釋6,我們根據(jù)我們的精靈紋理的紋理坐標(biāo),和偏移的的遮罩紋理的紋理坐標(biāo),得到屏幕上同一點(diǎn)對(duì)應(yīng)的,這兩個(gè)不同紋理的的具體的顏色,分別賦值給texcolor和maskcolor。
注釋7,我們用我們的精靈紋理的rgb顏色做為一個(gè)新的顏色的rgb值,但是我們用我們剛剛得到的兩個(gè)texcolor和maskcolor的透明度值相乘做為這個(gè)新的顏色的透明度值,這樣主是在同一點(diǎn)上,如果遮罩紋理上的透明度為0的話,這個(gè)新的顏色的透明度就是0,那么在這個(gè)點(diǎn)上這個(gè)顏色就不可見了,如果遮罩上不是零的話,而那么相乘的透明度就不是零,那么這個(gè)顏色是可見的。
注釋8,我們用注釋7得到的顏色乘以我們從頂點(diǎn)著色器得到的顏色,賦給我們的最終每個(gè)像素的顏色。
哦,好累呀,這一段東西實(shí)在是太繞口了,我自己都感覺說不清楚了,還是對(duì)著圖片多想想的話容易明白吧。吼吼……
希望就在前方
最后,我們應(yīng)該使用我們自己的這個(gè)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;
}
很簡(jiǎn)單的,我們初始化我們自己的MaskedSprite類的一個(gè)對(duì)象,把它加入到屏幕中央的位置,然后加入到我們的層,最后我們啟用這個(gè)層的isTouchEnabled方法,這樣我們就可以讓這個(gè)層響應(yīng)我們的觸摸事件了。
在init方法下面實(shí)現(xiàn)下面方法:
- (void)registerWithTouchDispatcher {
[[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self
priority:0 swallowsTouches:YES];
}
這個(gè)方法重新注冊(cè)我們對(duì)touch事件的響應(yīng)方式,默認(rèn)的話cclayer是響應(yīng)standard touch delegate的,現(xiàn)在我們讓它響應(yīng)targeted touch delegate。(兩種不同的響應(yīng)方式請(qǐng)參考CCTouchDelegateProtocol),priority是響應(yīng)的優(yōu)先級(jí),這是高為0,swallowsTouches設(shè)為Yes,這樣只我們的touch處理方法響應(yīng)并處理了這個(gè)touch事件,這個(gè)touch事件就不再繼續(xù)分發(fā)了。
下面主就是實(shí)現(xiàn)我們的touch響應(yīng)方法了:
- (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方法中,我們得到我們的觸摸點(diǎn)在layer中的坐標(biāo),用這個(gè)坐標(biāo)減去我們的MaskedSprite的實(shí)際位置,這樣就得到了觸摸點(diǎn)和我們的精靈的位置這間的偏差,把這個(gè)偏差傳遞給我們的精靈的postOffsetToShader:方法,這樣這個(gè)偏差值就傳給了我們的精靈了,它把這個(gè)值賦給精靈的offsetWithPositon變量,而這個(gè)變量又會(huì)在我們的精靈的draw方法里通過注釋3的那句代碼傳遞給,我們自己寫的片源著色器里的被uniform聲明的v_offset變量。然后經(jīng)過我們的著色器的計(jì)算,就得到了我們的移動(dòng)的遮罩的效果。
我們返回YES,表明,只要接收到touch事件,我們就處理。
在touchMoved方法中,我們重復(fù)在began方法里的計(jì)算
最后,在touchend方法里,我們傳遞給我們的精靈一個(gè)極大的負(fù)坐標(biāo),這個(gè)值和在我們的MaskedSprite類的init方法里,給offsetWithPosition賦的值是一樣的,這樣做的目的是,在沒有touch事件的時(shí)候,讓我們的遮罩紋理和精靈紋理完全不重疊,這樣精靈紋理所處的位置的遮罩紋理的透明度值肯定是0,這樣在沒有觸摸的情況下,我們的精靈就不可見了。可是只要你在屏幕點(diǎn)一觸摸你就會(huì)發(fā)現(xiàn)在,有一個(gè)經(jīng)過遮罩處理的精靈顯示在你的觸摸的位置,你移動(dòng)你的手指,這個(gè)遮罩的位置也隨著你移動(dòng)。

利用這個(gè)功能我們能做什么
這個(gè)功能可能會(huì)在塔防類的游戲里會(huì)有點(diǎn)用,就像本例中的一樣,如果在我們的HelloWorldLayer里先加一個(gè)背景,然后再加一個(gè)我們的MaskerSprite精靈的話,這個(gè)就可以實(shí)現(xiàn),動(dòng)態(tài)地只有在觸摸發(fā)生的情況下才在我們的游戲背景上顯示一個(gè)只在自己的遮罩范圍(遮罩應(yīng)該是一個(gè)塔的攻擊范圍的圖片)內(nèi)可見的一個(gè)位置網(wǎng)格是不是很不錯(cuò)?像這樣:

如果你有多個(gè)塔,每個(gè)塔的攻擊范圍是不一樣的,你只需要在我們的MaskedSprite類里多加幾個(gè)遮罩紋理就好了,在拖動(dòng)不同的塔的時(shí)候,根據(jù)塔的類型來綁定不同的遮罩紋理到紋理槽1,就可以了吧似乎,具體行不行,你試試吧呵呵,我也不確定能不能行呵呵。
這個(gè)demo雖然笨拙,但是它還是能工作的,如果你有更好的實(shí)現(xiàn)的方法,留言告訴我,我會(huì)好好學(xué)習(xí)的,謝謝。能力有限,文中可能會(huì)用不對(duì)的地方,希望大家指教。謝謝!!
參考文章:
dingwenjie博客 http://www.cnblogs.com/dingwenjie/archive/2012/04/02/2429576.html
子龍山人博客http://www.cnblogs.com/andyque/archive/2011/09/16/2155068.html