エンジニア

[Cocos2d-x] RenderTextureを利用してクリッピングマスクする

投稿日:2022年4月21日 更新日:

こんにちは、名古屋スタジオでアプリエンジニアをしている山元と申します。
今回は業務で実装した画面演出について、躓いたところや基本的な実装内容を紹介しようと思います。

クリッピングマスクについて

画面に配置されているターゲットの領域はそのまま表示、それ以外の領域はグレーアウトするような表示を実現したかったので、クリッピングマスク処理を利用しました。
クリッピングマスクは、マスク用テクスチャを使ってクリッピング対象の一部を隠して表示するための処理です。

マスク用テクスチャを用意する方法として「外部の画像を利用する」「DrawNodeで図形を描画して利用する」などが挙げられますが、前者はマスク処理のためだけに用意するのが手間、後者はAndroidだとOpenGL周りの影響で描画が上手くいかない場合があるなどの問題がありました。
そのため別の方法として、RenderTexutreをマスク用テクスチャとして利用する方法を業務では採用しました。

RenderTextureを利用した方法はあまり情報が見つけられなかったこともあり、本記事にメモとして残そうと思います。
外部画像やDrawNodeのように特殊な形のマスク処理には向かないですが、応用次第では色々なことができるのではないかと思います。

確認環境

・macOS Big Sur 11.2.1
・Cocos2d-x 3.2
・Xcode 12.5.1

下準備

まずは以下のような画面を作成します。
画面上に幾つかのターゲットを配置し、これらに対してマスク処理による演出をつけていきたいと思います。

auto size = Director::getInstance()->getWinSize();

//ベースレイヤー
auto baseLayer = LayerColor::create(Color4B::WHITE);
baseLayer->setContentSize(size);
baseLayer->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
baseLayer->ignoreAnchorPointForPosition(false);
baseLayer->setPosition(0, 0);
this->addChild(baseLayer);

//ターゲット1(赤)
auto sampleNode1 = LayerColor::create(Color4B::RED, 100, 100);
sampleNode1->setPosition(Vec2(size.width/4, size.height/4));
sampleNode1->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
sampleNode1->ignoreAnchorPointForPosition(false);
baseLayer->addChild(sampleNode1, 100);

//ターゲット2(青)
auto sampleNode2 = LayerColor::create(Color4B::BLUE, 100, 100);
sampleNode2->setPosition(Vec2(size.width/4, size.height - size.height/4));
sampleNode2->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
sampleNode2->ignoreAnchorPointForPosition(false);
baseLayer->addChild(sampleNode2, 100);

//ターゲット3(緑)
auto sampleNode3 = LayerColor::create(Color4B::GREEN, 100, 100);
sampleNode3->setPosition(Vec2(size.width - size.width/4, size.height/4));
sampleNode3->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
sampleNode3->ignoreAnchorPointForPosition(false);
baseLayer->addChild(sampleNode3, 100);

//ターゲット4(黄)
auto sampleNode4 = LayerColor::create(Color4B::YELLOW, 100, 100);
sampleNode4->setPosition(Vec2(size.width - size.width/4, size.height - size.height/4));
sampleNode4->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
sampleNode4->ignoreAnchorPointForPosition(false);
baseLayer->addChild(sampleNode4, 100);

クリッピングマスク処理

クリッピングするレイヤーの用意

画面全体を覆うグレーアウトレイヤーを作成します。こちらが一部分を隠すマスク処理の影響を受けます。

//グレーアウトレイヤー
auto layerColor = LayerColor::create(Color4B(0, 0, 0, 64));
layerColor->setContentSize(size);
layerColor->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
layerColor->ignoreAnchorPointForPosition(false);
layerColor->setPosition(0, 0);

マスク用テクスチャの作成

RenderTextureを利用して、プログラム上でテクスチャを生成します。

  1. 画面全体を覆うサイズを指定して、RenderTextureを生成
  2. RenderTextureへSpriteを焼き付けることをbeginで宣言
  3. 焼き付けるSprite(マスク処理する領域)を生成
  4. Spriteをretainでメモリ確保
  5. Spriteの位置やサイズを指定 (実装コードでは、ターゲット1の領域が対象)
  6. Spriteに不透明な色を指定する (色は何でもOK) ← 重要
  7. visitでSpriteを焼き付ける
  8. releaseでSpriteのメモリ解放
  9. RederTextureへの焼き付けが終わったらendで終了
//マスク用テクスチャ作成
auto render = RenderTexture::create(size.width, size.height);
render->begin();
//マスクする領域を設定、それ以外の領域は透明
auto invisible = Sprite::create();
invisible->retain();
invisible->setTextureRect(Rect(0.0f ,0.0f ,sampleNode1->getContentSize().width + 30, sampleNode1->getContentSize().height + 30));
invisible->setAnchorPoint(sampleNode1->getAnchorPoint());
invisible->setScale(1.0f);
invisible->setPosition(sampleNode1->getParent()->convertToWorldSpace(sampleNode1->getPosition()));
invisible->setColor(Color3B::ORANGE);
invisible->setOpacity(255);
invisible->visit();
invisible->release();
//マスク用テクスチャ生成処理終了
render->end();

作成したテクスチャでSpriteを生成

マスク処理に利用するClippingNodeへ適用させるため、マスク用テクスチャをSpriteとして生成します。
ここで注意なのが、このままClippingNodeにSpriteをセットすると、マスクする領域が上下反転した状態で表示されてしまいます。
これは、RenderTextureを生成すると原点(0, 0)が左上基準になっていることに起因します。
Cocos2d-xの座標系は原点が左下基準なので、そちらに合わせる処理が必要になります。
実装コードでは、Spriteのy座標を反転させて原点を合わせる処理を入れました。

