【ゲーム制作】テクスチャフィルタリングとは?|ポイント・バイリニア・トライリニア・異方性を正解画像との差とGPU時間で比べる

【ゲーム制作】テクスチャフィルタリングとは?|ポイント・バイリニア・トライリニア・異方性を正解画像との差とGPU時間で比べる ゲーム制作

ポイント、バイリニア、トライリニア、異方性。名前は聞くけれど、結局どれを選べばよいのか。異方性フィルタリングを16倍にすれば、いちばん綺麗になるのか。そして、フィルタリングを上げると、どのくらい重くなるのか。

この記事では、テクスチャを画面に貼るときのフィルタリング(サンプラーの設定)を、8通りの設定で並べて比べます。「見た目の印象」だけではなく、正解に近い画像との差(RMSE)と、GPUの描画時間を実測して、「どれがどれだけ良いのか、どれだけ重いのか」を数字にしました。ミップマップの作り方そのものは、別記事のミップマップとは?で扱っているので、今回は「サンプラー側でどう読むか」に絞ります。

テクスチャフィルタリングとは?

テクスチャフィルタリングとは、

・画面の1ピクセルが、テクスチャのどこをどう覗くかに応じて、テクスチャの複数の画素を混ぜて1つの色にする方法

です。画面のピクセルとテクスチャの画素(テクセル)は、1対1に対応しません。近くではテクスチャが引き伸ばされ、遠くではぎゅっと縮みます。そのときの色の決め方が、フィルタリングです。

  • ポイント:一番近いテクセル1つをそのまま使う。混ぜない
  • バイリニア:まわりの4テクセルを、距離に応じて混ぜる
  • トライリニア:バイリニアを、隣り合う2つのミップマップの階層でそれぞれ行い、さらに2階層を混ぜる
  • 異方性:床のように斜めに見る面では、1ピクセルが覗くテクスチャの範囲は「細長い楕円」になる。その細長い方向に沿って、複数回サンプリングする(最大2〜16回)

ミップマップは、テクスチャを1/2、1/4…と縮めた「縮小版」を用意しておく仕組みです。トライリニアと異方性は、これがないと本来の力を発揮できません。この点は、あとで数字で見ます。

実験:64枚平均の「正解」と比べる

フィルタリングの良し悪しは、見た目では判断しにくいものです。そこで、次の方法で「正解画像」を作りました。

  • 一番細かい階層(1024×1024)だけを、ポイントサンプリングで描く
  • 射影行列をサブピクセル単位で8×8=64通りにずらして、64枚を描く
  • 64枚を平均する。1ピクセルの中を64点でサンプリングした「ほぼ正解」の画像になる

あとは、各方式の画像と正解画像とのRMSE(二乗平均平方根誤差、0〜255)を、床までの距離の帯ごとに測ります。数字が小さいほど、正解に近いことになります。

テストするテクスチャは、模様の細かさが違う4つの領域(32px市松、8px市松、2px市松、1px縞)を持つ1024×1024の画像です。ミップマップはDirectXTexで作りました。

▼FilterDemo.cpp
if (withMips) {
    ScratchImage chain;
    hr = GenerateMipMaps(*img, TEX_FILTER_DEFAULT, 0, chain);
    if (FAILED(hr)) return false;
    hr = CreateShaderResourceView(GetDevice(), chain.GetImages(), chain.GetImageCount(),
                                  chain.GetMetadata(), &srv);
} else {
    hr = CreateShaderResourceView(GetDevice(), base.GetImages(), 1, base.GetMetadata(), &srv);
}

サンプラーは、方式ごとに次のように作り替えています。ここが今回の主役です。

▼FilterDemo.cpp
switch (mode) {
case 0: sd.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;       sd.MaxLOD = 0; break;
case 1: sd.Filter = D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT; sd.MaxLOD = 0; break;
case 2: sd.Filter = D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT; break;
case 3: sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;       break;
default: {
    static const UINT levels[4] = { 2, 4, 8, 16 };
    sd.Filter = D3D11_FILTER_ANISOTROPIC;
    sd.MaxAnisotropy = (aniso > 0) ? (UINT)aniso : levels[(mode - 4) & 3];
    break;
}
}

注目してほしいのは、ポイント(0)とバイリニア(1)でMaxLOD = 0にしている点です。ミップマップを持っているテクスチャでも、最も細かい階層しか使わせない設定にして、「ミップなし」の状態を再現しています。

