読者です 読者をやめる 読者になる 読者になる

WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

cocos2d-xでお手軽オフスクリーンレンダリング

今回のエンジニアブログを担当する安藤です。
cocos2d-xでオフスクリーンレンダリングする手法とちょっとしたテクニックをご紹介したいと思います。

●オフスクリーンレンダリングとは
フレームバッファを描画サーフェスにレンダリングせずフレームバッファやテクスチャなどにレンダリングするレンダリング手法です。今回はcocos2d-xの機能にあるCCRenderTextureを使ってお手軽にオフスクリーンレンダリングを実装してみたいと思います。

では、実際に実装します。
実装はいたってシンプルでCCRenderTextureのbeginWithClear()/end()メソッドの間に描画処理(visit)を挟むだけです。

CCSprite* pSprite = CCSprite::create("HelloWorld.png");// add "HelloWorld" splash screen"  
pSprite->retain();  // addしないオブジェクトはかならずrelease()しよう  
  
CCSize win_size = CCDirector::sharedDirector()->getWinSize();  
  
// CCRenderTexture生成  
CCRenderTexture* pRenderTexture = CCRenderTexture::create(win_size.width, win_size.height);  
pRenderTexture->setPosition(ccp(win_size.width/2, win_size.height/2));  
  
this->addChild(pRenderTexture);  
  
// オフスクリーンレンダリング  
pRenderTexture->beginWithClear(0,0,1,1);  
  
for(int i=0;i<1000;i++){  
  
  pSprite->setPosition(ccp(rand()%(int)win_size.width,rand()%(int)win_size.height));  
  
  pSprite->setScale(0.1f);  
  
  pSprite->visit();  
  
}  
  
pRenderTexture->end();  

オフスクリーンレンダリングした画像を描画した結果です。
初期化時に1000個のspriteを一枚のテクスチャにまとめて表示しています。
私の環境ではFPSは60を指しています。

1

1000個のspriteを素直にaddChild()して描画してみました。
私の環境ではFPSは20まで落ちます。

2

主な使い方としては重たいエフェクトの前に静的なSpriteをCCRenderTextureにレンダリングすることで高速化を計ったりします。

● 導入例  pushScene()先で前のシーンを表示する
cocos2d-xの仕様でpushScene()したらpush前のシーンが表示されません。
事前にCCRenderTextureに書き込んでおいて背景に描画することで解決します。
以下がソースコードになります。

HelloWorldScene.h

#include "cocos2d.h"  
  
class HelloWorld : public cocos2d::CCLayer  
{  
public:  
  // Method 'init' in cocos2d-x returns bool, instead of 'id' in cocos2d-iphone (an object pointer)  
  virtual bool init();  
  
  // there's no 'id' in cpp, so we recommend to return the class instance pointer  
  static cocos2d::CCScene* scene();  
  
  // a selector callback  
  void menuCloseCallback(CCObject* pSender);  
  
  // preprocessor macro for "static create()" constructor ( node() deprecated )  
  CREATE_FUNC(HelloWorld);  
};

HelloWorldScene.cpp

#include "HelloWorldScene.h"  
#include "SimpleAudioEngine.h"  
#include "Scene2.h"  
  
using namespace cocos2d;  
using namespace CocosDenshion;  
  
CCScene* HelloWorld::scene()  
{  
  // 'scene' is an autorelease object  
  CCScene *scene = CCScene::create();  
  
  // 'layer' is an autorelease object  
  HelloWorld *layer = HelloWorld::create();  
  
  // add layer as a child to scene  
  scene->addChild(layer);  
  
  // return the scene  
  return scene;  
}  
  
// on "init" you need to initialize your instance  
bool HelloWorld::init()  
{  
  //////////////////////////////  
  // 1. super init first  
  if ( !CCLayer::init() )  
  {  
    return false;  
  }  
  
  /////////////////////////////  
  // 2. add a menu item with "X" image, which is clicked to quit the program  
  //    you may modify it.  
  // add a "close" icon to exit the progress. it's an autorelease object  
  CCMenuItemImage *pCloseItem = CCMenuItemImage::create(  
    "CloseNormal.png",  
    "CloseSelected.png",  
    this,  
    menu_selector(HelloWorld::menuCloseCallback) );  
  
  pCloseItem->setPosition( ccp(CCDirector::sharedDirector()->getWinSize().width - 20, 20) );  
  
  // create menu, it's an autorelease object  
  CCMenu* pMenu = CCMenu::create(pCloseItem, NULL);  
  pMenu->setPosition( CCPointZero );  
  
  this->addChild(pMenu, 1);  
  
  /////////////////////////////  
  // 3. add your codes below...  
  // add a label shows "Hello World"  
  // create and initialize a label  
  CCLabelTTF* pLabel = CCLabelTTF::create("Scene1", "Thonburi", 34);  
  
  // ask director the window size  
  CCSize size = CCDirector::sharedDirector()->getWinSize();  
  
  // position the label on the center of the screen  
  pLabel->setPosition( ccp(size.width / 2, size.height / 2-40) );  
  
  // add the label as a child to this layer  
  this->addChild(pLabel, 1);  
  
  // add the sprite as a child to this layer  
  // スプライトを2000個描画してギリギリの負荷をかける  
  for(int i=0;i<2000;i++){  
  
    CCSprite* sprite=CCSprite::create("Icon.png");  
  
    float x = sprite->getContentSize().width*(i%80)*0.1f;  
    float y = sprite->getContentSize().height*(i/80)*0.1f;  
  
    sprite->setPosition(ccp(x,y));  
    sprite->setScale(0.1f);  
  
    this->addChild(sprite, 0);  
  }  
  
  return true;  
}  
  
