#ソフトウェアラスタライザー基本実装編 09.ミップマッピング(とトライリニアフィルタリング)
前回はバイリニアフィルタリングでテクスチャの補間を行いました。
それだけでもそれなりに綺麗なのですが遠くのポリゴンなどサンプリングするテクセルが隣接位置よりも広い場合には情報が欠落してしまいます。
そこで隣接するピクセルのuvの差から適切な画像サイズを求めて無駄を減らすための処理がミップマッピングになります。
##リポジトリ
https://github.com/NoriyukiHiromoto/Rendering01_SoftwareRasterizer/tree/01_09_mip_mapping
今回の実行結果になります。
遠くのポリゴンの滑らかさが向上しています。
画質的には微妙なところですがメモリアクセスが大幅に削減できるため速度面でもメリットが高い方法です。
##やりかた
今まではピクセルを打つときにそこでの(u, v)をつかってテクスチャをサンプリングしていましたが、今回は適切な画像サイズを探すために隣接するピクセルの(u, v)から差分を求めて、それを用いてサンプリングするテクスチャのミップレベルを求めます。
##実装
ラスタライズ処理を見てみます。
1ピクセル前のUVを保存しておき、その値で現在のUVとの差分を求めています。
本当は2x2のラスタライザーをちゃんと作って求めるべきなのですが今回は割と雑に1ピクセル前の値から求めています。
ただし最初のピクセルでは求められないためにミップマップ処理をなしにしてサンプリングします。
// 求めたUVからテクスチャの色をとってくる
auto pTexture = _Textures[TextureId];
Color texel = bFirstSampling
? pTexture->Sample(TexCoord.x, TexCoord.y)
: pTexture->Sample(TexCoord.x, TexCoord.y, LastU - TexCoord.x, LastV - TexCoord.y);
bFirstSampling = false;
LastU = TexCoord.x;
LastV = TexCoord.y;
次はテクスチャのサンプリング部分を見てみます。
UVの差分から実際のテクスチャサイズのドットの差分を求めてミップレベルを決定します。
Color Texture::Sample(fp32 u, fp32 v, fp32 du, fp32 dv) const
{
u += 256.0f;
v += 256.0f;
const auto ddx = du * fp32(_Width);
const auto ddy = dv * fp32(_Height);
const auto ddxy = std::max(abs(ddx), abs(ddy));
const auto level = int32(log2f(ddxy));
const auto& Src = _Surface[std::min(std::max(0, level), _SurfaceCount - 1)];
const auto uf = u * fp32(Src.Width);
const auto vf = v * fp32(Src.Height);
const auto ui0 = int32(uf);
const auto vi0 = int32(vf);
const auto mui0 = ui0 % Src.Width;
const auto mui1 = (ui0 + 1) % Src.Width;
const auto mvi0 = vi0 % Src.Height;
const auto mvi1 = (vi0 + 1) % Src.Height;
const auto x0y0 = Src.Color[mui0 + (mvi0 * Src.Width)];
const auto x1y0 = Src.Color[mui1 + (mvi0 * Src.Width)];
const auto x0y1 = Src.Color[mui0 + (mvi1 * Src.Width)];
const auto x1y1 = Src.Color[mui1 + (mvi1 * Src.Width)];
return Color::Lerp(x0y0, x1y0, x0y1, x1y1, uf - fp32(ui0), vf - fp32(vi0));
}
スクリーンショットはこれをもとにミップレベルごとに色を付けてどの程度ミップマップが聞いているかを確認できるようにしたものです。
こんな感じでミップレベルが切り替わっているのが確認できます。
何となく正しい結果になっているように見えます。
ラスタライズの開始位置が必ずミップレベル0になってしまうので遠いく行くほど線が見えてしまいますね。
このままだとミップレベルが高くポリゴン密度が高い個所ではノイズになって見えてしまいます。
この辺は2x2ピクセルずつの処理をするなどの対応をちゃんとするべきなのですが面倒くさいのでとりあえずスルーします。
2x2でクアッドレンダリング出来ると正確に面積からミップレベルを求められるのでもっと綺麗になるんじゃないでしょうか。
(少なくともDirectX9のddx,ddy系の命令も2x2の単位でしか取れなかった気がするので、ハードウェアでの実装も同じような都合なのかもしれません)
この辺をまじめにやろうとするとSIMD化して深度テストや内外判定を並列化しないと厳しいと思います。
##トライリニアフィルタリング
単純にミップマップ処理を入れた際の欠点としてはレベルの切り替え部分の境界線が見えるというものがあります。
ある深度から解像度が半分になるわけですから結構見てわかります。
そこでもう一歩踏み込んだ処理として、ミップマップをさらにレベルごとにブレンドしてバイリニアフィルタリングと合わせて3次元ブレンドするトライリニアフィルタリングというものがあります。
左のスクリーンショットは中央付近で解像度が変わっているのが見えるかと思います。
これを右のように切り替わりのグラデーションをいれるのがトライリニアフィルタリングになります。
具体的な実装は以下のような感じになります。
Color Texture::Sample(fp32 u, fp32 v, fp32 du, fp32 dv) const
{
u += 256.0f;
v += 256.0f;
auto BilinearFiltering = [](const Surface& Src, fp32 u, fp32 v) {
const auto uf = u * fp32(Src.Width);
const auto vf = v * fp32(Src.Height);
const auto ui0 = int32(uf);
const auto vi0 = int32(vf);
const auto mui0 = ui0 % Src.Width;
const auto mui1 = (ui0 + 1) % Src.Width;
const auto mvi0 = vi0 % Src.Height;
const auto mvi1 = (vi0 + 1) % Src.Height;
const auto x0y0 = Src.Color[mui0 + (mvi0 * Src.Width)];
const auto x1y0 = Src.Color[mui1 + (mvi0 * Src.Width)];
const auto x0y1 = Src.Color[mui0 + (mvi1 * Src.Width)];
const auto x1y1 = Src.Color[mui1 + (mvi1 * Src.Width)];
return Color::Lerp(x0y0, x1y0, x0y1, x1y1, uf - fp32(ui0), vf - fp32(vi0));
};
// 基準になるミップレベルを求める
const auto ddx = du * fp32(_Width);
const auto ddy = dv * fp32(_Height);
const auto ddxy = std::max(abs(ddx), abs(ddy));
const auto level_base = std::max(log2f(ddxy), 0.0f);
// 小数部分からブレンド率を求める
const auto level_rate = level_base - floorf(level_base);
// 基準のレベルとブレンド対象のレベルを求める
const auto max_mip_level = _SurfaceCount - 1;
const auto levelA = std::min(max_mip_level, (int32)level_base);
const auto levelB = std::min(max_mip_level, levelA + 1);
// 2つのレベルのバイリニアフィルタリングの結果をさらにブレンド
const auto& SrcA = _Surface[levelA];
const auto& SrcB = _Surface[levelB];
return Color::Lerp(
BilinearFiltering(SrcA, u, v),
BilinearFiltering(SrcB, u, v),
level_rate);
}
まずはミップレベルを求めて、求めたミップレベル+1とブレンドしています。
この際ブレンド率はlog2()で求めた値の小数部分を使います。
ここまでの処理が入った状態のものが冒頭のスクリーンショットになります。
#最後に
今回はミップマッピングを行いました。
距離に応じて適切なサイズのテクスチャを選択することで品質の上昇と無駄なメモリアクセスを減らすことが出来ます。
さらに高品質化をすることが出来る異方性フィルタリングという方法もありますが、具体的な実装方法があまり出てこないので誰か教えてください。