[Cocos3.0 Tutorial] Realistic looking animated real-time clouds

In this tutorial you will learn how to do multitexturing with cocos2d-x 3.0, create your own shader, and how to modify shader variables at runtime. The effect we are creating is realistic looking real-time animated background clouds.

![] ()

UPDATE

Shader implementation in cocos2d-x 3.1 has changed a bit, here are the files for 3.1 version

Here is a video of the final result

The technique behind the effect is to use multiple noise textures for a single sprite, offset them in different directions and different speeds, then blend and combine them in a shader.

I have used 3 different grayscale noise textures, sizes are 1024x1024, 512x512 and 256x256. For the noise pattern, I have rendered clouds in Photoshop. The different textures serve as a way to get organic looking result. In my experience 3 is enough to get a good looking effect.

Here’s what one of the noise textures looks like:

First, let’s load the textures we are going to use. For the background sky, I have a gradient sprite that I scale to fill the background.

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);

Next thing we are going to do, is to load the textures used for clouds. The scrolling of the textures is done by offsetting the uv-coordinates. We need our texture to repeat when the current texture coordinate is not in 0.0 - 1.0 range. For this, we need create a TexParams-object we use to configure the textures.

Texture2D::TexParams p;
p.minFilter = GL_LINEAR;
p.magFilter = GL_LINEAR;
p.wrapS = GL_REPEAT;
p.wrapT = GL_REPEAT;

The important bits here are the wrapS and wrapT-parameters. These define how the texture renders when the uv-coordinate is not in 0.0 - 1.0 range. The parameters are passed to each of the created textures, with setTexParameters function. We can reuse the parameters for each texture. The minFilter and magFilter determine how the texture is sampled when it’s scaled. It’s important to use textures that are power of two in size. Otherwise we can’t enable 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);

This code creates the 3 textures we use. The cloudsSprite is scaled to fit the screensize, same as the background before. The tex1 and tex2 textures we will bind to our shader later.

Now it’s time to create the shader object. I’m using initWithFilenames for loading, as it saves me the trouble from putting line feeds to end of each line (instead of having the shader in a header file).

// Create the clouds shader
GLProgram* prog = new GLProgram();
prog->initWithFilenames("clouds.vs", "clouds.fs");

We want to have position and texture coordinates vertex attributes at our use, so we need to bind them.

prog->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
prog->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORDS);
prog->link();

Updating the uniforms binds all the default cocos2d-x built-in shader uniforms that the shader uses to the shader object. The default uniforms consist of projection, modelview and modelviewprojection-matrixes, a texture sampler, different versions of time, and a randomized number attribute.

prog->updateUniforms();

After the default uniforms are set, we set up a couple of our own. We want be able to control the parallax speed of the clouds, and also the amount that we want to show. Both of the variables are a single floating point variable in the shader.

auto speedLoc = prog->getUniformLocationForName("u_cloudSpeed");
prog->setUniformLocationWith1f(speedLoc, m_cloudSpeed); 

auto amountLoc = prog->getUniformLocationForName("u_amount");
prog->setUniformLocationWith1f(amountLoc, m_cloudAmount);

Here we query the locations for our texture samplers, and assign them a value that maps to the texture unit of the texture.

auto tex1Loc = prog->getUniformLocationForName("CC_Texture1");
prog->setUniformLocationWith1i(tex1Loc, 1);

auto tex2Loc = prog->getUniformLocationForName("CC_Texture2");
prog->setUniformLocationWith1i(tex2Loc, 2);

And we bind the textures to their texture units. You should do this everytime before rendering the sprite in a real project, as other objects might also set different textures to different texture units. For this short tutorial this will do.

prog->use();
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, tex1->getName());

glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, tex2->getName());

It’s important to set the active texture unit back to GL_TEXTURE0, as this is what cocos2d-x uses by default.

glActiveTexture(GL_TEXTURE0);

Set the program to be used by our sprite.

ShaderCache::getInstance()->addProgram(prog, "clouds");
cloudsSprite->setShaderProgram(prog);
prog->release();

In this tutorial we control the amount of clouds drawn by dragging a finger (or mouse) on the screen vertically. For this, we need to have a touch event listener. Our scene holds a cocos2d::EventListenerTouchAllAtOnce - instance for the task.

I bind a lambda expression to listener’s onTouchesMoved-function, the weird-looking syntax [=] just means that we are going to capture by value any variable that is used inside the lambda. (in our case, we need the this-pointer, and the prog-pointer)

