原作者:
KJS
英文论坛原帖:
http://discuss.cocos2d-x.org/t/cocos3-0-tutorial-realistic-looking-animated-real-time-clouds/13694
在这篇教程中你讲学习如何使用cocos2d-x 3.0进行多材质处理,创建你自己的着色器shader,还有在运行时修改着色器变量。我们将要制作的效果是真实的实时变动的背景云层。
更新
Shader调用方法在cocos2d-x 3.1有些许变动,点击这里查看3.1版本:https://dl.dropboxusercontent.com/u/6502655/clouds_3.1.1.rar,1
这是最终结果视频(需翻出去):https://www.youtube.com/watch?v=d3WdN9vx9_w&feature=youtu.be
(看不了视频连接的可以参考)GIF:
在这效果背后所使用的技术就是对一个单独的精灵使用多层杂色材质,对他们在不同的方向和不同速度上进行补偿,然后将他们组合成进shader中。
我是用了3种不同的灰度杂色材质,他们的尺寸分别是:1024x1024, 512x512 和 256x256。对于这些杂色图层,我是在PhotoShop里制作的渲染云层。不同的材质提供了一种得到生动画面的方法。在我的经验中3种材质足够得到非常好的效果。
这是其中一张杂色材质的样子:

首先让我们先读取将要使用的材质。对于背景天空,我用一个梯度精灵来按比例将其填满背景。
Size visibleSize = Director::getInstance()->getVisibleSize();
Point origin = Director::getInstance()->getVisibleOrigin();
auto bg = Sprite::create("bg.png");
bg->setPosition(center);
auto bgsize = bg->getContentSize();
float scaleX = visibleSize.width / bgsize.width;
float scaleY = visibleSize.height / bgsize.height;
bg->setScaleX(scaleX);
bg->setScaleY(scaleY);
addChild(bg, 0);
```
接下来我们要做的就是读取云层材质。卷动材质是通过补偿UV-坐标完成的。我们需要我们的材质在现有的坐标不在0.0 - 1.0的范围内能够重复使用。为了实现这个目标我们需要创建一个TexParams物体用来安装材质。
Texture2D::TexParams p;
p.minFilter = GL_LINEAR;
p.magFilter = GL_LINEAR;
p.wrapS = GL_REPEAT;
p.wrapT = GL_REPEAT;
```
这里重要的部分是wrapS和wrapT参数。这些定义了当uv坐标不在0.0到1.0的范围时如何进行材质渲染。这些参数是要传递到每一个已经创建的材质中的,使用setTexParameters函数。我们能对每一个材质重复使用这些参数。minFilter和magFilter决定了测量时有多少材质被使用,使用材质尺寸的二次幂是非常重要的,否则我们将不能使用GL_REPEAT。
auto cloudsSprite = Sprite::create("noise_1024.png");
cloudsSprite->getTexture()->setTexParameters(p);
cloudsSprite->setPosition(center);
float cloudsScaleX = visibleSize.width / cloudsSprite->getContentSize().width;
float cloudsScaleY = visibleSize.height / cloudsSprite->getContentSize().height;
cloudsSprite->setScaleX(cloudsScaleX);
cloudsSprite->setScaleY(cloudsScaleY);
addChild(cloudsSprite, 0);
auto textureCache = Director::getInstance()->getTextureCache();
auto tex1 = textureCache->addImage("noise_512.png");
tex1->setTexParameters(p);
auto tex2 = textureCache->addImage("noise_256.png");
tex2->setTexParameters(p);
```
这些代码创建了3中我们将要使用的材质,云层精灵按照比例填充屏幕尺寸,就像之前制作背景一样。材质text1和text2我们稍后将他们绑定到着色器shader上。
现在是时候创建着色器物体了,我使用initWithFilenames来读取,毕竟它为我省下了在头文件写上shader的麻烦
// Create the clouds shader
GLProgram* prog = new GLProgram();
prog->initWithFilenames("clouds.vs", "clouds.fs");
```
我们需要使用方位和材质坐标定点属性,所以我们需要绑定他们。
prog->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
prog->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORDS);
prog->link();
```
更新uniforms以此绑定所有cocos2d-x默认用来给物体着色的shader类uniforms。 默认的uniforms由投射projection,模型预览modelview和模型预览投射矩阵modelviewprojection-matrixes, 一个材质样本,不同版本的时间,随机数字属性。
prog->updateUniforms();
```
在默认的uniforms设定后,我们制作几个我们自己的函数,我们需要能够控制云层在视差角度上的速度,还有我们要它展示数多少的云层。这两个参数都是一个在shader中的单浮点变量
auto speedLoc = prog->getUniformLocationForName("u_cloudSpeed");
prog->setUniformLocationWith1f(speedLoc, m_cloudSpeed);
auto amountLoc = prog->getUniformLocationForName("u_amount");
prog->setUniformLocationWith1f(amountLoc, m_cloudAmount);
```
这里我们查询我们材质样本的位置,并给他们分配一个值使每一个材质单位标记到地图上。
auto tex1Loc = prog->getUniformLocationForName("CC_Texture1");
prog->setUniformLocationWith1i(tex1Loc, 1);
auto tex2Loc = prog->getUniformLocationForName("CC_Texture2");
prog->setUniformLocationWith1i(tex2Loc, 2);
```
在绑定了材质每个材质单位后。你应该在每个项目中渲染精灵时都这么做,毕竟其他物体可能也会设置不同的材质给不同的材质单位。在这短篇的教程中应按如下做法:
prog->use();
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, tex1->getName());
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, tex2->getName());
```
把使用中的材质单位设置回GL_TEXTURE0是非常重要的,这也是cocos2d-x默认的使用方法。
glActiveTexture(GL_TEXTURE0);
```
把程序设置为供精灵使用
ShaderCache::getInstance()->addProgram(prog, "clouds");
cloudsSprite->setShaderProgram(prog);
prog->release();
```
在这个教程中我们通过在屏幕上拖拽手指(或鼠标)来控制云层数量。为了达到这个效果我们需要添加一个触摸时间监听器。我们的场景中应该有cocos2d::EventListenerTouchAllAtOnce——事例来完成这项任务。
我绑定了一个lambda表达式到监听器的onTouchesMoved函数上,长得有点怪异的语句=]代表我们将通过他们的值来捕获那些在lambda里面的变量。
m_eventListenerTouch->onTouchesMoved = =] (const std::vector& touch, Event* e)
{
if (touch.size() > 0)
{
auto t = touch;
auto delta = t->getDelta() * 0.001f;
m_cloudAmount += delta.y;
m_cloudAmount = fmaxf(0.0f, m_cloudAmount);
m_cloudAmount = fminf(1.0f, m_cloudAmount);
// 这里,在基于触摸的基础上我们把云层的数量上传到shader上。
prog->use();
auto amountLoc = prog->getUniformLocationForName("u_amount");
prog->setUniformLocationWith1f(amountLoc, m_cloudAmount);
}
};
```
最后,添加监听事件到时间分配器上(event dispatch)。
Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(m_eventListenerTouch, 1);
```
这里就是我们的shader代码。这是顶点着色器:
attribute vec4 a_position;
attribute vec2 a_texCoord;
#ifdef GL_ES
precision mediump float;
varying mediump vec2 v_texCoord1;
varying mediump vec2 v_texCoord2;
varying mediump vec2 v_texCoord3;
#else
varying vec2 v_texCoord1;
varying vec2 v_texCoord2;
varying vec2 v_texCoord3;
#endif
uniform float u_cloudSpeed;
const float layer1Speed = 0.1;
const float layer2Speed = 0.2;
const float layer3Speed = 0.05;
void main()
{
gl_Position = CC_PMatrix * a_position;
float time1 = mod(CC_Time.x * layer1Speed * u_cloudSpeed, 1.0);
float time2 = mod(CC_Time.x * layer2Speed * u_cloudSpeed, 1.0);
float time3 = mod(CC_Time.x * layer3Speed * u_cloudSpeed, 1.0);
v_texCoord1 = a_texCoord;
v_texCoord1.x += time1;
v_texCoord2 = a_texCoord;
v_texCoord2.xy += time2;
v_texCoord3 = a_texCoord;
v_texCoord3.xy += time3;
}
```
这些是一些我们需要注意的事项。我们把GL_ES的精度付给mediump的材质坐标。这将决定我们在这里用作计算的数值准确度。低精度会使用较少的内存。
这里也有一些用来控制材质视差角度速度的变量。我给每一个层都设置了一个。也有一个整体的修改参数(u_cloudSpeed),从应用程序中设置,控制整体速度(m_cloudSpeed在HelloWorld.cpp中)。我们在初始化代码中设置这个值。
Cocos2d-x 构造函数统一字符CC_Time是一个四维矢量物体(vec4-object)是一个能够对其内部成员设置不同时间的参数。我们将在x成员中使用。
对时间采用不同的系数非常重要。随着时间的过去,实际上很快,没有这项工作材质坐标变量将缺乏精度,并导致视差移动随着时间变得不那么顺畅。
这些声明成各不相同的变量就是我们要传递给我们碎片着色器fragment shader作处理的。这些变量的值将会被植入到每一个顶点中,作为我们的材质坐标。
现在是时候处理我们的碎片着色器fragment shader了。在这个着色器中像素将会得到他们最终的颜色。3个不同的材质样本将在这个文件中被定义。我们通过u-amount变量同样控制着可见云层的数量。
#ifdef GL_ES
precision mediump float;
varying mediump vec2 v_texCoord1;
varying mediump vec2 v_texCoord2;
varying mediump vec2 v_texCoord3;
#else
varying vec2 v_texCoord1;
varying vec2 v_texCoord2;
varying vec2 v_texCoord3;
#endif
uniform sampler2D CC_Texture0;
uniform sampler2D CC_Texture1;
uniform sampler2D CC_Texture2;
uniform float u_amount;
void main(void)
{
// 首先要在主函数里完成的就是三种材质绑定成同一种颜色。毕竟我们的云层是灰度图,所有的像素在r、g、b通道上都是相同的。对于alpha算法,RGB通道被添加并被除以3.这使得我们的alpha值处于0.0到1.0之间。
vec4 col = texture2D(CC_Texture0, v_texCoord1) * texture2D(CC_Texture1, v_texCoord2) * texture2D(CC_Texture2, v_texCoord3);
col.a = (col.r + col.g + col.b) * 0.33;
// 为了控制云层的多少。我们通过得到的最终颜色减去一个值过滤掉他们其中的一些。如果颜色值小于0,它将被固定为0。所有大于0的值将相对于减去的数量恢复比例,时它们回到0.0-1.0的范围。
col -= 1.0 - u_amount;
col = max(col, 0.0);
col *= (1.0 / u_amount);
// 我们差不多就要完工了,这里只有一个问题了:当他们变厚的时候云层应该颜色加深。现在则是相反。我们通过反转颜色解决这个问题。
col.r = 1.0 - col.r;
col.g = 1.0 - col.g;
col.b = 1.0 - col.b;
gl_FragColor = col;
}
```
完成!感谢你阅读这篇教程。我在自己的win32和
iOS(iPhone4s)上做过测试。用iPhone4s上有稳定的60帧。对应这篇教程cocos2d-x3.0版本的相关文件在这,可供下载:https://dl.dropboxusercontent.com/u/6502655/clouds.rar,1
v3.0 文件下载(翻不了墙的看这里): puzzle.zip (381 KB)



