Shaders(着色器) 隨着3D遊戲的發展會有一個很大的進步。它允許程序員創建新的特效,並決定如何顯示在屏幕上。如果你還沒有用過shader,那麼閱讀本教程之後,你就會了。
Cocos2D是現在最好的IOS遊戲開發框架之一,很幸運,新版本的Cocos2D 2.X支持OpenGL-ES 2.0 和 shaders。在本教程中,你會在cocos2D的幫助下學會如何創建和使用shaders。
你將會學到:
- GLSL的基礎語言
- 在cocos2D中如何創建和使用自定義的shaders
- 三個shader例子:
- 如何使用ramp textures(從白到黑的漸進紋理)來修改遊戲中的顏色
- 如何創建浮雕效果
- 如何創建草的隨風擺動效果
讓我們開始
在研究shaders和如何使用他們之前,你必須先下載並安裝。然後創建一個cocos2D的工程。步驟如下:
2、解壓
3、打開終端,進入到cocos2D的文件夾中,執行以下命令來安裝模板:./install-templates.sh -u -f
4、打開Xcode,創建cocos2D工程,iOS\cocos2d v2.x\cocos2d iOS
5、將你的工程命名爲CocosShaderEffects,選擇iphone設備
6、選擇一個路徑來保存你的工程,然後創建它。
接下來,你需要在工程中啓用ARC。儘管cocos2D本身沒有使用ARC,但是你可以在其他的代碼中啓用,這樣你可以少些一些代碼並減少內存泄露的可能。
在菜單中選擇Edit\Refactor\Convert to Objective-C ARC…。在打開的對話框中,選擇main.m, AppDelegate.m and HelloWorldLayer.m,並且點擊Check,Next和Save來完成操作,如圖
編譯運行,你可以看到如圖:
現在,下載程序的資源,解壓後拖到工程的根目錄下,然後加到工程中
工程默認沒有啓動高清,如果你想啓用的話,打開AppDelegate.m,註釋掉下面一行
if( ! [director_ enableRetinaDisplay:YES])
現在一切都搞定了,讓我們開始shader和cocos2D的探索之旅吧
什麼是Shaders?
shader就是一個簡單的類似C的程序,它用來執行渲染特效。像它的名字暗示的那樣,一個簡單的shader函數就是添加一個不同的遮蓋色到物體或者物體的局部。Shader是在GPU中執行的。對於移動設備來說,有兩種類型的Shaders:
1、Vertex shader頂點Shader:用來渲染每一個頂點,當渲染一個簡單的精靈時,它通常會被執行4次來計算4個頂點的顏色和其他屬性
2、Fragment shader片段Shader:用來渲染顯示在屏幕上的每一個像素。這意味着,要渲染iphone的真個屏幕,一個片段Shader將要執行320*480次
頂點和片段Shaders不能單獨使用,他們必須成對使用。一對Shaders叫做一個program。它一般這樣工作:
1、頂點Shader首先爲每個要顯示在屏幕上的頂點定義屬性
2、然後,片段Shader將每個頂點細分到每一個像素3、最終的像素被渲染到屏幕上
coco2D中內置的Shaders是如何工作的?
每一個CCNode都有一個shaderProgram實例變量,另外,cocos2D還有一個CCShaderCache類,允許你使用默認的shader程序或者緩存自定義的shader程序,這樣你就不用多次加載了。常用的一些shader方法都在libs\cocos2D\CCGLProgram.h中,默認的shader在libs\cocos2d\ccShader_xxx.h這類文件中。
打開工程,選擇其中一個文件ccShader_PositionTexture_vert.h,這是一個頂點shader。不過被存儲爲字符串類型,(爲了快速的加載),但是我們爲了可讀性,我們還是將代碼列出來。
//作爲頂點Shader的輸入,a_positon是一個4維向量代表每個頂點的位置,a_texCoord是一個2維向量代表每個頂點的紋理座標(被映射到圖片的4個角),
//該值由cocos2d傳入
attribute vec4 a_position;
attribute vec2 a_texCoord;
//最爲全局常量的輸入,這是一個4*4的矩陣,用來對所有的精靈進行位置平移、縮放、旋轉,該值由cocos2d傳入
uniform mat4 u_MVPMatrix;
//決定採用什麼精度的浮點數,varying代表該值由頂點Shader傳給片段Shader,而且該值是自動插值的,舉個形象的例子來說吧,如果頂點A的紋理座標爲0,B點的紋理座標爲1,那麼AB中點的紋理座標就是0.5
#ifdef GL_ES
varying mediumpvec2 v_texCoord;
#else
varying vec2 v_texCoord;
#endif
再來看下片段Shader :ccShader_PositionTexture_frag.h//每個shader都用一個main函數,這個是入口函數
void main()
{
//gl_Positon是內置的輸入變量,將各個頂點經過投影變換之後傳給片段Shader
gl_Position = u_MVPMatrix* a_position;
//將輸入的紋理座標傳給片段Shader
v_texCoord = a_texCoord;
}
//設置中等精度的浮點數,精度越高,速度越慢
#ifdef GL_ES
precision mediumpfloat;
#endif
//接收從頂點Shader傳入的紋理座標
varying vec2 v_texCoord;
//紋理常量,從cocos2d程序中傳入
uniform sampler2D u_texture;
想要理解一切是如何發生的,可以通過CCGrid.m,來找出來是怎麼來用shader的void main()
{
//gl_FragColor內置輸出,從u_texture紋理中提取座標爲v_texCoord的顏色作爲輸出顏色
gl_FragColor = texture2D(u_texture, v_texCoord);
}
首先,在初始化函數init中,shader被加載到cache中
self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];
如果你很好奇,可以進到CCShaderCache類中來觀察shader是如何編譯和保存的。接下來,在blit函數中,傳值給shader並運行它:
你可能想要知道圖片是在哪裏傳入的。在afterDraw中-(void)blit
{
NSInteger n = gridSize_.x * gridSize_.y;
// 開啓屬性 the vertex shader's "input variables" (attributes)
ccGLEnableVertexAttribs( kCCVertexAttribFlag_Position | kCCVertexAttribFlag_TexCoords);
// Tell Cocos2D to use the shader we loaded earlier,加載shader
[shaderProgram_ use];
// Tell Cocos2D to pass the CCNode's position/scale/rotation matrix to the shader,設置投影矩陣
[shaderProgram_ setUniformForModelViewProjectionMatrix];
// Pass vertex positions,傳入頂點座標
glVertexAttribPointer(kCCVertexAttrib_Position,3, GL_FLOAT, GL_FALSE, 0, vertices);
// Pass texture coordinates,傳入紋理座標
glVertexAttribPointer(kCCVertexAttrib_TexCoords,2, GL_FLOAT, GL_FALSE, 0, texCoordinates);
// 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);
// Just stat keeping here
CC_INCREMENT_GL_DRAWS(1);
}
ccGLBindTexture2D( texture_.name );
現在你已經看到了一個shader使用的全過程,讓我們來創建自己的shader吧
如何創建&使用自己的Shader
大多數2D遊戲由各種各樣的精靈組成,這些精靈大多有4個頂點,這部分沒有什麼好處理的,所有的2D特效部分都在片段shader中完成。
你可以使用一個cocos2D默認的頂點shader,然後用一個自己創建的片段shader。
我們的目標是創建一個次級紋理來修改原始的紋理顏色,你可以用”ramp“紋理手動設置原始紋理的顏色。這個特效可以用來創建遊戲皮膚或者做卡通化
這是我們要用的次級紋理,從左到右,顏色從白(1,1,1)到黑(0,0,0);在工程中創建一個繼承與CCLayer的類CSEColorRamp,打開
HelloWorldLayer.m,導入CSEColorRamp.h,
#import "CSEColorRamp.h"
然後用以下代碼替換init函數:
-(id) init {
if( (self=[super init])) {
// 1 - create and initialize a Label
CCLabelTTF *label = [CCLabelTTF labelWithString:@"Hello World" fontName:@"Marker Felt" fontSize:64];
// 2 - ask director the the window size
CGSize size = [[CCDirector sharedDirector] winSize];
// 3 - position the label on the center of the screen
label.position = ccp( size.width /2 , size.height/2 );
// 4 - add the label as a child to this Layer
[self addChild: label];
// 5 - Default font size will be 28 points.
[CCMenuItemFont setFontSize:28];
// 6 - color ramp Menu Item using blocks
CCMenuItem *itemColorRamp = [CCMenuItemFont itemWithString:@"Color Ramp" block:^(id sender) {
CCScene *scene = [CCScene node];
[scene addChild: [CSEColorRamp node]];
[[CCDirector sharedDirector] pushScene:scene];
}];
// 7 - Create menu
CCMenu *menu = [CCMenu menuWithItems:itemColorRamp, nil];
// 8 - Configure menu
[menu alignItemsHorizontallyWithPadding:20];
[menu setPosition:ccp( size.width/2, size.height/2 - 50)];
// 9 - Add the menu to the layer
[self addChild:menu];
}
return self;
}
打開CSEColorRamp.m,用以下代碼替換@implementation CSEColorRamp這一行
接下來,初始化你的變量:@implementation CSEColorRamp {
CCSprite *sprite; //原始的精靈圖片
int colorRampUniformLocation; //要傳給shader值,需要指定一個索引給他
CCTexture2D *colorRampTexture; //用來渲染的紋理
}
|
#ifdef GL_ES
precision mediump float;
#endif
// uniform從程序中傳入,varying從頂點shader中傳入
varying vec2 v_texCoord;
uniform sampler2D u_texture;//精靈圖片
uniform sampler2D u_colorRampTexture;//渲染圖片
void main()
{ // 從精靈中獲得顏色
vec3 normalColor = texture2D(u_texture, v_texCoord).rgb;
// vec3 finalColor = vec3(1.0)-normalColor;
//使用真實顏色的各個通道的值作爲地址,從ramp獲取修改後的顏色值
//渲染圖片從左到右,顏色爲從1->0,所以得到的顏色值剛好相反
float rampedR = texture2D(u_colorRampTexture, vec2(normalColor.r, 0)).r;
float rampedG = texture2D(u_colorRampTexture, vec2(normalColor.g, 0)).g;
float rampedB = texture2D(u_colorRampTexture, vec2(normalColor.b, 0)).b;
//計算輸出的顏色
gl_FragColor = vec4(rampedR, rampedG, rampedB, 1);
//gl_FragColor = vec4(finalColor,1);
}
編譯並運行程序,你會看到結果如下
創建一個浮雕Shader
下面我們開始創建一個更復雜的shader
1、打開工程,創建一個繼承於CClayer的類CSEEmboss。
2、複製CSEColorRamp.m的實現部分到ofCSEEmboss.m中3、在HelloWorldLayer.m中,添加頭文件,並添加emboss按鈕
#import "CSEEmboss.h"
// 7 - Emboss menu item
CCMenuItem *emboss = [CCMenuItemFont itemWithString:@"Emboss" block:^(id sender) {
CCScene *scene = [CCScene node];
[scene addChild: [CSEEmboss node]];
[[CCDirector sharedDirector] pushScene:scene];
}];
// 7.1 - Create menu
CCMenu *menu = [CCMenu menuWithItems:itemColorRamp, emboss, nil];
編譯並運行,發現結果仍和以前一樣,爲了改變效果,你需要更換shader和layer實現方法。
在工程中創建一個空文件,命名爲CSEEmboss.fsh,打開它,增加以下代碼
在CSEEmboss.m文件的init函數中,更改片段shader的名字爲CSEEmboss.fsh,刪除掉那些與ramp相關的行,編譯運行,你可以看到#ifdef GL_ES
precision mediump float;
#endif
//傳入的數據
varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform float u_time;
void main()
{
// 定義單個像素大小,轉換爲紋理座標
vec2 onePixel = vec2(1.0 /480.0, 1.0 / 320.0);
// 複製紋理座標
vec2 texCoord = v_texCoord;
//設置顏色爲0.5,然後加上每個像素右上顏色與左下顏色之差的5倍,其實可以換個寫法
vec4 color;
color.rgb = vec3(0.5);
// color -= texture2D(u_texture, texCoord - onePixel) * 5.0;
// color += texture2D(u_texture, texCoord + onePixel) * 5.0;
vec4 lbColor= texture2D(u_texture, texCoord - onePixel);
vec4 rtColor = texture2D(u_texture, texCoord + onePixel);
color += (rtColor - lbColor) * 5.0;
//平均三個顏色通道的值,使看起來更灰
color.rgb = vec3((color.r + color.g + color.b) /3.0);
gl_FragColor = vec4(color.rgb, 1);
}
增加移動特效
打開CSEEmboss.m,刪除與ramp相關的變量,增加2個新的實例變量
在init函數中,添加如下代碼int timeUniformLocation;
float totalTime;
然後增加update函數://綁定時間參數
timeUniformLocation =glGetUniformLocation(sprite.shaderProgram->program_,"u_time");
//更新函數,刷新時間
[selfscheduleUpdate];
//使用shader
[sprite.shaderProgramuse];
|
uniform float u_time;然後增加兩行代碼,使紋理做順時針運動
編譯運行,你會看到一個運動的精靈texCoord.x += sin(u_time) * (onePixel.x * 6.0);
texCoord.y += cos(u_time) * (onePixel.y * 6.0);
簡單搖擺的草
與上面的操作一樣,創建繼承於CClayer的類CSEGrass,更換實現文件
sprite = [CCSprite spriteWithFile:@"grass.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointZero;
[self addChild:sprite];
複製CSEEmboss.m的實現內容,修改爲:
sprite = [CCSprite spriteWithFile:@"grass.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointZero;
[self addChild:sprite];
打開HelloWorldLayer.m,導入頭文件
#import "CSEGrass.h"
在init函數中,
|
最後,修改CSEGrass.m文件,使用新的shader#ifdef GL_ES
precision mediump float;
#endif
varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform float u_time;
// 定義一些常量來更容易修改效果
const float speed =2.0;
const float bendFactor =0.2;
void main()
{
//獲得高度,texCoord從下到上爲0到1
float height = 1.0 - v_texCoord.y;
//獲得偏移量,一個冪函數,值愈大,導數越大,偏移量愈大
float offset = pow(height, 2.5);
//偏移量隨時間變化,並乘以幅度,設置頻率
offset *= (sin(u_time * speed) * bendFactor);
//使x座標偏移,fract取區間值(0,1)
vec3 normalColor = texture2D(u_texture, fract(vec2(v_texCoord.x + offset, v_texCoord.y))).rgb;
gl_FragColor = vec4(normalColor, 1);
}
編譯運行,可以看到如下結果:
整個程序的源代碼如下: