お気楽にジャギーを減らす全画面アンチエイリアス

画像をクリックするとサンプルのwebgl実装に飛びます(safariはダメみたいです)。

こんにちは。技術部平山です。

今回は、Unityで使えるアンチエイリアス手法について調べて整理してみました。 ここで言う「アンチエイリアス」は、画面に出るギザギザ(英語でjaggy。ジャギー)を目立たなくする処理のことです。

結論

まず結論です。

  • ForwardレンダリングであればMSAAのx2はアリ
  • x4は劇的に性能が落ちる機械があるので危険度が高そう
  • Deferredレンダリングだったり、描画面積が大きい場合にはFXAAが理論上良いが、無料で使えるこの実装は何故か手持ちのAndroidで動かなかった。

出る絵の比較

では、アンチエイリアスの効果を画像で見てみましょう。

f:id:hirasho0:20191225192338p:plain f:id:hirasho0:20191225192334p:plain f:id:hirasho0:20191225192336p:plain f:id:hirasho0:20191225192329p:plain

黒い背景に、赤い球を描いた時の輪郭の一部を拡大したものです。 順に、なし、MSAAx2、MSAAx4、FXAAの効果を示しています。

何もしない画像では、上部に1画素だけ突出した部分があって、 かなり気まずい感じですね。なだらかなカーブであって欲しい部分も明らかに 階段が見えます。

これがMSAAx2になると、変な突起はなくなり、かなりなだらかになっていますね。 x2は比較的小さな負荷でかけられる処理ですが、それなりに効くわけです。 ただし、x2には「苦手な角度」があり、この画像では左端に近い斜めの所はあまり ジャギーが消えていません。

そしてMSAAx4になると、x2ではイマイチ綺麗にならなかった角度でも画質の向上が見られます。

最後のFXAAは、Unity標準ではなく、AssetStoreで見つけてきたもの です。全域でジャギーが目立たなくなっていますがが、若干のクセがあります。これについては後述しましょう。

とある測定結果

測定結果を掲載します。数字はフレームレート(FPS)です。

Sharp Android One S3(Snapdragon430)

AA種別 塗り16画面 塗り32画面
なし 35.8 19.1
MSAAx2 35.5 19.3
MSAAx4 31.3 17.6

解像度は1920x1080です。

京セラ Android One S2(Snapdragon425)

AA種別 塗り16画面 塗り32画面
なし 41.7 23.2
MSAAx2 32.0 18.2
MSAAx4 23.5 13.6

解像度は1280x720です。

粗く比較

何をやっているのかの詳細は後で説明いたしますが、 まずは雑に数字を見てみましょう。

S3では、MSAAx2を有効にしてもほとんど性能が落ちません。 MSAAをx4にすると、さすがに性能が落ちますが、10%程度で済んでいます。

一方S2では、x2の段階で20%、x4にすると40%以上の性能低下が見られます。 x2であっても、何もしないのに比べればかなりジャギー軽減効果が高いので、 製品によっては20%の低下は許容できるでしょうが、 x4の40%はさすがに辛いものがあります。 S2のGPUはAdreno306というもので、Adreno300番台はかなりの数のスマホに 入っている非常にメジャーなチップです。 300番台の他の型番でももし同じ弱点があるとすれば、 x4は少々使いにくいと言えるでしょう。

そういうわけで、この2機種で何か結論を出すならば、「x2が妥当かな」となるわけです。 後で他のチップについても調べられれば、この記事に追記しようと思います。

なお、今回のプロジェクトもGithubに置いてありますFXAAはAssetStoreから持ってきましたので、 入れてありません。必要ならばご自分で入れてください。なければないでMSAAのみで動きます。

アンチエイリアスの種類

では、基本的なお話をいたしましょう。

そもそも画面にジャギーが出るのは、画素に大きさがあって、四角くて、ボヤけていないからです。

f:id:hirasho0:20191225192331p:plain

スマホであれテレビであれ、よく見てみれば四角い画素が並んでいます。 さらに拡大して見ると、一つの画素の中にも緑や青や赤の部分がいろんな形であったりもするのですが、 とりあえず画素は四角です。そして、液晶の画素は隣の画素に光がはみ出さないので、 くっきりと四角い形が見えます。これがジャギーの源泉です。 もしブラウン管であれば、そもそも四角で構成されておらず光が滲むので、 ジャギー感は緩やかになります。ファミコンミニのブラウン管を模した画面出力と クッキリ四角い画素の出力を比べてみると良いでしょう(こんなサイトがありました)。

と言っても、2Dのゲームではあまりジャギーは出ません。 なぜなら、アルファブレンドを使って不透明と透明の境界線を曖昧にしながら塗っていくからです。 しかし3DCGではこの手が取りにくい事情があります。 なぜなら「現状のGPUは三角形を描く機械」であり、その三角形の境界は曖昧にできないからです。

f:id:hirasho0:20191225192344p:plain

