微信小游戏开发-shader

小游戏的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: {},//存储glProgram

//初始化gl程序
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));
//设置shader
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();
//range用于指定安全区域的范围
glProgram.setUniformLocationWith1f('range', 1.5);
//centerTexCoord是安全区域圆心的坐标
glProgram.setUniformLocationWith2f('centerTexCoord', 0.5, 0.5);
//rate是sprite节点的宽高比
glProgram.setUniformLocationWith1f('rate', sprite.node.width / sprite.node.height);
sprite._sgNode.setShaderProgram(glProgram);
this.shaderPrograms[shaderName] = glProgram;
}
},
//设置shader参数
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,
},

//初始化shader
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;
//计算当前的安全区域相对于视野的坐标(0.0 - 1.0),circleMgr.CircleNode是安全区域圆心,circleMgr.PicR 是安全区域的半径
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参数。