カメラの高さ1.2m、見下ろし3度で、床を遠くまで見渡す構図です。テクスチャは4mごとに1枚繰り返します。

結果1:見た目

8種類のフィルタリングで描いた遠くの床の拡大比較。ポイントはノイズ、ミップはグレーに霞み、異方性は模様が奥まで残る
床の一部を切り出した8方式の比較(左上から右下へ、0〜7)

ポイントとバイリニア(ミップなし)は、遠くがノイズの塊です。動かすとチラチラ光ります。ミップを使う3つ(最寄り、トライリニア、異方性)は、遠くが自然なグレーに落ち着きます。

異方性フィルタリングは、ミップだけの方式より奥まで模様が残ります。トライリニアでは、少し遠くの2pxの市松が、早めにグレーに霞んでしまいます。異方性ではそれが、細長い方向のサンプルで粘り強く保たれています。

もう一つ、地味な違いも見つけました。「ミップは最寄り」(左段の2番目)では、ミップの階層の切り替わりに、くっきりした境界線が見えます。トライリニアは、この境目を2階層の混ぜ合わせでなだらかにしています。

結果2:正解との差

フィルタリング方式ごとの、64枚平均の正解画像とのズレを距離帯別に示したグラフ
距離帯ごとのRMSE。小さいほど、正解画像に近い
方式2〜5m5〜12m12〜30m30〜80m80〜200m
ポイント64.881.697.697.687.0
バイリニア(ミップなし)37.464.280.578.962.5
ミップは最寄り18.529.525.920.014.7
トライリニア17.528.725.920.014.7
異方性 x213.319.924.920.014.7
異方性 x410.014.922.620.014.7
異方性 x89.613.320.619.914.7
異方性 x169.613.520.019.614.7
正解画像とのRMSE(0〜255)。小さいほど良い。各帯の行数は、225・87・38・15・6行
  • ミップの有無が、圧倒的に大きい。ミップなしのバイリニアは、12〜30mで80.5と、ミップありのトライリニア(25.9)の約3倍ズレている
  • 異方性は、近〜中距離で効く。5〜12mでは、トライリニアの28.7が、x4で14.9、x16で13.5まで減った
  • x8とx16の差は、ほとんどない。2〜5mは9.6と9.6、5〜12mは13.3と13.5
  • 30m以遠は、ミップを使う方式はどれも同じ(20前後と14.7)。ここは細かい縞が平均されて、灰色になるためと考えている

30m以遠でどの方式も同じ値になるのは、異方性が効かなくなったからというより、この距離では模様が完全にグレーに潰れていて、差の付けようがないからだと考えています。なお、正解画像も64サンプルの有限個の平均なので、RMSEにはその分のノイズが含まれています(この点は確かめていません)。

結果3:GPU時間

フィルタリング方式ごとのGPU描画時間の比較グラフ
1280×720の床を1回描く時間(RTX 3050 Laptop、30回繰り返して3回測った平均)
方式1回の描画(マイクロ秒)トライリニアとの比
ポイント(ミップなし)19.71.8倍
バイリニア(ミップなし)25.72.4倍
ミップは最寄り10.71.0倍
トライリニア10.71.0倍
異方性 x213.41.3倍
異方性 x426.22.4倍
異方性 x842.84.0倍
異方性 x1655.95.2倍
値は環境に依存するので、比を見るのが目安。各方式3回の測定はほぼ同じ値だった

意外だったのは、ミップありのほうが、ミップなしより速いことです。ミップなしのバイリニアは、遠くでテクスチャの離れた場所を読みに行くため、キャッシュが効きにくいのだと考えています(原因までは調べていません)が、トライリニアの2.4倍かかりました。「ミップを使うと重くなる」というのは誤解で、速くて綺麗が実測の結果です。

異方性は、倍率に応じて時間が増えます。x16はトライリニアの5.2倍。ただ、上の表のとおり、x8とx16で見た目の差はほぼなく、時間はx8→x16で約30%増えています。「とりあえずx16」にせず、x4かx8で十分なことが多そうです。

【重要】実際に踏んだ罠:異方性を指定したのに、ぜんぜん効かない

ここからは、私が実際に踏んだ罠を、数字と一緒に紹介します。どれも、エラーが出ずに「なんとなく綺麗にならない」で済んでしまう失敗です。

罠①:MaxAnisotropyが1のまま

