ビデオゲームでアウトラインを描く方法
(ameye.dev)- Unityでオブジェクトの周囲に アウトライン を描く代表的な方法は、リム効果、頂点拡張、ブラー・バッファ、Jump Floodアルゴリズム、エッジ検出であり、品質・性能・設定コストはそれぞれ異なる
- リム効果 は、法線と視線方向の内積でフレネルを近似し、内側の縁を強調するが、丸いモデルと鋭いモデルで結果に大きな差が出る
- 頂点拡張 は、複製メッシュを背面側で拡大して外郭線を作る方法で、クリップ空間で処理すると画面上の太さをより一定に保てる
- ブラー・バッファ と Jump Floodアルゴリズム は、シルエットをバッファに描画してから拡張するアプローチで、それぞれ滑らかな線や非常に広いアウトラインに強みがある
- エッジ検出 は、深度・法線・色バッファの不連続を全画面で探して線を作るが、不要な線を減らすにはしきい値と変調を細かく調整する必要がある
アウトラインレンダリングを使う理由
- アウトラインはゲームで 視覚スタイル を作ったり、オブジェクトの強調や選択といった ゲームプレイ補助 に使われる
- Sableはコミックブックのようなスタイルのためにアウトラインを使用している
- The Last of Usは、プレイヤーがステルスモードに入ったときに敵を強調するためにアウトラインを使用している
- 5つの手法は、性能、視覚品質、手動設定量の間でそれぞれ異なるトレードオフを持つ
リム効果: フレネル近似で内側の縁を強調
- リム効果はフレネル効果を近似し、オブジェクトの リム/縁 に線をレンダリングする
- 計算は正規化された法線ベクトル
Nと正規化された視線方向Vの内積に基づく1.0 - saturate(dot(N, V))の値をべき乗Pに上げて効果の強さを作る- 物理的に正確なフレネルというより、アウトライン用の近似に近い
- Unityでの実装では、カスタムシェーダーがアウトラインの 幅、パワー、滑らかさ、色 を制御する
- 実装例では
smoothstep,lerp,_OutlineWidth,_OutlineSoftness,_OutlinePower,_OutlineColorを使用する
- 実装例では
- この方式は常に 内側の線 として現れ、オブジェクトの外側の輪郭には表示されない
- 丸く滑らかなオブジェクトにはよく合うが、キューブや複雑なモデルでは線幅が不均一になったり、アウトラインらしく見えないことがある
頂点拡張: 複製メッシュを拡大して外側の線を作る
- 頂点拡張は、元のオブジェクトやメッシュの 複製 を再描画し、元オブジェクトの背後で頂点を拡張してより大きな形状を作り、単色でレンダリングする方法である
-
拡張方向の選択
- 頂点をどの方向へ動かすかによって、アウトラインの品質は大きく変わる
- 頂点位置方向 に移動すると、メッシュが膨らむような形になる
- ローカル空間では頂点位置を、オブジェクト中心から頂点までのベクトルとして使える
- 正規化しないと、中心から遠い頂点ほど大きく移動する
normalize(positionOS) * widthを使うと、オブジェクト空間でより均一に移動できる- 法線ベクトル方向 に移動すると、球やカプセルのように滑らかな角を持つオブジェクトで良い結果になる
- キューブのように鋭い角を持つオブジェクトでは、アウトラインに隙間ができることがある
- 頂点カラー を拡張方向として使うこともできる
- これはカスタム法線を生成し、メッシュの頂点カラーチャンネルに保存する方式である
- キューブに球状の滑らかな法線を頂点カラーとしてベイクすると、より良いアウトラインを得られる
- 欠点は、メッシュごとにカスタム法線を作る手動設定が必要なことで、スクリプトで自動化することもできる
-
拡張空間の選択
- シェーダーの頂点段階では、頂点座標はオブジェクト空間から始まり、MVP行列を通ってクリップ空間へ変換される
- 流れはオブジェクト/モデル/ローカル空間 → ワールド空間 → カメラ/ビュー空間 → クリップ空間 → スクリーン空間 → ビューポート/ウィンドウ空間である
- オブジェクト空間 で拡張すると、MVP変換がまだ適用されていないため、アウトラインが歪むことがある
- モデル行列の適用時にスケーリングの問題が起きる
- クリップ空間からスクリーン空間へ移る際には、透視除算のため遠近による縮小が発生する
- 3D空間での移動の一部はカメラ方向への移動となり、画面上のアウトライン幅には寄与しない
- クリップ空間 で拡張すると、画面上でほぼ同じ太さのきれいなアウトラインを作れる
- 頂点位置と法線をクリップ空間へ変換したあと、
x,y座標だけを修正する - 画面の横幅・縦幅で割ってアスペクト比を補正する
- クリップ空間の
wを掛けることで、その後の透視除算がアウトライン幅に実質的な影響を与えないようにする - 幅単位1が画面上の1ピクセルに対応するように
width * 2を掛ける - クリップ空間方式もメッシュ法線に依存する
- カスタム法線を使わないと、鋭い角で隙間ができることがある
- 法線が誤って設定されて逆方向を向いていると、アウトラインの頂点も逆方向へ移動して隙間が生じる
- 関連する説明は creating an outline in clip space でさらに見られる
-
マスキング
- 複製メッシュでは、突き出した アウトライン部分だけ が見える必要がある
- 一般的な方法は、複製メッシュの front-facing geometry をカリングし、backfaceでアウトラインを作ることだ
- 深度テストには less than or equal to を使い、backfaceがアウトライン位置にだけ見えるようにする
- 別の方法は、ステンシルマスク を使って複製メッシュが元オブジェクトの前に見えないようにすることだ
- この場合、カリングは不要である
- オブジェクト内部の線はまったく発生しない
- 2つのオブジェクトが重なっても、両オブジェクトの周囲にだけアウトラインが見える
ブラー・バッファ: シルエットをぼかして拡張する
- ブラー・バッファ方式は、オブジェクトの シルエット をバッファにレンダリングしたあと、ブラーで拡張し、その結果をアウトライン描画に使う
-
シルエットバッファ
- 最初の段階はシルエットバッファを作ることだ
- 各オブジェクトを単色出力シェーダーでテクスチャにレンダリングする
- すべてのシルエットを白で描画すれば、最後に望むアウトライン色を乗算して単一色にできる
- オブジェクトごとに異なる色のアウトラインが必要なら、各シルエットを特定の色でレンダリングできる
-
ブラーパス
- ブラーパスはシルエットバッファを拡張するために使われる
- 通常は ボックスブラー または ガウシアンブラー を使う
- 性能改善のため、ブラー前にシルエットバッファを縮小できる
- ブラーパスは各ピクセルの周囲にある複数ピクセルの平均または加重平均を計算するため、コストが高い
- ブラーは2パスで行うのが望ましい
- 分離可能フィルタであるボックスブラーとガウシアンブラーでは、アルゴリズム複雑度を
O(N²)からO(2N)に下げられる - まず縦方向にブラーし、その結果を再度横方向にブラーして最終結果を作る
- アウトライン幅はブラーシェーダーの
_KernelSizeパラメータで制御する
-
アウトラインパスとマスキング
- ブラーパスの後、ぼかしたシルエットを元のシーンと合成してアウトラインを作る
- ブラー・バッファは 柔らかい、または発光するアウトライン に適している
- ブラー結果にstep処理を加えると、硬いアウトラインもレンダリングできる
- 頂点拡張方式のようにステンシルマスクを使って、アウトラインがジオメトリの背後にだけ描画されるようにできる
- 他の方式より性能への影響が大きい場合がある
Jump Floodアルゴリズム: 非常に広いアウトラインを処理する
- 4番目の方法は Jump Floodアルゴリズム でアウトラインをレンダリングする
- 利点は、非常に広いアウトラインを妥当な性能コストでレンダリングできる点にある
- 詳細な説明は Ben Golus の The Quest for Very Wide Outlines に続く
エッジ検出: 全画面で不連続を探す
- エッジ検出方式は、全画面パスでシーンの 不連続 を探して線を描く
- 不連続は、深度バッファ値、法線ベクトル、アルベド色、またはレンダリング過程で利用できるその他のデータから検出できる
-
Roberts cross
- Roberts cross operator は、対角ピクセル差の二乗和を計算する微分演算子である
- 実際の実装では、元画像にカーネルを畳み込んでエッジを検出する
x方向とy方向のカーネル2つを使う- カーネルサイズは
2 x 2である - 1ピクセルの周囲4サンプルだけが必要である
- 単純な演算子だが、良い結果を出せる
-
Sobel operator
- Sobel operator も
x方向とy方向のカーネル2つを使う - Sobelカーネルサイズは
3 x 3で、1ピクセルの周囲9サンプルを使う - Sobelフィルタの動作は blog post on Sobel filters でさらに見られる
- Sobel operator も
-
不連続ソース
- 一般的な方法は、レンダーパイプラインがシーン用に生成する 深度テクスチャ、法線テクスチャ、カラーテクスチャ から不連続を探すことだ
- エッジ検出パスはこれらのテクスチャをサンプリングし、前述の演算子で不連続を検出する
- こうして作られたエッジは、3つのバッファのいずれかで見つかった不連続によって描かれることがある
- この方式では、それらのバッファに書き込むすべてのオブジェクトにアウトラインが適用されるため、オブジェクト単位での制御性は低い
- 複数の不連続ソースを許可すると、より堅牢なアウトラインシステムを作れる
- あるエッジは3つすべてのソースで検出される
- 多くのエッジは、特定の1ソースの寄与だけで検出される
- 各ソースごとに異なる重みとしきい値を与えられるため、アウトラインの見た目を制御できる
-
エッジ検出の変調
- 不連続バッファにエッジ検出演算子だけを適用しても、アーティファクトのない結果を得るのは難しい場合がある
- 深度バッファは多くのレンダーパイプラインで非線形に実装されている
- カメラに近い2つのオブジェクトが1m離れているときの深度差は、遠くにある2つのオブジェクトが1m離れているときより大きい
- これを補正するため、深度不連続検出のしきい値を深度バッファ自体で変調できる
- 近いジオメトリは、エッジとして検出される前により大きな深度値の不連続を必要とする
- 小さな grazing angle で不要なエッジが発生することもある
- 法線ベクトル
Nと視線方向Vの内積で作るフレネルマスクを使って変調できる - このマスクはリム効果方式で使ったものと同種である
- 他の変調手法も使えるが、狙う視覚効果によって選択は変わる
-
カスタム不連続ソース
- アウトラインシェーダーに カスタム不連続ソース を提供することもできる
- これはレンダリング過程で直接作るレンダーテクスチャで、アウトライン生成に使うカスタムデータを含む
- どのオブジェクトがカスタムバッファに書き込まれるかを直接制御できるため、どのオブジェクトにアウトラインを付けるかも制御できる
- 例として、メッシュの頂点カラーをテクスチャにレンダリングして不連続ソースを作れる
- 別の方法として、ワールド位置に応じて面を着色したり、深度バッファと法線バッファの情報を組み合わせたカスタムバッファを作るやり方がある
- 追加情報は Linework section map で確認できる
1件のコメント
Hacker News のコメント
リンク先の Jump Flood Algorithm の記事は本当に良かった: https://bgolus.medium.com/the-quest-for-very-wide-outlines-b...
ピクセル/テクセルレベルで使えるさまざまなアプローチを考えるのが興味深く、ここでも 符号付き距離場(SDF) が多くの処理を肩代わりする賢い方法として使われている
任意の幅のアウトラインを線形時間で作れるという結果は、太い幅で総当たり的に計算する方法と比べるとすごい
ベクターベースであれ、Inigo Quilez の仕事のような関数ベースであれ、記事のようなテクセル/ボクセルベースのラスタ方式であれ、SDFを強くおすすめする
Houdini もラスタSDFをよくサポートしていて、成熟したSDFツールセットがあるので、無料版でも見る価値がある
GPUで動かすにはランダムアクセス書き込み、つまりコンピュートシェーダーが必要になるという欠点がある
CPUでよければ実装はいくつかある
JavaScript: https://parmanoir.com/distance/
C: https://github.com/983/df
C++: https://github.com/opencv/opencv/blob/4.x/modules/imgproc/sr...
Python: https://github.com/pymatting/pymatting/blob/afd2dec073cb08b8...
品質も良く、脈動するアウトラインのような距離ベースのエフェクトをレンダリングできるからだ
この3D線描画ツールも、SDFを小さなテクスチャに書き込んでおき、ランタイムでサンプリングしている: https://x.com/alexanderameye/status/1663523972485357569
SDFは本当に強力だ
同じ方法をラディアンスカスケード(radiance cascades)まで拡張するとさらに速くできる可能性があり、かなり面白い
いつか スタイライズされた3Dグラフィックスを研究開発プロジェクトとして深く掘り下げてみたい。
最近かなり進展はあったものの、まだ簡単に手に入れられる成果が多く残っているように見える。
カメラが遠ざかるとき、トゥーンレンダリングされた3Dモデルのディテールをどう減らすのか、また、よりスタイライズされた見た目と、あまりスタイライズされていない見た目の間をどう自然に遷移させるのかが気になる。
手描き2Dアニメーションの水彩背景を3Dシーンとして説得力ある形でレンダリングできるのか、スクリーン空間で筆跡や紙の質感をどう滑らかにアニメーションさせるのかも課題だ。
煙、炎、木、草、泥、雨、毛、水のような要素を、スタイライズされた3Dゲームでどう表現すべきかも未解決だ。
手描きアニメーションのように、現在のカメラ角度でより良く見えるようモデルを微妙に変形する作業を、自由カメラのゲームで自動化できるのかも気になる。
スタイライズされたレンダラーに合う理想的なメッシュエディターや背景エディターはどのようなものか、物理的に正確な3Dサーフェスやリグが本当に必要なのか、それとももっと抽象的に定義できるのかも大きな問いだ。
単純な3Dモデルからレトロなピクセルアートをレンダリングしてプロシージャルな2Dゲームを作れるのかも興味深いし、2つのメッシュが偶然交差したときに、スタイライズ表現を使ってその交差を目立ちにくくできるかもしれない。
問いだけで10人分のキャリアを埋められそうだが、むしろそれは良いことだと思う。
Kuwaharaフィルターも、たいていの人には十分それらしく見える。
スタイライズされたレンダラー向けの編集ツールは、Blender + Rigify + シェイプキー + ちょっとしたドライバーの魔法だけでも、自分の用途には十分だった。
Blenderのテクスチャリングは面倒だが、趣味レベルなら耐えられるし、非写実的レンダリングの制御がもっと必要ならDillonGoo Studioのフォークのほうがよいかもしれない: https://www.dillongoostudios.com/gooengine
3Dモデルからピクセルアートを作るのは、アニメーションやモデルを低解像度でレンダリングする形で試したことがあり、結果は悪くなかったが試行錯誤が必要だった。
ピクセルのちらつきのようなものをなくすために、後処理をさらに洗練させた事例もあったと記憶している。
低解像度: https://x.com/Navy_Green/status/1525564342975995904
安定化: https://x.com/Navy_Green/status/1693820282245431540
キャリアをVRアプリで始めたが、市場のほうが良かったのでほどなくWeb開発に移った。
こういう記事を見ると、その分野が懐かしくなる。
3Dグラフィックス、衝突、シェーダーを扱う仕事には、他の分野では見つけにくい魔法のような感覚があった。
実質的に世界を作り、物理を再現しているようなものだし、数学も他のプログラミング分野よりはるかに実用的で、頻繁に登場する。
仕事の満足度はずっと高く、テーマの深さも上限も高い。
4年目になるが、Web開発のときのような退屈の壁にぶつかったことがない。
月曜日に出社してエンジンエディターを開き、作業中の小さな世界がレンダリングされるのを見ながら、次にどんな格好いい機能を入れようかと考えることほど良いものはない。
逆にテクニカルアートの分野では、成果物を吸い上げてニューラルネットワークに食わせると、面白くやりがいのある部分が消え、専門的な作業には従来の退屈な仕事だけが残るか、むしろ増える。
技術セット全体の価値も下がり、テック業界の人たちは当事者よりその仕事をよく分かっているかのように振る舞い、シリコンバレー企業が元のデータを作った人々に報酬も払わず、Fの価格でC-の成果物を出して市場を壊したことを喜ばないのだと非難する。
長所と短所を考えると、Web開発へ移った選択は正しかったと思う。
私のゲーム Astral Divide では、記事にはない手法を作りました。
Blurred Buffer に似ていますが、ブラーパスは行わず、アンチエイリアシングが作った境界線を利用します。
透明な黒背景の上に物体を不透明な白で描画し、フラグメントシェーダーでアルファチャンネルが完全に不透明でも完全に透明でもないピクセルだけを、ハードコードしたしきい値でフィルタリングします。
結果は十分に良く、性能コストも低く、実装も非常に単純です。
以前、動画編集ソフトでクロップツールを実装したとき、編集中に切り取られた領域の外側を一時的にぼかす必要がありました。
そこで高価なブラーパスをまた入れたくなかったので、ミップマップバイアスだけを変えて低解像度テクスチャがレンダリングされるようにし、テクスチャフィルタリングが無料で全部やってくれました。
PowerPoint の似たようなクロップ時のブラー効果と比べてみるとほとんど同じで、むしろ PowerPoint には少しカラーバンディングがありましたが、私の実装にはありませんでした。
似たように、フル解像度のアンチエイリアシングがない状況で、キャンバスの大半が回転可能な 2D 長方形だったため、端のジャギーが見えていました。
全画面アンチエイリアシングを有効にする代わりに、すべての長方形を少し拡大し、UV 座標を比例して縮小して、見える端が実際の 3D 長方形の内側に来るようにしたところ、今回もテクスチャフィルタリングが無料で解決してくれました。
それともアルファを維持して色だけを設定し、アウトラインは薄いけれどジャギーにはならないようにする方式でしょうか?
ノートが素晴らしいです。
最近、エッジ検出のアプローチを探していて、Mars First Logistics の開発者が作った良い方法を見ました: https://www.reddit.com/r/Unity3D/comments/taq2ou/improving_e...
記事も素晴らしく、結果も非常に見栄えが良いです: https://ameye.dev/notes/rendering-outlines/edge-detection/co...
オランダの漫画 Franka の一場面のように見えます。
本当に優れた記事で、読書体験も素晴らしいです。
難しい概念を誰にでも理解できる言葉で説明しており、図表と例も良く、余白とタイポグラフィのおかげで可読性も最高レベルです。
現在のテーマを作ることになったきっかけが気になりますし、エンジニア中心の出版プラットフォームを作ってみようと考えたことはなかったのかも気になります。
ソフトウェアにおけるテクニカルアートは、間違いなく私の初恋です。
Godot で後処理効果用のコンピュートシェーダーパイプラインがもっと簡単になるといいですね。
現在のコンポジタープラグイン構成は、定型コードがかなり多いです。
このリポジトリは Godot の後処理の良い例です: https://github.com/sphynx-owner/JFA_driven_motion_blur_demo
この効果を初めて見たのは、Dreamcast の Wacky Races だったと記憶しています。
当時は、この効果を初めて導入したゲームで、開発者たちが事実上発明した、というような宣伝が多くありました。
それが本当だったのか宣伝上の誇張だったのかは分かりませんが、ゲーマーとして初めて体験したのは確かにそのときでした。
Dreamcast は正直、この効果を高品質でリアルタイム処理できた最初のハードウェアでした。
Dreamcast ゲーム Looney Tunes: Space Race のセルシェーディング効果を開発しましたが、Dreamcast 開発キットを受け取った最初の週にすぐ作りました。
Wacky Racers を作った Infogrames Sheffield は、私たちの実装の初期版を見て、似た効果を自分たちのゲームに追加しました。
見た目は良かったものの、制作の後半に入ってからだったため、私たちのゲームのようにその効果に合わせて最適化されてはいませんでした。
Jet Grind Radio チームも独自に同じ効果を作り、私たちより先に発売しました。
アルゴリズムはまったく同じでしたが、使い方が違っていて、彼らは不均一で幅広くギザギザしたアウトラインを積極的に受け入れており、Sheffield と私たちはより均一で伝統的な美術スタイルに合わせようとして、その特徴と格闘していました。
約 1 年後、Xbox の Dragons Lair 3D では、エッジ検出セルシェーディングをリアルタイムで走らせる方法を誰かが見つけたようです。
Dreamcast でそのアプローチを試験実装してみましたが、ゲームを動かしながら複数のキャラクターに同時に適用するには、性能がまったく足りませんでした。
Xbox がより強力だったからなのか、アルゴリズムがより賢かったからなのかは分かりませんが、結果は否定できません。
本物の手描き漫画のように見えるゲームを作りたいなら、個人的にはその方式が今でも最高品質の方法だと思います。
いつかまた実装してみる口実を見つけたいですし、今なら性能は問題にならなさそうです。
かなりすごい偶然です。
シェーダープログラミングと 3D レンダリングをぜひ始めなければ。
こういう記事は本当に良いですし、実際にシェーダーを書けるようになれたらいいですね。