アルファマップを使うとマスクしたい箇所を画像によって自由に指定できるのがメリットです。
ですが場合によってはアルファマップとなるマスク画像が大きくなってしまい容量が圧迫されてしまうことがあります。そのためリソースに含めたく無いこともあるかと思います。
例えば、画像の端をちょっとマスクしたいだけなんだけど、といった場合です。
そんなときはマスク画像を使わず、パラメータを使ってマスクしたい箇所を計算してしまうという方法があります。
画像の端だけマスクさせたい場合
アルファマップはその名の通り、どこのピクセルをどのアルファ値にする、といった情報が書き込まれているアルファ値の配置図です。
前回はその配置図からアルファ値を取り出して合成しました。
今回はそれを使わず、シェーダにパラメータだけを渡してアルファ値を計算するようにします。
画像の端だけ、という条件であれば範囲を指定するのは簡単です。
ソースコードの一部:
…
do
{
auto spr = Sprite::create("prince.png");
CC_BREAK_IF(!spr);
this->addChild(spr);
auto program = GLProgram::createWithFilenames("shader/vert.vsh", "shader/mask.fsh");
CC_BREAK_IF(!program);
auto glProgramState = GLProgramState::getOrCreateWithGLProgram(program);
CC_BREAK_IF(!glProgramState);
glProgramState->setUniformFloat("u_range", 0.4f);
glProgramState->setUniformFloat("u_margin", 0.3f);
spr->setGLProgramState(glProgramState);
} while(0);
…
シェーダ:
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform float u_range;
uniform float u_margin;
float calc_alpha(float st, float ed, float tex_y)
{
float val = 1.0 - (tex_y - st) / (ed - st);
return clamp(val, 0.0, 1.0);
}
void main()
{
vec4 texColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
float topA = calc_alpha(u_range, u_margin, v_texCoord.y);
float bottomA = calc_alpha(1.0 - u_range, 1.0 - u_margin, v_texCoord.y);
texColor = vec4(texColor.x, texColor.y, texColor.z, texColor.w * topA * bottomA);
gl_FragColor = texColor;
}

左側が元画像、右側が適用後の画像です。
rangeとmarginはこんな感じです。

パラメータとしてrangeとmarginを渡してcalc_alphaでアルファ値を計算します。
画像の上下の端が指定したrangeから徐々に消えてゆき、margin以降は透明になります。
スクロールリストの端のマスク
RenderTextureと組み合わせればもっといろいろな使い方ができます。
例えば、スクロールリストの端とか。

ソースコードの一部:
void HelloWorldLayer::createTableView()
{
Size visibleSize = Director::getInstance()->getVisibleSize();
_listboxView = ListboxView::create(Size(visibleSize.width, visibleSize.height));
_listboxView->setVisible(false);
this->addChild(_listboxView);
this->setupRenderTexture();
}
void HelloWorldLayer::setupRenderTexture()
{
auto visibleSize = Director::getInstance()->getVisibleSize();
auto contentSize = _listboxView->getViewSize();
auto renderTex = _renderTex = RenderTexture::create(static_cast(contentSize.width), static_cast (contentSize.height));
renderTex->setPosition(contentSize.width * 0.5f, contentSize.height * 0.5f);
this->addChild(renderTex);
do
{
auto program = GLProgram::createWithFilenames("shader/vert.vsh", "shader/mask.fsh");
CC_BREAK_IF(!program);
auto glProgramState = GLProgramState::getOrCreateWithGLProgram(program);
CC_BREAK_IF(!glProgramState);
glProgramState->setUniformFloat("u_range", 0.03f);
glProgramState->setUniformFloat("u_margin", 0.0f);
_renderTex->getSprite()->setGLProgramState(glProgramState);
} while(0);
auto listener = EventListenerTouchOneByOne::create();
listener->setSwallowTouches(true);
listener->onTouchBegan = [this](Touch* touch, Event* event)
{
_listboxView->setVisible(true);
bool ret = _listboxView->_tableView->onTouchBegan(touch, event);
_listboxView->setVisible(false);
return ret;
};
listener->onTouchMoved = [this](Touch* touch, Event* event)
{
_listboxView->setVisible(true);
_listboxView->_tableView->onTouchMoved(touch, event);
_listboxView->setVisible(false);
};
listener->onTouchEnded = [this](Touch* touch, Event* event)
{
_listboxView->setVisible(true);
_listboxView->_tableView->onTouchEnded(touch, event);
_listboxView->setVisible(false);
};
listener->onTouchCancelled = [this](Touch* touch, Event* event)
{
_listboxView->setVisible(true);
_listboxView->_tableView->onTouchCancelled(touch, event);
_listboxView->setVisible(false);
};
this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, _renderTex);
}
void HelloWorldLayer::visit(cocos2d::Renderer *renderer, const cocos2d::Mat4& parentTransform, uint32_t parentFlags)
{
if (_renderTex)
{
_renderTex->beginWithClear(0.0f, 0.0f, 0.0f, 0.0f);
{
_listboxView->setVisible(true);
_listboxView->visit();
_listboxView->setVisible(false);
}
_renderTex->end();
}
Layer::visit(renderer, parentTransform, parentFlags);
}
シェーダ:
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform float u_range;
uniform float u_margin;
float calc_alpha(float st, float ed, float tex_y)
{
float val = 1.0 - (tex_y - st) / (ed - st);
return clamp(val, 0.0, 1.0);
}
void main()
{
vec4 texColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
float topA = calc_alpha(u_range, u_margin, v_texCoord.y);
float bottomA = calc_alpha(1.0 - u_range, 1.0 - u_margin, v_texCoord.y);
texColor = texColor.rgba * topA * bottomA; // ※1
gl_FragColor = texColor;
}
※1:RenderTextureとして使うので、texColorの計算式が前節に書いたものと少し変わっているのに注意してください。

range=0.03,margin=0.0です。
ListboxViewはTableViewDataSourceとTableViewDelegateを継承した独自のスクロールリストのクラスです。
スクロールリストをまるごとRenderTextureに書き込みます。
そのRenderTextureに対してシェーダを適用すればいいわけです。
スクロールリストはオフスクリーンでRenderTextureに書き込むため非表示にしておきます。
そのためタッチイベントが受け取れません。
タッチイベントを受け取るために、RenderTextureにイベントリスナを加えます。
それを非表示にしているスクロールリストに渡します。
タッチイベントを渡すときと、オフスクリーンで描画するときだけ表示させます。
このようにすることで元のスクロールリストと同じ挙動をさせることができます。
まとめ
今回はシェーダにパラメータを渡してマスク処理を行う方法を説明しました。
また、RenderTextureを組み合わせることで、スクロールリスト丸ごとに対しマスクをかける方法も紹介しました。
他にも円形のパラメータを与えてスポットライト風にするなど様々な表現ができると思いますのでみなさんも試してみてください。
画像端やスクロールリストの上下を徐々にマスクさせることができるとデザイン的に以下のようなメリットがあります。
・リスト表示の美しさ(デザインに対してしっかり作り込んでいる感)
・ふわっと消すことで柔らかさや繊細さを表現し、ユーザーによりゲームの世界観に浸ってもらえる
・UIとしてはユーザーにデザインの欠点を意識させないというのが一番大事なことなので、それらの解消につながる
UIチームとシステムチームが連携して表現の検討をし、実現していくことも大切ですね。
各チームそれぞれでがんばるのではなく、チーム一丸となってゲームのクオリティアップに挑んでいきましょう。