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);
}
编译运行,可以看到如下结果:
整个程序的源代码如下: