What you are going to learn:
The easiest way to create normal maps for your sprites is to use SpriteIlluminator. For a quick start you simply
Drag + drop your sprites on the SpriteIlluminator window
Select all sprites
Apply Bevel and Emboss effects
Publish the normal map images to the sprite directory, using the _n
suffix
With TexturePacker you can pack sprites + normal maps on two different sprite sheets, using the same layout:
Start TexturePacker and create a new cocos2d project
Drag + drop sprites and normal maps on the TexturePacker window
Enable the Pack with same layout option in the Normal Maps section
Publish the sprite sheets
To create a new Cocos2d-x project we can use the cocos
command, with the option -l cpp
we create a C++ "hello world" project.
~/Frameworks/cocos2d-x $ cocos new LightingDemo -l cpp
Running command: new
> Copy template into /Users/joachim/Frameworks/cocos2d-x/LightingDemo
> Copying cocos2d-x files...
> Rename project name from 'HelloCpp' to 'LightingDemo'
> Replace the project name from 'HelloCpp' to 'LightingDemo'
> Replace the project package name from 'org.cocos2dx.hellocpp' to 'org.cocos2dx.LightingDemo'
> Replace the mac bundle id from 'org.cocos2dx.hellocpp' to 'org.cocos2dx.LightingDemo'
> Replace the ios bundle id from 'org.cocos2dx.hellocpp' to 'org.cocos2dx.LightingDemo'
~/Frameworks/cocos2d-x $
Open the new project using Xcode or VisualStudio.
We can replace the init()
method of HelloWorldScene.cpp by our own: First we have to load the sprite sheet data file (the one with the .plist extension)
into the SpriteFrameCache
. The texture image file is automatically loaded, its name is composed by replacing the .plist suffix with .png:
bool HelloWorld::init()
{
if (!Layer::init())
return false;
auto spritecache = SpriteFrameCache::getInstance();
spritecache->addSpriteFramesWithFile("spritesheet.plist");
Now we can fetch the individual sprite frames of our animation from the cache:
Vector<SpriteFrame*> animFrames;
char str[100];
for(int i = 1; i <= 8; i++)
{
sprintf(str, "character/%02d.png", i);
animFrames.pushBack(spritecache->getSpriteFrameByName(str));
}
Finally we create a sprite, set the Animate
action on it, and add the sprite to the scene:
auto sprite = Sprite::createWithSpriteFrame(animFrames.front());
Animation *animation = Animation::createWithSpriteFrames(animFrames, 1.0f/8);
sprite->runAction(RepeatForever::create(Animate::create(animation)));
sprite->setPosition(Director::getInstance()->getWinSize() / 2);
addChild(sprite);
return true;
}
If you start the demo application, the (still unlit) animation is played in the center of the screen.
The brightness of each sprite pixel depends on the angle between the light vector and the normal vector (stored in the normal map). This calculation is done by an OpenGL fragment shader:
PointLight.frag contains the shader code for a point light
LightEffect is a class which sets some uniforms (global OpenGL variables) on the shader
Effect is a base class for LightEffect
SpriteEffect is derived from Sprite
, it provides a method to set an Effect
on the sprite
You can clone all sources from GitHub.
First of all you have to modify the code from above, as we want to use an EffectSprite
instead of a Sprite
:
auto sprite = EffectSprite::createWithSpriteFrame(animFrames.front());
Now we create an instance of the LightEffect
and configure some properties.
_effect = LightEffect::create();
_effect->retain();
Vec3 lightPos(200, 200, 100);
_effect->setLightPos(lightPos);
_effect->setLightCutoffRadius(1000);
_effect->setBrightness(2.0);
The LightEffect
creates a point light. A point light is a light source that emits light from a single spot in all directions.
Think of it as a candle, torch, light bulb.
The effect class lets you set several parameters for the effect:
setLightPos
setLightColor
setBrightness
setLightCutoffRadius
setLightHalfRadius
setAmbientLightColor
The newly created LightEffect
can be set on the EffectSprite
. The sprite sheet with the _n
suffix contains the normal maps for all sprites. It is important that this sheet is packed with the same layout as the sprites!
sprite->setEffect(_effect, "spritesheet_n.png");
After adding these few lines our application displays a lit animation, but we are not yet able to move the light position. We are going to change this in the next section.
Let's add a sprite symbolizing the light source:
_lightSprite = Sprite::create("lightbulb.png");
_lightSprite->setPosition(lightPos.x, lightPos.y);
this->addChild(_lightSprite);
To allow the user to move around the light bulb we register some touch callbacks:
auto listerner = EventListenerTouchAllAtOnce::create();
listerner->onTouchesBegan = CC_CALLBACK_2(HelloWorld::handleTouches, this);
listerner->onTouchesMoved = CC_CALLBACK_2(HelloWorld::handleTouches, this);
listerner->onTouchesEnded = CC_CALLBACK_2(HelloWorld::handleTouches, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listerner, this);
The implementation of handleTouches()
is quite simple, we update the position of the light bulb sprite, and tell the effect where our light is positioned now:
void HelloWorld::handleTouches(const std::vector<Touch *> &touches, cocos2d::Event *)
{
for (auto &touch: touches)
{
Point pos = touch->getLocation();
_lightSprite->setPosition(pos);
_lightPos.set(pos.x, pos.y, _lightPos.z);
_effect->setLightPos(_lightPos);
}
}
In the demo project on GitHub we've also added some background images: