A Day In The Life

とあるプログラマの備忘録

オフスクリーンレンダリングを使って画像を動的にアトラス化する

オフスクリーンレンダリングという手法があります。この手法をつかって動的に画像をアトラス化してスプライトフレームキャッシュ(SpriteFrameCache)に乗せることができると、必要最小限の画像をアトラス化して効率よくキャッシュできるのではないかということで試してみました。
まずはオフスクリーンレンダリングとスプライトフレームキャッシュそれぞれの使い方を説明してから、その2つを組み合わせる方法を最後に紹介します。

オフスクリーンレンダリング

オフスクリーンレンダリングとは、テクスチャを動的に変更するためのテクニックです。動的にテクスチャを生成する時に画面の表示領域でやるのではなく画面の表示されないところで行います。オフスクリーンと言われるのはそのためです。
Cocos2d-x でオフスクリーンレンダリングを行うには RenderTexture クラスを使います。RenderTexture オブジェクトの beginWithClear メソッドと end メソッドの間に Sprite オブジェクトの描画処理(visit メソッド)を挟みます。

// オフスクリーンレンダリング用テクスチャ
Size winSize = Director::getInstance()->getWinSize();
RenderTexture *texture = RenderTexture::create(winSize.width, winSize.height);
// レンダリング開始
texture->beginWithClear(0, 0, 0, 0);

Size size = Director::getInstance()->getWinSize();
for (std::string fileName : files) {
  // スプライト生成
  auto sprite = Sprite::create("hoge.png");
  // 上下反転させる
  sprite->setScaleY(-1.0f);
  sprite->setPosition(Vec2(rand()%(int)size.width, rand()%(int)size.height));
  // 書き込み
  sprite->visit();
}

// レンダリング終了
texture->end();

RenderTexture オブジェクトと Sprite オブジェクトになんの関連もないのでこれでいいのか感はありますが、裏で Director オブジェクトがうまいことやってくれてます。あと RenderTexture には begin というメソッドもあるのですがたまにゴミが残って透過画像の描画でおかしくなる時があるので beginWithClear メソッドを使いましょう。Sprite オブジェクトに scale をかけて上下反転させているのは OpenGL のテクスチャ座標が左上原点だからです。

スプライトフレームキャッシュ

画像を一つ一つ個別で描画するよりも、複数の画像をまとめた(アトラス化された)画像を使うことで描画効率が良くなります(ドローコールが減るなど)。Web でいうところの CSS スプライトに近い発想です。Cocos2d-x でアトラス化された画像を扱う場合は SpriteFrameCache クラスを使います。アトラス画像とその画像の中にどのような画像が配置されているかを管理するための plist ファイル(中身はXMLです)を開発者側であらかじめ用意してから SpriteFrameCache オブジェクトに plist ファイルを読み込ませます。そのようにすることでキャッシュからスプライトを生成することができます。プログラム的には以下のようになります。

// plistからキャッシュ生成
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("atlas.plist");
// スプライト生成
auto sprite = Sprite::createWithSpriteFrameName("hoge.png");

アトラス画像と plist ファイルの生成にはいろいろな方法があります。個人的には ShoeBox という無料のアトラス画像制作ツールをおすすめします。

複数の画像を選択して ShoeBox にドラッグするとアトラス化された画像と plist ファイルが生成されます。plist ファイルの中身は以下のようになっています(hoge.png と piyo.png 画像をまとめて atlas.png にまとめた例)。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>frames</key>
    <dict>
      <key>hoge.png</key>
      <dict>
        <key>frame</key>
        <string>{{0,0},{256,256}}</string>
        <key>offset</key>
        <string>{0,0}</string>
        <key>rotated</key>
        <false/>
        <key>sourceColorRect</key>
        <string>{{1,3},{447,173}}</string>
        <key>sourceSize</key>
        <string>{256,256}</string>
      </dict>
      <key>piyo.png</key>
      <dict>
        <key>frame</key>
        <string>{{0,0},{128,128}}</string>
        <key>offset</key>
        <string>{0,0}</string>
        <key>rotated</key>
        <false/>
        <key>sourceColorRect</key>
        <string>{{1,3},{447,173}}</string>
        <key>sourceSize</key>
        <string>{128,128}</string>
      </dict>
    </dict>
    <key>metadata</key>
    <dict>
      <key>format</key>
      <integer>2</integer>
      <key>size</key>
      <string>{512,512}</string>
      <key>textureFileName</key>
      <string>atlas.png</string>
    </dict>
  </dict>
