こんにちはphi16です。この記事は VRChatワールド探索部 Advent Calendar 2022 の 5日目の記事です。
いろんなワールドを巡っているとうっかり気づいてしまう「エイリアシング」(aliasing) に関して、その原理と対処などについて書きます。
次の2枚の画像、どっちの方が「良い」ですか?
答えは2枚目です。「答え」という言葉を使うのは、この比較においては明確で客観的な「良さ」の基準が存在するという意味で、これはわかりやすく言えば「ピクセルがごちゃごちゃしていないこと」です。エイリアシングが発生しているとごちゃごちゃして見えます。
エイリアシングが発生していると、絵としても余計な色変化が多くなって見づらくなる上、VRの場合は常時視点移動がある為に時間的にチカチカする (頭をほんの少し動かすだけでピクセルの色が激しく変わる) ので体験としてよくありません。
これを軽減する為に歴史的に様々な手法が生み出されていて、VRChat でも実用されています。しかしその仕組みを上手く活用できていない事例も多く見かけます (私の昔のワールドもちょこちょこやっちゃってます)。
そこでまずエイリアシングの原理から、それをどのように減らしていく (anti-aliasing する) かについて網羅的に記していこうと思います。
原理
エイリアシングの原因は、一言で言うと「ピクセルの大きさを考慮していないこと」です。
ピクセルには大きさがあるのです。
あるピクセル領域には (狭いながらも) 様々な方向から来るいろんな色が含まれています。本来はこれら全てが描画に反映されるべきなわけで、シンプルに考えればこれらの平均値がピクセル色になる、ということになります。
即ち、「ピクセルの大きさを考慮する」ということは「ピクセル領域上での積分によって色を決定する」ということです。
以降、「積分」という言葉が怖い場合は、全て「平均の計算」に置き換えてください。
しかし一般に積分は大変です。でもおおよそのケースでは既に積分手法が用意されているのです! なので、基本的にはそれらの使い方を知るだけで anti-aliasing を行うことができます。
それだけではうまくいかない場合は、自前で積分しましょう。
なお、完璧に正しい積分というのは事実上不可能なので、この話で大事なのは「それらしく近似すること」です。
基本的な手法
VRChat もとい Unity (Built-in RP) においては大きく分けて2つ、anti-aliasing の為の仕組みが (元から) 存在しています。
1つは「ポリゴンの境界部分を綺麗にする」Multi-Sampling。
もう1つは「ポリゴン内部に貼られたテクスチャを綺麗にする」Texture Filtering です。
もう一度書くと、ここで言う「綺麗にする」という行為は「積分する」と同義です。
Multi-Sampling
ポリゴンの描画において、まず「ピクセルがポリゴンに含まれていたら色が付く」という直観があると思います。しかしピクセルには大きさがあるので、単純な「ピクセルがポリゴンに含まれているか否か」という2通りの話にはなってくれません。
GPU (というか描画API) はデフォルトでは「ピクセル中心がポリゴンに含まれているか否か」という2通りとして描画を行ってくれるんですが、それだとポリゴン境界がジャギジャギになってしまいます。
理想的には面積比に応じて色がついてほしいところですが、実際には「あとからポリゴンが一部だけ被さってくる」など状況が複雑な為、現実的ではありません。
そこで Multi-Sample Anti-Aliasing (MSAA) という手法があります。
multi- (複数回) sample (抽出) という名前の通り、「ピクセル内の複数箇所を調べてみて積分値を近似する」ことを行います。8xMSAA なら8箇所調べます。
単純に解像度を上げる (Super-Sampling) とは何が違うのかというと、色の計算は複数回行わないところです (大変なので)。ポリゴンの境界を綺麗にする為に、ただ「ポリゴン内か否か」の情報だけを anti-aliasing します。
VRChat ではこの機能はデフォルトで有効なので、ワールド制作者は何もしなくてもジャギの無いポリゴン境界を享受できます。
Graphics Quality を落としたり、直接 Multisample Anti-Aliasing の倍率を変えたりすると設定が変わります。
ちなみにエディタの Scene View では無効っぽいです。Game View のほうが綺麗に見えるはず。
エディタ上だとカメラは静止しているのでそこまで違和感に気づくことはないかもしれませんが、VRだと視点が常に動いているので、Multi-Sampling が無効だとポリゴン境界がずっとジャギジャギしている状態を眺めることになると思います。ビデオメモリを食うのは確かなのでトレードオフではあるんです (ほぼ全ての技術はそうです) が、これによる寄与は大きいと感じています。
(追記 231106) なんと Unity Japan から MSAA の解説が出ました! 観ましょう!
Texture Filtering
今度はテクスチャについての話です。フィルター (信号処理的な意味で) を掛ける、即ち積分をする機能のことです。
先程の Multi-Sampling はユーザ側の設定なのでワールド側にはどうしようもないんですが、テクスチャの設定は完璧にワールド作者に依るものですね。
まずテクスチャが貼られたポリゴンは、ピクセルの色を「テクスチャの色」で塗るはずです。しかしピクセルには大きさがあるので、この「テクスチャの色」というのは単純なものではありません。
ピクセル領域に対応する「テクスチャ領域内の色の積分」が欲しいわけですが、これはなかなか大変なわけです (大量の足し算を行うと重いので)。
そこで、ある程度の範囲を積分しておいたものを用意しておくことで近似計算を行うことが一般的です。これがミップマップ (Mipmaps) です。
上の画像における各ピクセルが、「ある小区画上での色の平均を計算したもの」です。最も左上はオリジナルで 1x1 (Mipmap Level 0)。最終的には右下の 128x128 (Mipmap Level 7, テクスチャ全体の平均値) にまで到達します。
元々計算したかった領域に近い領域に関する積分値を読み出すことで、求めたい積分の近似を行います。
「斜めの平行四辺形」を「軸並行の四角形」で近似し、さらにそれを「小区画の重み付きの集合体」(赤い部分) として捉えます。各小区画上の積分値は既に用意してあるので、これを用いて「斜めの平行四辺形上での積分値」の近似を算出できるわけです。
さて、この仕組みをやるに当たって必要なのが「1px 内に入っているテクスチャ領域はどこか?」という情報です。Fragment Shader でテクスチャを参照するときには UV 座標を指定しますが、これには一点の情報しか与えていません。このままでは「狭い範囲の (0.5,0.5)
」と「広い範囲の (0.5,0.5)
」とが区別できないはずです。
この為にハードウェア側で用意されているのが ddx
ddy
命令です。これらはなんと隣接ピクセルにおける計算中の値を読み取って、差を出力してくれます!UVの値の差によって自身のピクセル領域がわかるので、無事正しい Mip Map を選択できるようになるのです。
ちなみに ddx
ddy
というのは $\displaystyle \frac{\partial}{\partial x}, \frac{\partial}{\partial y}$ のことです。つまりここで求めているのは勾配 (gradient) です。
float2 dx = ddx(uv);
float2 dy = ddy(uv);
tex2D
は呼び出すだけで勝手に ddx
ddy
を計算してくれて、1px の大きさを計算した上で Mipmap をサンプルしてくれるのです。とてもありがたいです。
この仕組みはなかなかおもしろいものです。隣接との差分というのは普通考えると $f(x+1)-f(x)$ で計算するわけで、1番目のピクセルの計算時には2番目のピクセルも計算しなきゃいけない → 3番目のピクセルも計算しなきゃいけない → 4番目も... となって困ってしまう感じがします。
対してこの命令では「$f$ は滑らかな関数である」即ち線形近似として $f(x+2)-f(x+1) \approx f(x+1) - f(x)$ であることを前提にすることで、「1番目のピクセルと2番目のピクセルにおける差分はどちらも $f(2)-f(1)$」「3番目と4番目における差分は $f(4)-f(3)$」などのようにすることで「同時に計算するのは2ピクセルで十分」(実際にはX軸Y軸あるので4ピクセル) なようにできているのです。
逆に言えばこの性質から、ddx
ddy
の値は「隣接2ピクセルで同じものになってしまう」ということです。本来の使い方なら問題となることは無いと思いますが、ちょっと変わったこと (法線方向抽出とか) をすると綺麗な結果にならないこともあるかもしれません。
(ポリゴン境界の解像度に対してハイライトの解像度が半分になっている)
これで一般的な Mipmapping はできるんですが、さらにもうちょっと積分の精度を上げたいということもしばしばあります。それは異方性の強いケースです。
異方性 (Anisotropic) は等方性 (Isotropic) の反対で、Mipmap においては「ある方向には幅が広いが、その垂直方向には狭い」ような場合を指しています。
つまり「遠い地面を見ているケース」や「視線と垂直な方向を向いている板」だと「1px に相当するテクスチャ領域」がめちゃくちゃ伸びちゃって、先程の等方的 (各小領域が正方形なので) な計算方法だとぼやけてしまうという状況なのです。
その為に Anisotropic Filtering を行うことで異方性にも対応できるようにすることが一般的です。特に効果的なのはやはり地面ですね。
Anisotropic Filtering の実装方法についてはハードウェアに依存するので確実なことは言えないんですが、例えば「(正方形に限らない) 小矩形領域たちに関する積分値を予め計算しておく」という方法があります。
そうすると横長の矩形領域に関する積分もそこそこの精度で計算できます。斜めだと足りないのでまた別の方法になるのかも。
まぁ、実装というのは既に用意されているので、私たちは「何を目的とする機能で」「いつ使うべきか」を知っていればそれで十分です。
つまり、「Mipmap は入っているけど、側面から見ることが多くちょっと遠方がぼやけて見えるとき」「Aniso Level を 2 以上にする」ということだけを…
…なんか常に有効になってるみたいです。知らなかった。
デフォルト (Aniso Level 1) でも勝手にいい感じになるらしいです。足りなそうだったらレベル上げましょう。
ところで今回の話はピクセル領域が平行四辺形、即ち一次近似としての扱いをベースにしていました (ddx
ddy
で算出できるのは一次まで)。
実際これで困っていません。それは微分可能なら局所線形と見なせるからです。ポリゴンは射影変換によってちょっと除算の項が入ったりもします (Perspective Correction) が、分数で表せる程度なら当然微分可能です。
分かって頂けるかと思いますが、anti-aliasing とは積分の話であり、その近似の為に微分を必要とするような話です。数学というのは知らないところでめちゃくちゃ使われているというのがよくわかりますね。
人類がこれまで積み上げてきた叡智を認識してあげてほしいです。これは個人的な気持ち。
様々な事例
さて、エイリアシングに纏わる色々な事例について紹介します。なお別に「エイリアシングがあることは悪いこと」というわけでは一切ありません。丁寧にやるのはしんどいし、根本的に不可能なことも多いです。
エイリアシングが気になって直そうと思ったときとか、何故こんなことになっているんだろうと思ったときとかの参考になるかなと思います。
まず Mipmap が画像に入っていない
えー、Mipmap が入っていないテクスチャを使用すると、遠方から見たときにだいたいジャギジャギになります。VRで静止して見ているつもりでもずっとジャーーって見えます (頭は微小に動き続けているので)。
先程述べたように基本的に Mipmap は使うべきです (その方が「正しい」描画結果に近いから)。使用メモリ量は (理論上 4/3 倍に) 増えますが、VRでの体験の質のほうが大事だと思っています。
何故か Mipmap が入ってないテクスチャを至るところで見かけますが、1つの原因として「Sprite としての画像はデフォルトで Generate Mip Maps がオフである」ことがあるのかなと思います。
uGUI は恐らく基本的に 2D 用に作られていて、「UIとしての画像がめっちゃ遠くに表示されている状態」みたいなものを想定していないのでしょう。怪しい話 も聞きますし、根本的にVR用で使うべきではないということな気がしています。
まぁ Quad にテクスチャ張るのがめんどくさい気持ちもわかりますので、とりあえず Sprite 画像の Generate Mip Maps にチェックを入れればいいと思います。
Mip Map Bias が欲しいケース
ちゃんと Mipmap を理解していて敢えて切っているケースとして、「Mipmap による過剰なボケ感を防ぎたい」という需要があるということを訊きました。
これは確かに私も感じるところで、Mipmap がある種の平均化を行ってしまう以上、確かに画像の解像感が失われる部分はあります。特に写真などには致命的なわけです。
ただ、だとしても Generate Mip Maps を切ってしまうことは最善策ではありません。
シェーダを書けることが前提にはなってしまいますが、この為に 利用する Mipmap をズラしてテクスチャをサンプルする関数 tex2Dbias
があります。
float _Bias;
float4 frag (v2f i) : SV_Target {
return tex2Dbias(_MainTex, float4(i.uv, 0, _Bias));
}
確かに Bias = -1 くらいがちょうど良さそうな気がしますね。遠くから見てもジャギらないので良いソリューションだと思います。
Alpha-Clip (Cut-out)
Cut-out によってポリゴンを削る場合、Multi-Sample の恩恵は得られません (Clipping は Fragment Shader で行うため) 。よってその境界部分がジャギることになります。
Opaque である時点でポリゴンは「ピクセル上にあるかないか」の2値になってしまう…わけですが、それを少し改善する方法として Alpha To Coverage があります (AlphaToMask On
ってシェーダに書くと使えます)。
これは端的に言えば Multi-Sample されたことにする 機能で、Fragment Shader での alpha 出力を Multi-Sample 量に考慮してくれるものです。8点被ってたとしても、alpha = 0.5 だったら、4点だったということにするわけです。
これは擬似的な半透明を作るのに便利なんですが、当然 Multi-Sample 数しか段階を持てない のでもちろん万能ではありません。特に Low Quality 設定にしている人には一切見えません。
なので草木などの「クオリティが落ちても多少問題のないモノ」に適用するのがぴったりなのだと思います。はい。
Alpha Blend が使えるという前提で、綺麗に Clipping する方法として fwidth
を用いた alpha 算出方法があります (reference)。
float threshold;
alpha = saturate((alpha - threshold) / max(fwidth(alpha), 0.0001) + 0.5);
これは「alpha の変化の仕方に依らず、threshold 近傍で 1px 幅のグラデーションを作る式」です (勾配 fwidth(a) = abs(ddx(a)) + abs(ddy(a))
で割っているので、傾きが強制的に「1
」になる)。alpha が滑らかに変化することを想定している式ですが、bilinear 補間であればおおよそ問題ありません。
遠方から見たときの見た目は (言うまでもありませんが) Mipmap に依存するので、ちゃんとまともな Mipmap を生やしておいてください。
ところで、Cut-out 用の Alpha 入りのテクスチャはちゃんと Import Settings を確認したほうがいいです。
-
Alpha Is Transparency をオンに: たぶん、完全に透明なピクセル (RGB が黒になっちゃってることがある) を前処理で「周囲の色に合わせた透明」に変換しておくものです。自動的に dilation 処理をしてくれるみたいです。
- Mip Maps Preserve Coverage をオンに: Cutoff 値に応じて各 Mipmap の Alpha の値を調節してくれる機能だと思います。一応確かに差は出るっぽいんですが有意な例を出せなかった… (デフォルトで結構綺麗なのかな?)。まぁわざわざ設定があるわけなので設定しておいて損は無いと思います。
名前から機能がわからないのはめちゃくちゃそう。というかこういうのは「こういうときに使ってね」というユースケースみたいなのを提示してほしいですね…。
テクスチャアトラス
ちょっと種類が違う話ですが、テクスチャアトラスを作ったときに「隣のテクセルが漏れ出てくる」現象がよく発生します。それによってポリゴンの端に変な線が見えてしまう。
これは原因が2つあって、1つは「ギチギチまで詰めてしまうと Bilinear 補間で隣区画が見えてしまう」こと。
確かに元々テクスチャ的に繋がっている領域を分割したところで、その内部的な繋がりは消えません。結局は「端の1px幅領域は、使わないほうが良い」ということです。
厳密には0.5px領域までで十分です (Bilinear 補間はそういうやつなので)。何にせよ対処はそんなに大変ではありません。
しかしそれだけでは済みません。もう1つの原因は Mipmap です。「近づくと大丈夫だけど遠くから見ると1px幅の変な線が見える」のはこれのせいです。
これです。すいません。
延々書いたようにピクセルには大きさがあります。よって描画時には「ピクセルの大きさに対応するテクスチャ領域の積分値」が使われます。
遠くから見たときには当然その該当テクスチャ領域は広くなり、隣のテクスチャまで読みに行ってしまいます。
この問題の完璧な対処は結構難しいと思います。「ある程度以上の Mipmap は生成しない・参照しない」ということも描画APIレベルならできた気がするんですが、Unity 自体にはその機能は無さそう?後述する tex2Dgrad
とかで無理やり範囲制限することは一応できますが…。
結局はある程度の padding を持たせる必要がある、という一般的な解決法に落ち着きそうです。「見えるかもしれない Mipmap レベルでの 1px」よりも大きい padding であれば、大丈夫です。
ちなみに、これはアトラス内の各テクスチャが2の冪のサイズになっていて且つ綺麗に (= ミップマップぴったりに) 配置されていることを前提にしています。そうじゃない場合 (GCとか) は各 Mipmap が綺麗に区分けされないので漏れやすいと思います。
GCのアレはどうにか直したいんですけどね…。tex2Dgrad
による力任せでどうにかならないかなぁ。
追記: そういえば Fadeout Mip Maps とかもありました。これは指定したレベル以上の Mipmap をグレーにする機能で、まぁ余計な線が出るよりは「何もない」ほうがまだマシだろうという判断だと思います。使えるときは確かにありそう。
追記2:
— lil (@lil_xyzw) December 5, 2022
尚これに限りませんが、「ポリゴンに指定したUV領域の内側のテクスチャが参照される」という保証は特に無いので、困ったときにはこういう原理をちゃんと見直すと良いと思います。
DepthTexture によるエイリアシング
これはどうしようもない話ですが、Depth Texture を用いたエフェクト (影・ポスプロフォグ・水中の色の減衰・Deferred Renderer もどきなど様々) では一般にエイリアシングが発生します。
この原因は Multi-Sampling です。Depth Texture から得られる深度値は、Multi-Sampling によって得られた深度値とは一般に一致しません (複数の深度サンプルを1つに押し込んでしまったものなので)。つまりポリゴンの境界になったピクセルで「現在位置が深度値より手前か?」を判定しようとすると、必ず誤判定するピクセルが発生します。実質上 Multi-Sampling が無効化されてしまうのです。
Multi-Sampling を切ると改善します (下) (が、もちろんポリゴン境界のジャギがひどくなります)。技術というのは往々にしてそういうものです。今のところは諦めるしかないです。
Signed Distance Field による輪郭描画
エイリアシングとはちょっと違う話ですが、まぁ補助的に使える話題として。
図形を「領域」ではなく「各点における輪郭からの (符号付き) 距離 (Signed Distance Field)」として定義することで色々と処理がしやすくなることがあります。
特に、Alpha-Clipping は alpha が (局所的に) 線形に変化することを前提にしているので、距離場のほうが相応しい部分もあるのです。
元のテクスチャ (左上) で Clipping した左下より、距離場 (右上) を Clipping した右下の方が線が綺麗であることがわかります。拡大しても大丈夫。
ちなみに今回の右上の距離場 (っぽい画像) はただの「適当な半径大きめの Gaussian Blur を掛けた画像」です。そのせいでちょっとふにゃんとしちゃってるかな?元々やわらかめの図形ならこれでも大丈夫だと思います。ちゃんと作ったらもうちょっと鋭さ (?) も保てると思う。
値の線形性を前提にした処理をする場合は sRGB のチェックを外すことに注意してください。何故ならそれは sRGB 画像ではないので…。
ちなみにシェーダーでSDFを作っている場合も前項の「fwidth
を用いた Clipping」をすると輪郭がとても綺麗になります。おすすめです。
単純な SDF だと「ぱきっと折れ曲がる線」に弱いという特性もあるんですが、これを「複数のSDFを重ねる (Multi-channel SDF, MSDF)」ことで解決するという方法も あります。
生成するのはだいぶ面倒ですが、その分のクオリティはあると思います。
テキスト
前の話に付随して…。
VRでのテキスト表示は本当に難しいですね。それぞれ一長一短だと思います。
- 画像で置く
- ○ いろんな方法で作れる
- △ 近づくとピクセルが見える
- uGUI Text・3D Text
- ○ 動的な文字追加が可能
- △ 近づくとピクセルが見える (普通のテクスチャサンプリングなので)
- Text Mesh Pro
- ◎ SDFによる綺麗な輪郭表現が可能
- △ 予め文字セットを用意する必要がある
- △ 離れて見るとポリゴン形状が見えちゃう
- メッシュを置く
- ◎ MSAAによる綺麗な輪郭表現が可能
- △ 用意するのが面倒
- △ 曲線の表現が苦手
私はメッシュ置きが一番綺麗だと思っていますが、これができるのはロゴとかそういう「決まったもの」に限るところはありそうです。遊び甲斐もあって楽しい。
メタフェス の kintone ブースの文字は (画像になっちゃってるもの以外) 全部メッシュだったのでめちゃくちゃビビリました。やっぱり綺麗です。QRコードもメッシュでした…。
Inter-action on the Math の解説板は「タイトルたちは3D Text」「本文はTMP」「本文中の数式は画像」という感じになってます。今ならもっとよくできそう…。
不連続なUV
タイリングテクスチャだし uv
の代わりに frac(uv)
を使っても結果は同じだろうと思ってサンプルすると死にます。
何故かというと Mipmap を選ぶ為に隣接ピクセルを読むため、「0.98 と 1.01 の差」ではなく「0.98 と 0.01 の差」だと認識して「この狭い隙間にめちゃくちゃテクスチャが詰まっている」と勘違いするからです。不連続なUVを tex2D
に与えるべきではありません。
[Unity]無限平面を描画する過程でGPUの理解を深める という記事でも解説されていますが、解決方法は tex2Dgrad
を用いることです。つまり明示的に「正しい隣接ピクセルとの差」を与えてあげるということですね。
float2 uv;
float2 fuv = frac(uv);
float4 c = tex2Dgrad(_MainTexture, fuv, ddx(uv), ddy(uv));
実際この問題はしばしば起きていて、特に Skybox に見られます。たまにある謎の縦線はこれのせいです。
Equirectangularテクスチャをサンプルする場合、視線方向 (長さ1のベクトル) を経度 (-π ~ π) と緯度 (-π/2 ~ π/2) に分解しなければなりません。
float3 dir;
float longitude = atan2(dir.z, dir.x);
float latitude = asin(dir.y);
float2 uv = float2(longitude / UNITY_PI * 0.5 + 0.5, latitude / UNITY_PI + 0.5);
お察しの通り、atan2
は -π と π の間に不連続性を発生させます。Unity に用意されている Skybox/Panoramic
はその対策がされていません。
雑に解決するなら「隣接ピクセルとの差分が 1 を越えていたら 1 を引く」をやればいいような気がします。それをさらに雑にやるとこう。
float2 dx = ddx(uv), dy = ddy(uv);
// 値を [-0.5, 0.5] の範囲に無理やり収める
dx = frac(dx + 0.5) - 0.5;
dy = frac(dy + 0.5) - 0.5;
float4 c = tex2Dgrad(_MainTex, uv, dx, dy);
タイリングテクスチャとかでなければこれはわりと汎用的な手法ですね…。
まぁ Cubemap にするのが当然一番いいんですけどね。
シェーダによって作られる模様
シェーダおえかきによって作られる模様は、概ね UV を入力して色を返す関数として実現されていると思います。なので、特に何も考えなければ、UV の変化が激しい遠方などではジャギが目立ちます。海とかが顕著。
完全に汎用的な手法は存在しませんが、例えば iqさんが band limiting という記事を書いていて、「cos
などの周期関数を使う場合は fwidth
に応じて振幅を抑える」などの方法が有効です。
まぁ強いて言うなら「積分できるなら積分する」ことが汎用的な手法です。ddx(uv)
, ddy(uv)
範囲の平行四辺形領域での積分値を返せればいいです。
その点についても iqさんは filterable procedurals という記事を書いていて、実際に積分値を解析的に出せる関数の例があります。
一般には無理なので、まぁせめて「遠方では fog のように色の分散 (variance) を無理やり抑える」などによって見かけ上あまり気にならないようにするのが良いところなのではないかと思います。違和感がなければそれでいいので。
HDR
小さな話ですが、Bloom の無い環境で「1」より強い色を Emission などで出してしまうと、ちょっとだけエッジがジャギって見えます。
これは例えば、「8」を出力するような Emissive 物体があったとしても、MSAA は 8 段階までしか anti-aliasing をせず、どう足掻いても「少し見えるだけで光が 0 から急に 1 を越える」状況になる、みたいな話です。
まぁ眩しさの表現としては理論上間違ってないので大して気にするところではありませんが、もしも気になるようであれば「ベイク時には 4 の強さ、VRChat上では 1 の強さ」のように微妙に数値を分けると良いのかもしれません。
あと Bloom を炊くとどうせ境界線がぼやけてわからなくなるので大丈夫という話もあります。一般に、エイリアシングは Bloom を炊くとわかりにくくなります (色の局所的な分散が下がる為)。それもまたごまかし方の一つだと思います。
Moiré
モアレというのは「2つの周波数の異なる波が重なることで元々無かった波が見えるようになること」だと思います。VRではピクセルによって視界が離散化されたことで生まれた周期的な「波」が常に見える為、細かい繰り返し配置のオブジェクトは遠方から見ると基本的にはモアレが発生します。頭を動かすとちょっとジラジラして見えます。実際「エイリアシング」という語の原義はこっちに近いのかな?
オブジェクトの繰り返しによるモアレの解決に関してはなかなか良いソリューションは無いんじゃないかなと思います。こういう「VRだと実は難しいタイプのデザイン」みたいなものはいくつかありそう。
対して Mipmap によって色を平均化することはモアレを軽減する手法になってくれて、いわゆる LED 系のシェーダみたいなものはシェーダによって図形を作ることはせずにテクスチャで模様を描いたほうが「何もせずに」いい感じになります。
ところで LED 系モニタは遠景での見た目を整える為に「元のテクスチャとクロスフェードする」処理が入っていることが多い気がしますが、このとき色味がずれてるとちょっと勿体ない感じがします。
なんでこうなってるかはよくわかんないですが、とりあえず、遠景で見える色には本来「元のテクスチャの色」に「LED 用のテクスチャの平均色」が掛け合わさっているはずです。
つまり例えば半径 0.25 のドットを使っているなら、全体の色は 20% (π/16) くらいにまで落ちるはずということです。
上の画像で塗られている色は 0.478 くらいですが、それは sRGB 上の話で、linear だと 0.478^2.2 = 0.197 くらいなので合ってます。
逆に言えば「近づいたときには 5 倍眩しく見えるくらいじゃないと遠くから見たときに意図した色味にならない」ということですね。まぁこの辺はちゃんと計算すれば色々わかるはずです。
GCの中庭の巨大モニタは綺麗だと思っています。ちょっとモアレが見えるくらいが (ぼけすぎるよりは) 良いと思う。
ドット絵調
これは特殊な話ですが。ドット絵っぽいテクスチャ (質感) をVRChatで出そうと思うとき、Nearest Filteringを使ってしまうのはよくありません。理由はドットの境界がジャギるからです。ドット絵なのに!
Silentさんによる Pixel Standard というシェーダがあるっぽいのでそれを使うと良いと思います。中身は fwidth
をちゃんと見てサンプル位置を算出するというものです。わかる。
元のテクスチャがドット絵だとしても、私達の視界の下では積分を計算する方が適切なのです。
Specularによるエイリアシング
根本的なところから考えます。物体表面の凸凹というのはどうやって表現するのでしょうか? これは凸凹の大きさに合わせて大まかに3つの方法があります。
- 輪郭でわかるほど凸凹している場合 (cm単位): ポリゴンで作る。
- 光を当てるとわかる程度の凸凹がある場合 (mm単位): Normal Map で対応する。
- 素材の性質レベルでの凸凹の場合 (μm単位): Roughness で対応する。
材質 (material) というのはおおよそ「どれだけ表面が凸凹か」で決まります。完璧に凹凸がなければ完全鏡面、めちゃくちゃ凸凹してたら完全拡散面です。それ以外は概ねこれらの中間です。
凸凹が一切無いときの「完璧な Specular」(鏡のことです) が、小さな凸凹が増えていく (表面が荒くなる) につれ、ぼやけていきます。
材質レベルでの凸凹感は (小さすぎるので) もはや具体的に指定するものではなく微小面の分布として指定することが一般的です。細かい話はいろいろありますが、Unity ではそのパラメータを Roughness (Smoothness) で指定するようになっています。
$\mathsf{Smoothness} = 1 - \sqrt{\mathsf{Roughness}}$ らしいです。Unity は。
そして物体自体が小さく凸凹しているなら Normal Map を使って表現。それでも足りなければ (スケールが大きければ) Displacement Map / Height Map による Parallax Mapping とか、Tessellation とか、最終的にはポリゴンで組む、というようにスケールによって手法を変えていくことになります。
さて、遠方から見たときに Specular をどう描画すべきでしょうか。 基礎から考えると「ピクセル範囲内にある凸凹の分布を元にして色を決定する」ことが適切なはずです。
「1px に含まれる細かい凸凹が多くなる」ということは、「平均色」の計算には単なる凸凹量の平均だけでは足りず、「凸凹がどれくらい激しいか」即ち分散 (variance) が必要になってくる状況にあります。
これまでの anti-aliasing は平均値としての積分で済んでいましたが、それは入力 (ポリゴンの面積・テクセルの色) と出力 (見える色) が線形な相関があったからです。Specular (入力は法線方向、出力は見える色) は線形な振る舞いにはならないので…。
なので、Normal Map による Specular のエイリアシングを綺麗にしようとしたら Normal Map に Mipmap を付けるだけでは不十分で、ピクセル範囲内の Normal の分散 まで調べなければなりません。その分散を利用して強すぎる Specular を抑えるのです。
色々と話題はあるっぽいんですが実際に活用したことはあまりないのでアレです。
-
Mipmapping Normal Maps: Normal Map の Mipmap を拾ったときに「分散が大きいときほど Normal の長さが短くなる」ことを利用して分散を推定するらしいです。適当に Roughness に突っ込んでみた (右側) んですがなんかこう、思ったのと違う感がしますね…。雑なのがいけないのかなまぁ。Unity の Normal Map の枠組み (根本的にデータフォーマットが違う) とも相性悪そうなので微妙そう。
- Normal2Roughness: 事前に Normal Map を前処理して適切な Roughness 値を足し込んでくれるらしい?です。最悪みたいな Normal Map を突っ込んで見たところ何も起きなかったんですが多分これは私が悪いです。
-
Geometric Specular Antialiasing: 上記は Normal Map の話でしたが、加えて ポリゴンの法線が激しく変化する場合 (Phong shading 的な意味で) にも対応するべきと言えます。実際に
ddx
ddy
を用いて分散を計算できて、わりとシンプルな形式で適切な Roughness を算出できるようです。お陰様で CUE の単管もそこそこ綺麗になってます。
実際いくつか最近の汎用シェーダには組み込まれているみたいですね。HDRP/Lit にも用意されているらしいですし。
まとめ
様々な話題を見てきました。実際の事例から色々とわかってくることがあると思います。
-
完璧にやるのは不可能
- Multi-Sample も Mipmap も基本的にはうまく綺麗にしてくれますが、同時に別のエイリアシングを発生させることもあります。また正しい積分は基本的にはできません。なので「気になる部分は対処する」「大変すぎる部分は諦める」という姿勢が (まぁ3DCG全般において) 必要だと思います。
- 大事なトリックとして、別に正しい積分値であることを人々が求めているわけではないという点があります。ジャギで気が散るようなことがなければいいわけで、例えば強めの Bloom を掛けるとほぼ気づきません。「問題の外側で解法を探す」みたいな話はこういう意味ですね。
-
気になる部分はわりと対処できる
- Alpha-Clipping や Specular によって発生するエイリアシングは (何もしなければそのままですが) ちゃんと対処法が世の中には存在します。どうしようもない部分はもちろんありますが、いつでも描画の原理に立ち返ったり Unity の機能を調べたりして対策はないものか調べることに意味はあると思います。
-
努力は気づかれない
- エイリアシングが出ていることには人は気づけるんですが、エイリアシングが無いことに気づくことはありません。世の中の作品というのはそういう見えない努力が無数にあるもので、まぁ作る側としてはそういう部分は評価されないものだと割り切るのが私は良いと思っています (何も言われないことは良いこと、です)。
- でも色々と知識を持っていると「無いことに気づく」ことができるようになるので、もしもこの記事を読んで「このワールドはちゃんとエイリアシングが目立たないように頑張ってる…!」みたいな気付きがあったら是非称賛の気持ちを持ってください。
-
数学は大事
- 「ちょっとの変化」を扱うのが微分で、「変化のかきあつめ」を扱うのが積分です。さらに言えば「変化の激しさ」を司るのが分散です。どれもレンダリングに出てくる大事な概念です。別にこれらは誰にとっても「知らない概念ではない」はずなのです。そこにあるけど言葉として認識していないもの、だと思います。
- そういう「当たり前にあるけど認識が難しいもの」に関する語彙を集めた領域が「数学」だと思うので、私はそれを大切にしたいと思っています。大切にしてください。
ワールド探索をするときにこういう細かい部分を見るのも楽しいと思っています。別に貶す為に見るのではなく、そういう部分に「作者がどこまで気にしてどこまで気にしないか」の情報が入っている (作者がどういう人かをある側面から知ることができる) からです。
もう一度書くとエイリアシング自体に良い悪いはないですからね。それで意図した表現になっているのかどうかだけです。
おわり。
おまけ
ワールド探索部要素として、今までに挙げた画像のワールドを列挙しておきます。
- AA By phi16 (private world)
- 「VR」についてかんがえてみた vol.01 後編 で言及した「アンチエイリアスの博物館」、に成り損ねたワールドです。この記事を書いたことによって心残りが完全に昇華されました。
-
ǀ> By phi16
- ここは上の AA ワールドから分離して「MSAA と Mipmap の原理の展示」だけ作ったものです。似たような思いで制作後に文章を書いたりもしました。
-
Cocoon-05 By mikka-bouzu
- 穏やかでいいところです。ポリゴンがめっちゃあるワールドを探そうとして思い出しました。
-
Metamorphosis By Connected Ink
- なかなか無いうにょうにょのシェーダ表現があって面白いです。2px アーティファクトは多分シェーダで生成した Height を Normal にしようとして発生してるのかなと推測します。これくらいなら綺麗で効果的でいい感じだと思いました。
-
Vket6 Laguna Conchiglie By VirtualMarket1
- 昔から Vket は Mipmap の入ってないテクスチャが多く悲しい気持ちによくなっていましたが、最近はきれいになってきたなと感じています。
-
Sweets Dish! By とかげ/khtokage
- かわいいワールド。すき。ワ探で行きました。
-
SPURGEAR Plants lab By [JP]Rsea⁄るしぃ
- 植物が綺麗で良いです。ワ探で行きました。
-
Inter-action on the Math By phi16
- うちです。
-
追憶の庭園 - garden of nostalgia - By tiwa
- 雰囲気がめちゃ良です。すき。
- GHOSTCLUB 5.0
- 某所。マジでCRTの縁に出てくる 1px 線はさっさと直したいと思ってる。
-
|Flying buttress|++ Milano Gothic ++ By ᴅᴇʀᴜᴛᴀ
- ゴシック建築のかっこいいとこ。ワ探で行きました。
- Amebient By phi16
-
omochi museum By phi16
- うちです。
-
Half Line By phi16
- うちです。地面のロゴはかなり気に入ってます。
- メタフェス
- 文字がメッシュでできているのを見たときには本当にびっくりしました。嬉しくなりました。
-
Mental Well-Being Clinic By Softengi_meta
- 穏やかそうなところです。ワ探で行きました。
-
Namuanki By Kevin Mack
- ワ探の2周年記念の「ワ談会」の後に流れで行きました。めちゃ Houdini と Shader を感じられて面白いです。技術の良い使われ方。
-
Shader Fes 2021 By ShaderFes
- 言わずと知れた。
-
Wakaさんの Gallery&Home World
- 正確にはワ談会の会場です。Wakaさんのおうちにはお世話になっております。
-
Re˸collection - リコレクション - By rocksuch
- 写真展示の綺麗なワールド。モアレなんかで紹介してしまって申し訳ないです。メインはそこではない。
-
Vket2022S VketPlaza -CyberMode- VIVID PARK By VirtualMarket4
- Vketです。まぁ LED そんなに気になるかというと最初の一瞬だけなので別にそこまででもないです。
-
Nocturnal Loft By AltCentauri
- 小ぢんまりとした海外系のワールドです。ワ探で行きました。
-
CUE[Archive]By 0b4k3
- アレです。来てね。
ワールド探索部に連れてもらっているおかげで (どうせ一人じゃ絶対見ないような) 様々なワールドを見ることができとても感謝しております。適度に sparse な良い雰囲気のコミュニティです。「粗」の話ができるのも嬉しい。