GPUが三角形を描く時の図です。細い緑の線で区切られたのが画素で、 その上に赤い線で示される三角形を描くとします。 何をやるかと言えば、各画素について画素の中心が三角形に入っているかを判定して、 入っていれば塗り(=フラグメントシェーダを実行)、入っていなければ塗りません。塗るか塗らないかの二択です。 だからジャギーが出るわけです。この仕組み自体はそうそう変えられません。

というわけで、ジャギーを軽減するには、どうにかして画素を滲ませて色の変化をゆるやかにするか、 解像度を上げて画素を小さくすればいいということになります。 そして、解像度はスマホごとに決まっていて上げることはできませんから、 やれることは「滲ませる」ことだけです。

なお、スマホの解像度は本当に高くて、高解像度の機種では私にはジャギーが見えません。 5インチで1920x1080もあれば、何もする必要はないと私は思います。 しかしそんなに高い解像度をそのまま使うと負荷や電池消費が激しくなりますから、 敢えて解像度を落とすのも一つの選択のはずです。 それによる画質の劣化を補うために「滲ませる」を意図的にやることも考えて良いと思います。

ボカす

さて、なにぶん画素が四角なのは動かし難い事実でして、 物理的に「滲ませる」ことはできません。できることは、それぞれの画素の色をいじることだけです。 そこで、隣の画素の色を混ぜて変化を緩やかにしてしまいましょう。つまり、ボカすわけです。

f:id:hirasho0:20191225192326p:plain

白と黒の境界線あたりでは、白と黒を混ぜて灰色にしてしまいました。 変化がなだらかになるので、階段が目立ちにくくなります。 こんなに大きいとよくわかりませんが、ある程度縮小して見ると「階段」というよりは「斜めの線」に見えてきます。

さて、このボカしをやる方法は2つあります。

一つは、そもそも大きな解像度で絵を描いてから縮小することです。 例えば縦2倍、横2倍で描いて、2x2の画素ごとに色の平均を作ります。

f:id:hirasho0:20191225192348p:plain

4x4の画像を作るにあたって、まず8x8を用意し、その解像度で 斜めに白と黒を分け、その後2x2の画素ごとに平均を取ります。 白が4つなら平均も白、黒が4つなら平均も黒ですが、 境界線あたりでは白1つに黒3つの組合せが出てきて、 これが平均すると灰色になるわけです。

もう一つの方法は、前もって倍サイズで書くような面倒をせずに、 できた絵を見て「いい具合に」ボカすことです。 4x4の白黒を見て、「このへんジャギってるけど、きっと元は斜めの線だったんじゃないの?ちょっと灰色にしとくかな」 みたいな感じのことをやります。

MSAAは一つ目の「元々デカく描いて縮小」の一種であり、 FXAAは二つ目の「なんとなくジャギっぽい所をボカす」手法となります。

縮小系の手法SSAAとMSAA

素直に大きく描画する手法をSSAA(Super-sample Anti-Aliasing)と言います。 1024x1024で描画した後、512x512に縮小すればまさにそれです。 フラグメントシェーダは1024x1024全部で計算されます。 一番簡単ですが、計算負荷が大きいので普通はやりません。

そこで、先程紹介した三角形の輪郭に特化してやる手法が開発されました。 これがMSAA(Multisample Anti-Aliasing)です。 同じように倍の解像度で描くのですが、 フラグメントシェーダの実行回数は元のままに据え置きます。 例えば縦2倍、横2倍の解像度で描く場合、4画素の中心の座標でフラグメントシェーダを実行して、 4画素とも同じ色で塗ってしまいます。 ただ、三角形の内側に入らなかった画素は除外されるので、 輪郭付近では2x2の中に違う三角形由来の色が混ざります。 これを平均すると色が混ざってボケるわけです。

MSAAのx2は縦か横のどちらかだけ2倍、x4は縦と横がそれぞれ2倍、と考えれば良いでしょう(そうとは限りませんが)。 x2で苦手な角度があったのは、縦横どっちかしか解像度が上がらないからです(これもそうとは限りませんが)。 x4ならこういう弱点はなくなります。

MSAAは使うメモリが何倍かになり、Zテストやメモリ書き込みの負荷が上がりますが、 ハードウェアによっては素敵な工夫(タイル処理)でその負荷が著しく小さくなることもあります。 上のSharp S3でx2の負荷がほとんど見えなかったのは、たぶんそれでしょう。 おそらくiPhoneの類でも同じようなことが起こるのではないかと思います。

なお、MSAAは三角形の輪郭でしか効きません。 テクスチャがそもそもジャギっているとか、 ライティングの結果鋭いハイライトが出てジャギってしまうとか、 アルファテスト(CutOut、アルファキル、パンチスルーとも) によってできたジャギーとか、 そういうものには無力です。SSAAならそれもケアできます。

後からどうにかする手法

MSAAは「元々デカく描く」という単純な方法なので、いろいろ無駄です。 また、三角形を描画する、まさにその時にレンダーターゲットの解像度を倍にしておかないといけないので、 前準備もいります。