</plist>

plist の形式や中のタグの意味なんかは以下のページを参考にしてください。

動的にアトラス画像を生成する

オフスクリーンレンダリングで生成した RenderTexture オブジェクトからテクスチャを取り出して、それをスプライトフレームキャッシュに追加すると画像を動的にアトラス化できます。

#include "cocos2d.h"

using namespace cocos2d

// ファイル(動的に変わる想定)
std::vector<std::string> files;
files.push_back("hoge.png");
files.push_back("piyo.png");
files.push_back("fuga.png");
files.push_back("foo.png");
files.push_back("bar.png");
files.push_back("buz.png");

// テクスチャの最大サイズ
Size winSize = Director::getInstance()->getWinSize();
int x = 0;
int y = 0;
int maxHeight = 0;

// オフスクリーンレンダリング用テクスチャ
RenderTexture *texture = RenderTexture::create(winSize.width, winSize.height);
texture->beginWithClear(0, 0, 0, 0);

// plist中の画像に関するデータを書き込むためのMap
ValueMap frames;

for (std::string fileName : files) {
  // スプライト生成
  auto sprite = Sprite::create(fileName);
  if (x + sprite->getContentSize().width > winSize.width) {
    y += maxHeight;
    x = 0;
    maxHeight = 0;
  }
  
  // 上下反転するので
  sprite->setScaleY(-1.0f);
  sprite->setAnchorPoint(Vec2(0, 1));

  sprite->setPosition(Vec2(x, y));
  // 書き込み
  sprite->visit();
  Director::getInstance()->getTextureCache()->removeTextureForKey(fileName);

  // plist情報の生成
  ValueMap frame;
  frame["frame"] = StringUtils::format("{{%f, %f}, {%f, %f}}",
                                       sprite->getPosition().x,
                                       sprite->getPosition().y,
                                       sprite->getContentSize().width,
                                       sprite->getContentSize().height);
  frame["offset"] = StringUtils::format("{%d, %d}", 0, 0);
  
  frame["rotated"] = false;
  frame["sourceColorRect"] = StringUtils::format("{{%f, %f}, {%f, %f}}", 0.0f, 0.0f,
                                                 sprite->getContentSize().width,
                                                 sprite->getContentSize().height);
  frame["sourceSize"] = StringUtils::format("{%f, %f}",
                                            sprite->getContentSize().width,
                                            sprite->getContentSize().height);
       
  x += sprite->getContentSize().width;
    
  if (maxHeight < sprite->getContentSize().height ) {
    maxHeight = sprite->getContentSize().height;
  }
  frames[fileName] = frame;
}

// レンダリング終了
texture->end();

// plistのメタ情報を生成
ValueMap plist;
plist["frames"] = frames;
ValueMap metadata;
metadata["format"] = 2; // いろいろ種類があるみたいだけど今回は2で
metadata["size"] = StringUtils::format("{%d, %d}", winSize.width, winSize.height);
metadata["textureFileName"] = "atlas.png";
plist["metadata"] = metadata;

std::string filePath = "atlas.plist";
// plist ファイルを保存
FileUtils::getInstance()->writeToFile(plist, filePath);
// キャッシュを生成
SpriteFrameCache::getInstance()->removeSpriteFramesFromFile(filePath);
SpriteFrameCache::getInstance()->addSpriteFramesWithFile(filePath,
                               texture->getSprite()->getTexture());

// スプライとを使うときはこんな感じ
auto sprite = Sprite::createWithSpriteFrameName("hoge.png");

アトラス画像用の plist ファイルを生成する部分が複雑ですがそれ以外は特に問題ないかと思います。
すべての画像をアトラス化するには画像が多すぎて現実的ではないとか、サーバからの情報で使用する画像が動的に変わるとか、そんな時に使うと有効だと思います。

問題点が...。

今回紹介したオフスクリーンレンダリングの方法では、端末の画面サイズ以上の画像を生成することができません。次の記事でこのあたりを改良してみます。

// こっちじゃなくて
Size winSize = Director::getInstance()->getWinSize();
// こんな感じで画面サイズ以上の領域でも使いたい
GLint glSize;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, & glSize);
const int TEXTURE_SIZE = a_val;