m_eventListenerTouch->onTouchesMoved = [=] (const std::vector<Touch*>& touch, Event* e)
{
	if (touch.size() > 0)
	{
		auto t = touch[0];
		auto delta = t->getDelta() * 0.001f;
		
		m_cloudAmount += delta.y;
		
		m_cloudAmount = fmaxf(0.0f, m_cloudAmount);
		m_cloudAmount = fminf(1.0f, m_cloudAmount);

Here we update our amount of clouds to the shader based on the touch delta.

        prog->use();
        auto amountLoc = prog->getUniformLocationForName("u_amount");
        prog->setUniformLocationWith1f(amountLoc, m_cloudAmount);

	}
};

Finally, add the listener to the event dispatcher.

Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(m_eventListenerTouch, 1);

Now to our shader code. This is the vertex 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;
}

There are a couple of things here to look at. We are setting precision with GL_ES to mediump with our texture coordinates. This determines how accurate the numbers used in the calculations are. Lower precision values use less memory.

There are also a couple of variables that control the speeds of the parallax of the textures. I have set one for each of the layers, and also a global modifier (u_cloudSpeed), that we set from our application, that controls the overall speed (the m_cloudSpeed in HelloWorld.cpp). We set this uniform in the initialization code.

Cocos2d-x built-in uniform CC_Time is a vec4-object that holds different versions of time in it’s components. We are using the one in the x-component.

It’s important to take the modulus of the time. Over time, quite fast actually, without this operation the lack of precision in our texture coordinate variables would cause the parallax movement to became less smooth over time.

The variables declared as varying are variables we pass on to our fragment shader for processing. The values in these variables are interpolated for each of the fragments between vertices, to be used as our texture coordinates there.

Now it’s time for our fragment shader. In this shader the pixels get their final color. The 3 different texture samplers are defined in this file, and we also control the amount of visible clouds by our uniform amount-variable (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)
{

First thing done in the main function is combining the three textures to a single color. As our clouds are grayscale, all the r == g == b for each pixel. For alpha calculation, the rgb channels are added and divided by 3. This results in our alpha value to be in 0.0 - 1.0 range.

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;

To control the amount of the clouds, we filter out some of them by just subtracting an value from the resulting color. If the color goes below 0, it’s clamped to be 0. All the values above 0 are again upscaled by amount relative to what was subtracted, to get them back to 0.0 - 1.0 range.

col -= 1.0 - u_amount;
col = max(col, 0.0);
col *= (1.0 / u_amount);

We are almost done here, there is just one problem: clouds should be darker where they are thicker, now it’s just the opposite. We fix this by inverting the color value.

col.r = 1.0 - col.r;
col.g = 1.0 - col.g;
col.b = 1.0 - col.b;

gl_FragColor = col;
}		

And we are done! Thank you for reading this tutorial. I have tested this on win32 and iOS (iPhone4s). With iPhone4s, I get steady 60fps.

The relevant files for cocos2d-x 3.0 version this tutorial are available for download Here

5 Likes

This is super awesome. +100 for my vote.

@slackmoehrle Thanks, I’m glad you like it :slight_smile:

Yeah that’s a very useful tutorial. Thanks :slight_smile:

what’s the frame rate on that?

@vkreal I tested on my iPhone4s, it’s 60fps on that device.

Hi, I’m unable to make it work on win32 VS 2012…

It seems like it could works fine except for the fmaxf and fminf…
m_cloudAmount = fmaxf(0.0f, m_cloudAmount);
m_cloudAmount = fminf(1.0f, m_cloudAmount);

Could you please tell me what can I do to make it works?

Thanks :slight_smile:

@panor Hi, i was using VS2013. If fmaxf is not found, try using fmax, and check http://www.cplusplus.com/reference/cmath/fmax/

fmaxf and similar is not a true C++ way. Use std::max

Thanks @dotsquid! :slight_smile: It works!

Ha! I used to create those grayscales on the photoshop. To be honest, I never it can be use like this. Thanks, pal!

Ha! I used to create those grayscales on the photoshop. To be honest, I never it can be use like this. Thanks, pal!

Hey, nice tutorial! But, I’ve loaded your tutorial to cocos2d-x 3.1 and I am getting error at

infoFunc(object, GL_INFO_LOG_LENGTH, &logLength);

Do you know how to fix it?
Hugest thanks if you do, I can’t wait to start learning shaders. Because every tutorial I found is giving me this error D:

Hi,

The shader support has been improved in cocos2d-x 3.1, and I needed to fix the tutorial to conform that. The files for also 3.1 are available in the tutorial now.

the cloud file can not visit again?

Hi @KJS, I realize it’s been a few years, but any chance the source files could be shared again (link at the top could is dead).

This looks very cool.

thx
Greg