[cocos2dx] RenderTexture + setScissorInPointsのトラップ

 どうもみなさんこんばんは、クリスマスはいかがお過ごしでしたか?僕はぼっちでした。次は年越し&お正月ですね。僕はぼっちです。さて、本日はcocos2dxの中でも特に地雷原として有名なRenderTextureのトラップとその対処法をまとめておきたいと思います。

setScissorInPointsは実装が超適当

 こんな感じの実装になっています。

void GLViewProtocol::setScissorInPoints(float x , float y , float w , float h)
{
     glScissor((GLint)(x * _scaleX + _viewPortRect.origin.x),
              (GLint)(y * _scaleY + _viewPortRect.origin.y),
              (GLsizei)(w * _scaleX),
              (GLsizei)(h * _scaleY));
}

 この中で使用されている_scaleXと_scaleYは、updateDesignResolutionSizeの中で計算されています。大雑把に言えば、スクリーンサイズとDesignResolutionSize(AppDelegateとかで指定するやつ)の比率として実装されています。

 glScissorはピクセルシェーダが走った後に適用されます。もちろんビューポート変換の前です。つまり、DesignResolutionSizeとビューポートのサイズに差異があると、正しくクリッピングされません。setScissorInPointsは、ビューポート変換後に呼び出されるような想定で実装されている予感がします。

問題点

 問題は、DesignResolutionSizeとビューポートのサイズに差異が発生することと、RenderTexture生成時のサイズがDesignResolutionSizeをベースに決まる(ようにコードを書くことが多い)ことの2点です。普通にsetScissorInPointsを使う分には問題ありません。RenderTextureを用いてスクリーンショットを撮る(画面全体をFBOに転送する)際、以下のようにしてRenderTextureを利用することが多いかと思います。

const Size &size = Director::getInstance()->getWinSize();
RenderTexture *rt = RenderTexture::create(size.width, size.height);
// いろいろ…

 このDirector::getWinSizeが返却する値はDesignResolutionSizeから計算される値です。つまり、この方法でRenderTextureを作ってしまうと、実際のビューポートのサイズとRenderTextureの(仮想的な)ビューポートのサイズに差異が発生してしまい、setScissorInPointsを利用した時のクリッピング領域がずれてしまう、というわけです。言葉じゃ伝わりにくいのでちょっと図にしてみましょう(クリックすると大きくなります)。

 赤い四角形がDesignResolutionSizeとして指定した領域で、上の緑色のほうが4インチのiPhoneでの処理の流れ、下の青色の方が手元のXperia Zでの処理の流れです。黒い枠はglScissor(setScissorInPoints)でクリッピングしたい領域です。iPhoneの例のように、DesignResolutionSizeとの差異がない(ResolutionPolicy::FIXED_WIDTHを指定しているので、上下に黒帯が出ます)ため、特に問題なくクリッピングされますが、解像度が無駄に高いXperiaでは、DesignResolutionSizeと実際のスクリーンサイズが大きく異なっているため、RenderTextureへのレンダリングの際にクリッピング領域が狂い、RenderTextureを実際に描画する際に再度ビューポート変換でクリッピング領域が拡大されてしまいます。

解決方法

 上記の通り、原因はRenderTextureがビューポートを変更してしまうことにあります。もっとも、変更しないとうまく描画できないので仕方ないのですが、問題はsetScissorInPointsでのクリッピング領域の計算にキャッシュされた比率を用いて計算してしまっていることです。したがって、setScissorInPointsとその周辺の関数を以下のように変更します。

void GLViewProtocol::setScissorInPoints(float x , float y , float w , float h)
{
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);

    const float scaleX = viewport[2] / _designResolutionSize.width;
    const float scaleY = viewport[3] / _designResolutionSize.height;

    glScissor((GLint)(x * scaleX + _viewPortRect.origin.x),
              (GLint)(y * scaleY + _viewPortRect.origin.y),
              (GLsizei)(w * scaleX),
              (GLsizei)(h * scaleY));
}

 ついでに取得する方も

Rect GLViewProtocol::getScissorRect() const
{
    GLint viewport[4];
    GLfloat params[4];
    glGetIntegerv(GL_VIEWPORT, viewport);

    const float scaleX = viewport[2] / _designResolutionSize.width;
    const float scaleY = viewport[3] / _designResolutionSize.height;

    glGetFloatv(GL_SCISSOR_BOX, params);
    float x = (params[0] - _viewPortRect.origin.x) / scaleX;
    float y = (params[1] - _viewPortRect.origin.y) / scaleY;
    float w = params[2] / scaleX;
    float h = params[3] / scaleY;
    return Rect(x, y, w, h);
}

 こんな感じにします。cocos2dx 3.3ではGLViewProtocolではなくGLViewに該当メソッドが存在します。これでRenderTextureを使っても問題なく動作すると思います。

 普通にバグだと思うのでプルリク投げてもいいような気はしますが、面倒なので気がものすごく向いた時にやるか誰かが投げてくれるのを待ちましょう。

まとめ

 cocos2dxにはトラップがいっぱい!

0 件のコメント :

コメントを投稿