サンプラーのフィルターをD3D11_FILTER_ANISOTROPICにしても、MaxAnisotropyを1のままにすると、異方性フィルタになりません。実測すると、5つの帯すべてでトライリニアと完全に同じ値(17.5、28.7、25.9、20.0、14.7)になりました。フィルターの種類だけ変えて安心してしまうと、この失敗に気づけません。

罠②:ミップがないテクスチャに、異方性を指定した

こちらが一番怖い罠でした。テクスチャを読み込む関数が、初期設定ではミップマップを作らない実装だったため、サンプラーだけ異方性16倍にしても、遠景がチラつき続けました。数字にすると、こうなります。

条件5〜12m12〜30m30〜80m80〜200m
異方性x16 + ミップあり13.520.019.614.7
異方性x16 + ミップなし30.544.747.035.7
トライリニア + ミップなし64.280.578.962.5
バイリニア(ミップなし)64.280.578.962.5
正解画像とのRMSE。小さいほど良い

ミップがないと、異方性x16でも遠くのズレが2倍以上(12〜30mで20.0→44.7)になりました。また、トライリニアの指定は、ミップなしだとバイリニアと完全に同じ値になります。「ミップを混ぜる」フィルターを指定しても、混ぜるミップがなければ何も起きません。サンプラー(読む側)とテクスチャ(作る側)の両方が必要です。

罠③:MaxLODを0にしてしまった

サンプラーのMaxLODを0にすると、ミップマップを持っていても、最も細かい階層しか使えません。異方性x16でMaxLODを0にした画像は、ミップなしの異方性x16と、5つの帯すべてで同じ値でした(11.3、30.5、44.7、47.0、35.7)。D3D11_FLOAT32_MAXにしておくのが基本です。

罠④:LODバイアスで、暗黙のうちに霞ませていた

MipLODBiasは、ミップの階層を選ぶ計算にずらしを足す設定です。+1にすると、階層が1つ粗い側に寄ります。トライリニアで試すと、2〜5mの正解とのズレが17.5から27.3に悪化しました(ぼやけた)。逆に-1にすると16.7と少しだけ良くなりましたが、これは正解画像に近づいただけで、動かしたときのチラつきは測っていません。バイアスを負にするのは、シャープに見える反面、チラつきが増えるトレードオフがあると言われているので、動かして確認してください。なお、12m以遠では±1でも値が変わりませんでした。この帯は、ミップが最も小さい階層に達しているためだと考えています。

テクスチャフィルタリングのよくある失敗例と対処法

①フィルターの種類だけを変えて満足する

ANISOTROPICにしたのに、MaxAnisotropyが1のまま。あるいは、ミップがない。どちらも「エラーなし、効果なし」になります。サンプラーの設定と、テクスチャのミップ数を、セットで確認します。

②とりあえず異方性16倍にする

x8とx16の差は、ほぼありませんでした。GPU時間はx16のほうが約30%かかります。まずx4かx8で試して、床や道路のような「斜めに長く見える面」にだけ効くことを確認します。

③UIやピクセルアートにも同じサンプラーを使う

ポイントサンプリングは、遠景では汚くなりますが、ドット絵やUIをくっきり見せたいときは、逆に正解です。3D用と2D用で、サンプラーを分けます。

注意点

  • ミップマップがないと、トライリニアも異方性も意味がない(サンプラーとテクスチャの両方が必要)
  • MaxAnisotropyは1にしない。x4かx8が現実的
  • MaxLODはD3D11_FLOAT32_MAXのまま
  • ミップは「重くなる」とは限らない。今回はミップなしのほうが遅かった(原因は未調査)
  • 異方性の効果は、床や壁のように斜めに見える面で出る。正面を向いた面では差が出にくいはず(今回は床しか測っていません)

まとめ

  • 64枚平均の正解と比べると、ミップの有無が最大の差。ミップなしのバイリニアは、トライリニアの約3倍ズレる(12〜30m)
  • 異方性は近〜中距離で効く(5〜12mで28.7→13.5)が、x8とx16の差はほぼない
  • GPU時間は、ミップありのほうが速く、異方性x16はトライリニアの5.2倍
  • 「異方性を指定したのに効かない」の多くは、MaxAnisotropyが1、ミップなし、MaxLODが0のどれか

サンプラーの設定は、書き換えても見た目の変化が分かりにくく、間違えたまま気づかないことの多い項目です。正解画像との差と、GPU時間を数字で見る、という見方を持っておくと、「どの設定が効いているのか」が、はっきりします。

関連記事