//マスク用テクスチャをSpriteとして作成
auto stencil = Sprite::createWithTexture(render->getSprite()->getTexture());
stencil->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
stencil->setPosition(0, 0);
stencil->setFlippedY(true);

ClippingNodeの利用

グレーアウトレイヤーにマスク処理を施すためのコードを書いていきます。
はじめにClippingNodeを生成し、マスク用テクスチャをセットします。
次にマスク処理のかかり方を決めるsetInverted(bool)を指定します。
(trueではマスク用テクスチャの透明領域が表示、falseでは不透明領域が表示されます)
次に表示と非表示を分ける閾値をsetAlphaThresholdで指定します。
最後にClippingNodeにグレーアウトレイヤーをaddChildすることで、マスク処理が有効になります。

//クリッピングノード作成
auto clipping = ClippingNode::create();
clipping->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
clipping->setPosition(0, 0);
//マスク用テクスチャをセット
clipping->setStencil(stencil);
//透明部分を表示
clipping->setInverted(true);
//設定した透明度より不透明な部分を非表示
clipping->setAlphaThreshold(0.5);
//グレーアウトレイヤーをクリッピングマスク処理
clipping->addChild(layerColor);
baseLayer->addChild(clipping, 150);

表示結果

以上の内容を踏まえて画面表示すると、グレーアウトレイヤーにマスク処理が施されているのが確認できます。
マスク用テクスチャに設定した領域(ターゲット1)のみ明るく、それ以外の領域はグレーアウトされて表示されています。

ちなみに、マスク用テクスチャを以下のように作成すると、各ターゲット領域にマスク処理が施されます。

//マスク用テクスチャ作成
auto render = RenderTexture::create(size.width, size.height);
render->begin();
//マスクする領域を設定、それ以外の領域は透明
auto invisible1 = Sprite::create();
invisible1->retain();
invisible1->setTextureRect(Rect(0.0f ,0.0f ,sampleNode1->getContentSize().width + 30, sampleNode1->getContentSize().height + 30));
invisible1->setAnchorPoint(sampleNode1->getAnchorPoint());
invisible1->setScale(1.0f);
invisible1->setPosition(sampleNode1->getParent()->convertToWorldSpace(sampleNode1->getPosition()));
invisible1->setColor(Color3B::ORANGE);
invisible1->setOpacity(255);
invisible1->visit();
invisible1->release();

auto invisible2 = Sprite::create();
invisible2->retain();
invisible2->setTextureRect(Rect(0.0f ,0.0f ,sampleNode2->getContentSize().width + 30, sampleNode2->getContentSize().height + 30));
invisible2->setAnchorPoint(sampleNode2->getAnchorPoint());
invisible2->setScale(1.0f);
invisible2->setPosition(sampleNode2->getParent()->convertToWorldSpace(sampleNode2->getPosition()));
invisible2->setColor(Color3B::ORANGE);
invisible2->setOpacity(255);
invisible2->visit();
invisible2->release();

auto invisible3 = Sprite::create();
invisible3->retain();
invisible3->setTextureRect(Rect(0.0f ,0.0f ,sampleNode3->getContentSize().width + 30, sampleNode3->getContentSize().height + 30));
invisible3->setAnchorPoint(sampleNode3->getAnchorPoint());
invisible3->setScale(1.0f);
invisible3->setPosition(sampleNode3->getParent()->convertToWorldSpace(sampleNode3->getPosition()));
invisible3->setColor(Color3B::ORANGE);
invisible3->setOpacity(255);
invisible3->visit();
invisible3->release();

auto invisible4 = Sprite::create();
invisible4->retain();
invisible4->setTextureRect(Rect(0.0f ,0.0f ,sampleNode4->getContentSize().width + 30, sampleNode4->getContentSize().height + 30));
invisible4->setAnchorPoint(sampleNode4->getAnchorPoint());
invisible4->setScale(1.0f);
invisible4->setPosition(sampleNode4->getParent()->convertToWorldSpace(sampleNode4->getPosition()));
invisible4->setColor(Color3B::ORANGE);
invisible4->setOpacity(255);
invisible4->visit();
invisible4->release();

//マスク用テクスチャ生成処理終了
render->end();

応用

もっとマスクする領域を増やして表示すると格子状の模様が入った画面を作成できます。

//画面を覆う黒いレイヤー
auto layerColor = LayerColor::create(Color4B::BLACK);
layerColor->setContentSize(size);
layerColor->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
layerColor->ignoreAnchorPointForPosition(false);
layerColor->setPosition(0, 0);

//マスク用テクスチャ作成
auto render = RenderTexture::create(size.width, size.height);
render->begin();
//マスクする領域を設定、それ以外の領域は透明
auto maskWidth = 80;
auto maskHeight = 80;
for (int i = 0; i < size.width / maskWidth; i++)
{
    for (int j = 0; j < size.height / maskHeight; j++)
    {
        auto invisible = Sprite::create();
        invisible->retain();
        invisible->setTextureRect(Rect(0.0f ,0.0f ,maskWidth, maskHeight));
        invisible->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
        invisible->setScale(1.0f);
        invisible->setPosition(i * (maskWidth + 40), j * (maskHeight + 40));
        invisible->setColor(Color3B::ORANGE);
        invisible->setOpacity(255);
        invisible->visit();
        invisible->release();
    }
}
//マスク用テクスチャ生成処理終了
render->end();

おわりに

以上、RenderTextureを利用したクリッピングマスク処理の紹介でした。
他の応用例として、ClippingNodeにrunActionをかけて回転させたり点滅させたりしても面白いかも知れません。
こちらを参考にして、画面演出のお役に立てれば幸いです。

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-

© WonderPlanet Inc.