ソフトウェアラスタライザー 最適化編02 遅延レンダリングを使った最適化
リポジトリ
今回の対応で FPS 5.9 ⇒ 8.2 に上昇しました。
最初に
今回は遅延レンダリングを使った最適化を行います。
遅延レンダリングとは
ディファードシェーディングと呼ばれているものになりますが、今回はそれを最適化目的で利用します。
内容としてはPowerVRなどで実装されているTBDRのDR部分になります。
ディファードシェーディングではラスタライズ時にシェーディングを行わずに、シェーディングを行うためのパラメーターをバッファに保存しておきます。
そして、最後にまとめて画面全体をシェーディングするといった手順を踏みます。
これがなぜ最適化につながるのかというと、最も重い処理であるシェーディングの絶対数を減らすことが出来るからです。
一般的にシーンが複雑になるほどポリゴンの重なりが発生するようになります。
ポリゴンがいくつも重なっているピクセルではその回数分のシェーディング処理が行われてしまいますが、最終的な絵に影響するのは其のうち一番手前の1回分だけです。
これをいかに減らすかというのが処理速度に影響してきます。
そのためZ-prepassや手前から描画などといったZバッファを利用してシェーディングするピクセル数を減らすといった手法がよくとられます。(UnityなどのゲームエンジンもOpacityは手前から描画しているのは同じ理由です)
今回はGBufferを使って最終的にポスト処理でシェーディングをすることで、どんなに多くても画面の解像度分のシェーディングしか行わないようになります。
GBufferの作成処理
まずはシェーディング情報を記録しておくためのGBufferを用意します。
struct GBufferData
{
uint16 TextureId;
uint16 TriangleId;
Vector3 Normal;
Vector2 TexCoord;
};
typedef FrameBuffer<GBufferData> GBuffer;
こんな感じです。
ラスタライズ部分ではシェーディングをする代わりにパラメーターをGBufferへ保存します。
for (auto x = x0; x <= x1; ++x, px += 1.0f, ++x_offset)
{
auto b0 = EdgeFunc(p1.x, p1.y, p2.x, p2.y, px, py);
if (b0 < 0.0f) if (bRasterized) break; else continue;
auto b1 = EdgeFunc(p2.x, p2.y, p0.x, p0.y, px, py);
if (b1 < 0.0f) if (bRasterized) break; else continue;
auto b2 = EdgeFunc(p0.x, p0.y, p1.x, p1.y, px, py);
if (b2 < 0.0f) if (bRasterized) break; else continue;
bRasterized = true;
const auto Depth = (b0 * p0.z) + (b1 * p1.z) + (b2 * p2.z);
auto& DepthBuf = pDepthBuffer[x_offset];
if (Depth >= DepthBuf) continue;
DepthBuf = Depth;
b0 *= InvDenom;
b1 *= InvDenom;
b2 *= InvDenom;
const auto w = 1.0f / ((b0 * p0.w) + (b1 * p1.w) + (b2 * p2.w));
auto& GBuff = pGBuffer[x_offset];
GBuff.TextureId = TextureId;
GBuff.TriangleId = TriangleId;
GBuff.Normal.x = ((b0 * n0.x) + (b1 * n1.x) + (b2 * n2.x)) * w;
GBuff.Normal.y = ((b0 * n0.y) + (b1 * n1.y) + (b2 * n2.y)) * w;
GBuff.Normal.z = ((b0 * n0.z) + (b1 * n1.z) + (b2 * n2.z)) * w;
GBuff.TexCoord.x = ((b0 * t0.x) + (b1 * t1.x) + (b2 * t2.x)) * w;
GBuff.TexCoord.y = ((b0 * t0.y) + (b1 * t1.y) + (b2 * t2.y)) * w;
}
GBufferを使ったシェーディング処理
次に生成したGBufferを参照してシェーディングを行います。
void Renderer::DeferredShading(int32 x, int32 y, int32 w, int32 h)
{
auto pGPixel = _pGBuffer->GetPixelPointer(x, y);
auto pColorBuffer = _pColorBuffer->GetPixelPointer(x, y);
fp32 LastU = 0.0f, LastV = 0.0f;
uint16 TriangleId = 0xFFFF;
const int32 count = w * h;
for (int32 i = 0; i < count; ++i, ++pGPixel, ++pColorBuffer)
{
const auto GBuff = *pGPixel;
auto bChangedTriangle = TriangleId - GBuff.TriangleId;
TriangleId = GBuff.TriangleId;
if (GBuff.TextureId == 0xFFFF) continue;
Vector3 Normal;
Vector_Normalize(Normal, GBuff.Normal);
const auto NdotL = Vector_DotProduct(Normal, _DirectionalLight) * 0.25f + 0.75f;
auto pTexture = _Textures[GBuff.TextureId];
Color texel = bChangedTriangle
? pTexture->Sample(GBuff.TexCoord.x, GBuff.TexCoord.y)
: pTexture->Sample(GBuff.TexCoord.x, GBuff.TexCoord.y, LastU - GBuff.TexCoord.x, LastV - GBuff.TexCoord.y);
LastU = GBuff.TexCoord.x;
LastV = GBuff.TexCoord.y;
const auto Brightness = uint32(NdotL * 128.0f);
pColorBuffer->r = (texel.r * Brightness) >> 7;
pColorBuffer->g = (texel.g * Brightness) >> 7;
pColorBuffer->b = (texel.b * Brightness) >> 7;
}
}
シェーディングの処理自体は前回までと大きな違いはありません。
パラメーターがラスタライザーから補間して求めた値からGBufferに保存されている値に代わっているくらいです。
ディファードシェーディングの欠点
ディファードシェーディングにはよくある難点として以下の2つがあります。
1つは事前にGBufferにパラメーターを記載する必要があるので半透明処理をすることが出来ません。
半透明を扱いたい場合は完全に別処理として通常のレンダリングを半透明用に走らせる必要があります。
2つめはdiscard(clip)命令を使ったピクセル破棄のコストが高い事です。
これは実際にiPhoneなんかに搭載されているPowerVR系でもよく知られている事でGBufferにパラメーターを描画する際にテクスチャのアルファ値をみてピクセルの破棄をするという事は、その時点でシェーディングをしないといけません。
今回の実装でいえばシェーディングを減らすためにディファード化したのにGBufferへの書き込みのチェックのためにシェーディングを行うという本末転倒な状態になります。
これがPowerVR系でdiscard命令が重いといわれる理由ではないかと思います。
最後に
今回はディファードシェーディングを行う事で負荷を軽減する最適化を行いました。
そこまで複雑なシーンではないですがシェーディング処理は基本的に負荷の高い処理なのでそれなりの効果がありました。
特にテクスチャのサンプリングは負荷が高くなりがちなのでここを如何にして減らすかがレンダラーとしては重要かと思います。