怎麼在cocos2d與GLSL2.0中用shader實現很酷的效果

原文地址:http://www.raywenderlich.com/10862/how-to-create-cool-effects-with-custom-shaders-in-opengl-es-2-0-and-cocos2d-2-x

Shaders(着色器) 隨着3D遊戲的發展會有一個很大的進步。它允許程序員創建新的特效,並決定如何顯示在屏幕上。如果你還沒有用過shader,那麼閱讀本教程之後,你就會了。

Cocos2D是現在最好的IOS遊戲開發框架之一,很幸運,新版本的Cocos2D 2.X支持OpenGL-ES 2.0 和 shaders。在本教程中,你會在cocos2D的幫助下學會如何創建和使用shaders。

你將會學到:

  • GLSL的基礎語言
  • 在cocos2D中如何創建和使用自定義的shaders
  • 三個shader例子:

  1. 如何使用ramp textures(從白到黑的漸進紋理)來修改遊戲中的顏色
  2. 如何創建浮雕效果
  3. 如何創建草的隨風擺動效果

讓我們開始

在研究shaders和如何使用他們之前,你必須先下載並安裝。然後創建一個cocos2D的工程。步驟如下:

1、下載最新版本的cocos2D 2.X

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都用一個main函數,這個是入口函數

void main()

{

//gl_Positon是內置的輸入變量,將各個頂點經過投影變換之後傳給片段Shader

  gl_Position = u_MVPMatrix* a_position;

//將輸入的紋理座標傳給片段Shader

  v_texCoord = a_texCoord;

}

再來看下片段Shader :ccShader_PositionTexture_frag.h

//設置中等精度的浮點數,精度越高,速度越慢

#ifdef GL_ES

precision mediumpfloat;

#endif

 

//接收從頂點Shader傳入的紋理座標

varying vec2 v_texCoord;

//紋理常量,從cocos2d程序中傳入

uniform sampler2D u_texture;

 

void main()

{

//gl_FragColor內置輸出,從u_texture紋理中提取座標爲v_texCoord的顏色作爲輸出顏色

  gl_FragColor =  texture2D(u_texture, v_texCoord);

}

想要理解一切是如何發生的,可以通過CCGrid.m,來找出來是怎麼來用shader的
首先,在初始化函數init中,shader被加載到cache中
self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];
如果你很好奇,可以進到CCShaderCache類中來觀察shader是如何編譯和保存的。

接下來,在blit函數中,傳值給shader並運行它:

-(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);

}

你可能想要知道圖片是在哪裏傳入的。在afterDraw中

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; //用來渲染的紋理
}
接下來,初始化你的變量:

- (id)init
{
  self = [super init];
  if (self) {
    // 初始化原始精靈
    sprite = [CCSprite spriteWithFile:@"Default.png"];
    sprite.anchorPoint = CGPointZero;
    sprite.rotation = 90;
    sprite.position = ccp(0, 320);
    [self addChild:sprite];
 
    // 用默認的頂點shader和自定義的CSEColorRamp.fsh片段shader來創建shader程序,然後爲每個屬性指定索引,屬性綁定必須在鏈接之前
//注意,updateUniforms中指定了投影矩陣的索引以及精靈紋理的索引以及存儲位置

    const GLchar * fragmentSource = (GLchar*) [[NSString stringWithContentsOfFile:[CCFileUtils fullPathFromRelativePath:@"CSEColorRamp.fsh"] encoding:NSUTF8StringEncoding error:nil] UTF8String];
    sprite.shaderProgram = [[CCGLProgram alloc] initWithVertexShaderByteArray:ccPositionTextureA8Color_vert
                                       fragmentShaderByteArray:fragmentSource];
    [sprite.shaderProgram addAttribute:kCCAttributeNamePosition index:kCCVertexAttrib_Position];
    [sprite.shaderProgram addAttribute:kCCAttributeNameTexCoord index:kCCVertexAttrib_TexCoords];
    [sprite.shaderProgram link];
    [sprite.shaderProgram updateUniforms];
 
    //爲次級紋理指定索引以及把並初始化存儲位置
    colorRampUniformLocation = glGetUniformLocation(sprite.shaderProgram->program_, "u_colorRampTexture");
    glUniform1i(colorRampUniformLocation, 1);
 
    //加載次級紋理,禁用線性插值
    colorRampTexture = [[CCTextureCache sharedTextureCache] addImage:@"colorRamp.png"];
    [colorRampTexture setAliasTexParameters];
 
    // 使用GL_TEXTURE1位置的紋理,並綁定次級紋理
    [sprite.shaderProgram use];
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, [colorRampTexture name]);
    glActiveTexture(GL_TEXTURE0);
  }
  return self;
}
現在,我們需要一個片段shader來完成渲染。在工程中創建一個iOS\Other\Empty空文件,命名爲CSEColorRamp.fsh,然後在菜單Editor/Syntax Coloring中選擇GLSL,這有助於我們編輯shader。在shader中寫入

#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,打開它,增加以下代碼

#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文件的init函數中,更改片段shader的名字爲CSEEmboss.fsh,刪除掉那些與ramp相關的行,編譯運行,你可以看到


加移動特效

打開CSEEmboss.m,刪除與ramp相關的變量,增加2個新的實例變量

  int timeUniformLocation;
  float totalTime;
在init函數中,添加如下代碼

        //綁定時間參數

timeUniformLocation =glGetUniformLocation(sprite.shaderProgram->program_,"u_time");

        

//更新函數,刷新時間

[selfscheduleUpdate];

        

        //使用shader

        [sprite.shaderProgramuse];

然後增加update函數:

- (void)update:(float)dt
{
  totalTime += dt;
  [sprite.shaderProgram use];
  glUniform1f(timeUniformLocation, totalTime);
}
現在你需要在CSEEmboss.fsh中添加一個時間變量

  	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函數中,

		// 7.1 - Grass menu item
		CCMenuItem *grass = [CCMenuItemFont itemWithString:@"Grass" block:^(id sender) {
			CCScene *scene = [CCScene node];
			[scene addChild: [CSEGrass node]];
			[[CCDirector sharedDirector] pushScene:scene];
		}];
		// 7.2 - Create menu
		CCMenu *menu = [CCMenu menuWithItems:itemColorRamp, emboss, grass, nil];
創建一個新的shader,命名爲CSEGrass.fsh,打開,添加以下內容:

#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從下到上爲01

    float height = 1.0 - v_texCoord.y;

    //獲得偏移量,一個冪函數,值愈大,導數越大,偏移量愈大

    float offset = pow(height, 2.5);

    

    //偏移量隨時間變化,並乘以幅度,設置頻率

    offset *= (sin(u_time * speed) * bendFactor);

    

    //使x座標偏移,fract取區間值(01

    vec3 normalColor = texture2D(u_texture, fract(vec2(v_texCoord.x + offset, v_texCoord.y))).rgb;

    gl_FragColor = vec4(normalColor, 1);

}

最後,修改CSEGrass.m文件,使用新的shader

編譯運行,可以看到如下結果:


整個程序的源代碼如下:

example project 



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章