小游戏的shader开发
由于小游戏的开发目前还主要是以2D为主,所以shader主要是用来对贴图进行自定义的裁剪处理,没有涉及到光照、贴图材质等内容。下面通过一个例子来说明小游戏开发中的shader编程。
毒气圈的实现
在吃鸡类的游戏中,为了加快游戏的节奏,会有一个毒气圈的概念,被毒气覆盖的玩家会受到毒气的伤害。随着游戏的进行,安全区域会不断缩小,存活的玩家为了继续生存会往安全区域移动,与其它存活的玩家相遇。
如下图所示,我们通过在屏幕上覆盖一层蓝色的蒙板来表示当前区域被毒气覆盖,没有被蒙板覆盖的区域则为安全区域,安全区域是以地图上某个点为圆心的圆形区域。在游戏的过程中,通过扩大蓝色蒙板覆盖的范围来实现安全区域收缩的效果。

代码
GasShader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| var GasShader = { shaderPrograms: {}, setShader: function (sprite, shaderName) { let glProgram = this.shaderPrograms[shaderName]; if (!glProgram) { glProgram = new cc.GLProgram(); let vert = require(cc.js.formatStr('%s.vert', shaderName)); let frag = require(cc.js.formatStr('%s.frag', shaderName)); glProgram.initWithVertexShaderByteArray(vert, frag); glProgram.addAttribute(cc.macro.ATTRIBUTE_NAME_POSITION, cc.macro.VERTEX_ATTRIB_POSITION); glProgram.addAttribute(cc.macro.ATTRIBUTE_NAME_COLOR, cc.macro.VERTEX_ATTRIB_COLOR); glProgram.addAttribute(cc.macro.ATTRIBUTE_NAME_TEX_COORD, cc.macro.VERTEX_ATTRIB_TEX_COORDS); glProgram.link(); glProgram.updateUniforms(); glProgram.use(); glProgram.setUniformLocationWith1f('range', 1.5); glProgram.setUniformLocationWith2f('centerTexCoord', 0.5, 0.5); glProgram.setUniformLocationWith1f('rate', sprite.node.width / sprite.node.height); sprite._sgNode.setShaderProgram(glProgram); this.shaderPrograms[shaderName] = glProgram; } }, changeVariable: function(range, centerX, centerY) { let shaderName = 'gas'; let glProgram = this.shaderPrograms[shaderName]; if (glProgram) { glProgram.use(); glProgram.setUniformLocationWith1f('range', range); glProgram.setUniformLocationWith2f('centerTexCoord', centerX, centerY); } }, };
module.exports = GasShader;
|
在GasShader脚步中主要是初始化了gl程序和设置shader默认参数,声明了三个全局的shader变量,其作用会在下面的shader代码中说明,其中range和centerTexCoord可以通过changeVariable来更新。
gas.vert
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| module.exports = ` attribute vec4 a_position; //传入的默认的这张图片的顶点信息 attribute vec2 a_texCoord; //传入的默认的这张图片的纹理 attribute vec4 a_color; //传入的默认的这张图片的颜色 varying vec4 v_fragmentColor; varying vec2 v_texCoord;
void main() { gl_Position = CC_PMatrix * a_position; v_fragmentColor = a_color; v_texCoord = a_texCoord; }`;
|
顶点着色器的内容很简单,仅仅是将顶点坐标换算成观察坐标,同时将UV坐标和片元颜色传递给片段着色器。
gas.frag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| module.exports = ` #ifdef GL_ES precision lowp float; #endif
varying vec4 v_fragmentColor; varying vec2 v_texCoord; uniform vec2 centerTexCoord; uniform float range; uniform float rate;
void main() { vec4 c = v_fragmentColor * texture2D(CC_Texture0, v_texCoord); vec2 point = vec2(v_texCoord.x * rate,v_texCoord.y); vec2 centerPoint = vec2(centerTexCoord.x * rate,centerTexCoord.y); gl_FragColor = c; if(length(point - centerPoint) < range) discard;
}`;
|
在片段着色器中,我们首先计算出当前片段的颜色。接着我们使用片段的UV坐标来计算与传进来的centerTexCoord的距离,如果距离超过了我们定义的range,则丢弃这个片元。我们不能直接使用UV坐标来计算距离,因为当前渲染的sprite的宽高是不一样的,所以我们将x坐标乘上了传进来的宽高比rate。其实如果我们知道片元的世界坐标的话,可以直接用片元的世界坐标与安全区域圆心的世界坐标计算距离,但是我们从顶点着色器中得到的gl_Position是观察坐标,已经不是世界坐标。
CircleRenderingCtrl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| var GasShader = require('GasShader'); let Util = require('Util');
cc.Class({ extends: cc.Component, properties: { _x:0.5, _y:0.5, _r:1.2, }, onLoad: function () { GasShader.setShader(this.node.getComponent(cc.Sprite), 'gas'); }, update: function(dt) { let camRect=window.GGameEntry.BattleRoot.GetCameraViewArea(); let circleMgr = window.SceneMgr.GetBattleScene().getSceneBattleLogic().getBattleSystem().CircleMgr; let x=(circleMgr.CircleNode.x-camRect[0]) / (camRect[2]-camRect[0]); let y=(circleMgr.CircleNode.y-camRect[1]) / (camRect[3]-camRect[1]); let r= circleMgr.PicR / (camRect[3]-camRect[1]); if(!Util.isNodeVisible(circleMgr.CircleNode)){ x=0.5; y=0.5; r=1.2; } if(x !== this._x || y !== this._y || r !== this._r) { this._x = x; this._y = y; this._r = r; GasShader.changeVariable(r, x, 1-y); } } });
|
CircleRenderingCtrl 挂在实际渲染的节点上,首先是为节点上的sprite绑定shader,在每个update中去计算shader中的参数。
因为在片元着色器中我们实际上使用的是片元的UV坐标来计算距离,所以我们需要将安全区域的坐标转换成相对于视野的‘UV’坐标。
首先我们通过GetCameraViewArea接口获取了视野的矩形框坐标,也就是视野左上角、左下角、右下角、右上角的坐标,下面是GetCameraViewArea接口的实现。
1 2 3 4 5 6 7 8 9 10 11
| GetCameraViewArea: function () { var v2CenterPos = this.BattleCamera.node.position; let irzoomRatio = 1 / this.BattleCamera.zoomRatio; var sceneWidth = cc.visibleRect.width * irzoomRatio; var sceneHeight = cc.visibleRect.height * irzoomRatio; this.worldCameraFrame[0] = v2CenterPos.x - sceneWidth * 0.5; this.worldCameraFrame[1] = v2CenterPos.y - sceneHeight * 0.5; this.worldCameraFrame[2] = v2CenterPos.x + sceneWidth * 0.5; this.worldCameraFrame[3] = v2CenterPos.y + sceneHeight * 0.5; return this.worldCameraFrame; },
|
接着我们以视野的高度为单位1,将安全区域的坐标换算成相对于视野的‘UV’坐标。
最后我们在这几个shader参数发生变化的时候调用接口更新shader参数。