void HelloWorld::menuCloseCallback(CCObject* pSender)  
{  
  CCSize win_size = CCDirector::sharedDirector()->getWinSize();  
  
  // CCRenderTexture生成  
  CCRenderTexture* pRenderTexture = CCRenderTexture::create(win_size.width, win_size.height);  
  pRenderTexture->setPosition(ccp(win_size.width/2, win_size.height/2));  
  
  // オフスクリーンレンダリング  
  pRenderTexture->beginWithClear(0,0,1,1);  
  
  this->visit();  
  
  pRenderTexture->end();  
  
  CCDirector::sharedDirector()->pushScene(Scene2::scene(pRenderTexture));  
}

Scene2.h

#include "cocos2d.h"  
  
class Scene2 : public cocos2d::CCLayer  
{  
public:  
  // Method 'init' in cocos2d-x returns bool, instead of 'id' in cocos2d-iphone (an object pointer)  
  virtual bool init();  
  
  // there's no 'id' in cpp, so we recommend to return the class instance pointer  
  static cocos2d::CCScene* scene(cocos2d::CCRenderTexture* pRenderTexture);  
  
  // preprocessor macro for "static create()" constructor ( node() deprecated )  
  CREATE_FUNC(Scene2);  
};

Scene2.cpp

#include "Scene2.h"  
  
using namespace cocos2d;  
  
CCScene* Scene2::scene(CCRenderTexture* pRenderTexture)  
{  
  // 'scene' is an autorelease object  
  CCScene *scene = CCScene::create();  
  
  // 'layer' is an autorelease object  
  Scene2 *layer = Scene2::create();  
  layer->addChild(pRenderTexture,0);  
  
  // add layer as a child to scene  
  scene->addChild(layer);  
  
  // return the scene  
  return scene;  
}  
  
// on "init" you need to initialize your instance  
bool Scene2::init()  
{  
  if ( !CCLayer::init() )  
  {  
    return false;  
  }  
  
  CCLabelTTF* pLabel = CCLabelTTF::create("Scene2", "Thonburi", 34);  
  
  CCSize size = CCDirector::sharedDirector()->getWinSize();  
  
  pLabel->setPosition( ccp(size.width / 2, size.height / 2+40) );  
  
  this->addChild(pLabel,2);  
  
  // 同じくスプライトを2000個描画して処理落ちしていないか確認  
  for(int i=0;i<2000;i++){  
  
    CCSprite* sprite=CCSprite::create("Icon.png");  
  
    float x = sprite->getContentSize().width*(i%80)*0.1f;  
    float y =-sprite->getContentSize().height*(i/80)*0.1f;  
  
    sprite->setPosition(ccp(x,y+size.height));  
    sprite->setScale(0.1f);  
  
    this->addChild(sprite, 1);  
  }  
  
  return true;  
}

Push前のシーンです。Spriteを2000個表示してギリギリの処理負荷を掛けてます。

3

Push後のシーンです。さらにSpriteを2000個表示しても処理が落ちていないのが確認できます。

4

pushScene()を行ったシーンは更新も描画もされることがないので、 pushされたシーンはpush前のシーン処理を気にせずコードを書けるメリットがあります。

お手軽とはいえ癖のあるクラスです。少しではありますがTIPSをまとめてみました。

● 3Dオブジェクトを描画した場合、Zが破損しているDepth Bufferを保存するよう引数を追加する必要があります。

// CCRenderTexture生成  
CCRenderTexture* pRenderTexture = CCRenderTexture::create(win_size.width, win_size.height,kCCTexture2DPixelFormat_RGBA8888,GL_DEPTH_COMPONENT16);  
  
// オフスクリーンレンダリング  
pRenderTexture->beginWithClear(0,0,1,1,kCCTexture2DPixelFormat_RGBA8888,GL_DEPTH_COMPONENT16);  

● 描画したRenderTextureがチラツキとめり込みを起こす DepthTextをfalseにするとなおります。

CCDirector::sharedDirector()->setDepthTest(false);

● 毎フレーム更新して使用したい 低端末で処理落ちを起こします。特にAndroid2.3系でその処理落ちが顕著に現れます。