シェーダーで条件分岐は遅いらしいのでベクトル演算に置き換えようとしてできなかった話

一般的にシェーダーでの条件分岐は遅いという話がある

今回作ったシェーダーではブレンドモードや使用するマスクのチャンネルをアプリから送信してシェーダー内で利用するということをした

これを条件分岐を使って素直に実装するとこんな感じになる(空で書いたので動くか不明)

fixed3 blendColor(fixed3 src, fixed4 dest, float blend, int mode) {
    float alpha = dest.a * blend;
    fixed3 blended_dest = dest.rgb * alpha;
    if (mode == 0) {
        return lerp(src, dest, alpha);
    } else if (mode == 1) {
        return src + blended_dest;
    } else if (mode == 2) {
        return src - blended_dest;
    } else if (mode == 3) {
        return src * blended_dest;
    }
    return src;
}

fixed useMask(fixed4 mask, int use_mask_number) {
    if (use_mask_number == 0) {
        return 1.0 - mask.r;
    } else if (use_mask_number == 1) {
        return 1.0 - mask.g;
    } else if (use_mask_number == 2) {
        return 1.0 - mask.b;
    } else if (use_mask_number == 3) {
        return 1.0 - mask.a;
    }
    return 0.0;
}

これの modeuse_mask_number を使用したいチャンネルが1.0のベクトルで渡すことで、if文を行列とベクトル演算に置き換えることができる

fixed3 blendColor(fixed3 src, fixed4 dest, float blend, float4 mode) {
    float alpha = dest.a * blend;
    fixed3 blended_dest = dest.rgb * alpha;
    // GLESでは非正方行列が使用できないので使わないところは0で埋める
    return mul(mode, fixed4x4(lerp(src, dest, alpha), 0,
                                src + blended_dest, 0,
                                src - blended_dest, 0,
                                src * blended_dest, 0)).rgb;
}

fixed useMask(fixed4 mask, float4 use_mask_vector) {
    return 1.0 - dot(mask, use_mask_vector);
}

これで高速化できただろう!と思ってましたが、結局最初に書いたifのパターンの方がパフォーマンスが出てました。

こちらのリンク先のとおりであれば、実行中に実際に分岐されない場合ではifの方がパフォーマンスがでるんじゃないかと思いました。(未検証)

But on modern GPUs, they don't cause a performance issue unless they actually do diverge at runtime.

www.khronos.org

またピクセル毎に分岐するような場合はDivergent分岐と呼ばれるらしくて、こういう時にifのパフォーマンスが悪くなるらしい(uvで偶数奇数を分岐するなど)

ただし、この場合でも算術演算によるブランチングがifと比べて速くなるかは検証していないので不明...

(想像の話しかしてないのでアセンブリ読んでみたいけどどこから参照すればいいのかわからない...)