怎么在cocos2d与GLSL2.0中用shader实现很酷的效果

17 篇文章 0 订阅

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



  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值