そこで、「描いちゃった絵をいじってジャギーを消す」 という手法が発達しました。MLAAFXAAが代表格でしょうか。 最近だと、AIにやらせるとか、 前のフレームで出た絵の情報を使って「仮にデカく描いていたとしたらどうだったか」を推測する(超解像) とかいう話にもなっていますが、ゲームで60fpsでやれる状況にはなっていません。

FXAAについては理屈がややこしいし、私が自力で実装したこともないので詳細は書きませんが、 前準備がいらず、普通に描画する時に余計な負荷もかからない、というのが魅力です。 また、三角形の輪郭かどうかにおかまいなく、ジャギって見える所は全部やってくれます。

ただ、それが困ることもあります。これをご覧ください。

f:id:hirasho0:20191225192341p:plainf:id:hirasho0:20191225192350p:plain

左は元画像で、右がFXAAさせたものです。 ポリゴンの輪郭のジャギーが消えるのはいいのですが、 字までボケてしまっています。 それが都合が良いか悪いかは時と場合によるわけです。 「クセがある」と書いたのは、そういうことです。

今回のサンプルの設計と、それによる制限事項

今回のテストは、それぞれのアンチエイリアスによって、フレームレートが どう変わるかを見るものです。

しかし上述のようにMSAAは「何かを描画する時の負荷が上がる」 という特性があり、「画面を何度も何度も半透明で塗った場合」 と「不透明のものを一回だけ塗って完成させた場合」 では負荷が異なります。

画面を半透明で塗る処理を追加し、 画面下のスライダーでその回数を指定できるようにしておきました。 測定値の表に「塗り16画面」「塗り32画面」とあるのはこれです。 MSAAがかかっていなくても、画面を塗れば時間がかかるわけですが、 MSAAを有効化することで、この時間がより大きくなります。

一方FXAAは画面を何度塗ろうが負荷は固定なので、 MSAAとFXAAのどちらが速いかは、 「どんなものをどれくらい画面に描いているのか」 によって変わり得ます。 Overdrawが少なければMSAAが有利になりますし、 エフェクト等で何度も何度も半透明を重ねるならMSAAは不利になるでしょう。

ただし、MSAAが遅いのは「Zテスト」「アルファブレンド」「レンダーターゲットへの書き込み」 といった処理で、これはフラグメントシェーダと並列で走りますから、 元々シェーダが重ければ、問題にならないこともあります。 tex2Dしてreturnするだけのシェーダで大量に塗れば不利でしょうし、 たくさん照明計算をして元々時間がかかっていれば、MSAAを有効化しても ほとんど変わらないかもしれません。 今回のテストではテクスチャすら貼らずに真っ赤や灰色で塗っているだけなので、 MSAAで性能が落ちやすいテストであると言えます。

と、いろいろ書きましたが、 要するに「今作ってる製品でONにしてみて、性能低下が許容できて効果があると思ったらONにすればいい」 と考えれば簡単です。 「他の製品ではFXAAの方が速かった」「他の場面ではMSAAが速かった」 といった情報は、あまり役には立ちません。 スイッチ一つで済むので、理屈を考えるよりも、とりあえずスイッチをOn/Offして様子を見るのが良いでしょう。

改めて結論

MSAAとFXAAの長所短所をまとめましょう。

  • MSAAはハードウェアによってはメモリを食い、描画する面積に比例して負荷がかかる
    • 一部ハードウェアではほとんど遅くならないこともある
  • FXAAはポストプロセスで行うので、負荷は1画面分のフラグメントシェーダ負荷で固定になる
    • このシェーダはそれなりに重い

Unity特有の事情としては、

  • MSAAは標準機能なのでスイッチ一個で使える
  • FXAAはAssetStoreから持ってくるか自力で実装する必要がある(ただしURPには入っているらしい)
    • 今回試した実装は、何故か今回のAndroid2機種では効果が見られず、動いているのかわからなかった
    • よって負荷も不明
    • 加えて、RenderTextureへ描画するカメラだと正常に動かない

以上から考えて、スマホ向けの製品であれば、MSAAのx2が無難かなという印象です。 解像度が400DPI(Dots per inch)以上あればそれすら不要だと思うのですが、 逆に、MSAAが多少でもかかっていれば解像度が低くてもあまり気になりません。 MSAA有効で3D描画部分だけ少し解像度を下げて描画し、 元の解像度に引き伸ばしてからUIをフル解像度で描画する、 という昔「解像度詐欺」と言われた技法が今でも有効かと思います。

FixedDPI設定の記事で紹介したやり方だとUIまで解像度が落ちてしまい、 特に文字のにじみが気になりやすいのですが、 3D部分だけ解像度を下げるならば、それほど気になりません。 スマホのように縦横の解像度が大きく違う場合には、長い方だけ半分、 というのが結構いい感じになる気がしています。 例えばiPhoneXであれば、2436x1125ですから、長い方を半分にして1218x1125なんてのはどうでしょうか(試していませんけど)。