9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

グレンジAdvent Calendar 2024

Day 1

[Unity]モデルをイジらずポスプロも要らず。ハードエッジをサクッと解決する背面法アウトラインの作り方

Last updated at Posted at 2024-11-30

グレンジ Advent Calendar 2024 1日目担当の、flankidsです。
普段は操作、アニメーション、カメラワークなどなどで遊び心地を作ることを主にやっています。楽しいものを作るのが好きです!

今回は、ハードエッジ部分のアウトラインの問題を手軽に解決する機能について書きました。
もしこの記事がお役に立てたら「いいね」を押してもらえると、とても励みになります!

ハードエッジのアウトライン

3Dモデルのアウトライン表現では、たびたびハードエッジの表示破綻が問題になります。

image.png

▲ 曲線的な部分はイイ感じにアウトラインがついているけど、グローブのトゲトゲ辺りの線がちょっと汚い‥‥

image.png

▲ キバ、トサカが全体的にアウトラインが上手くいっていないし、舌の辺りは特に変な感じに

シンプルな立方体のモデルでも発生するほどのメジャーな課題なので、これに対する解決法も既にたくさんのノウハウがあります。
しかしよくある解決方法では、3Dモデルを手直ししたり、アウトラインの描画をポストプロセス任せたりと、コスパや取り回しが悪くてちょっと手を出しづらいものが多い印象です。

もっとお手軽に解決できないものか‥‥


「お手軽」の条件として

  • メジャーな背面法を使ったアウトラインシェーダー
  • 3Dモデルを3DCGソフトなどで手直ししない
  • ポストプロセスを使わない

この3つを満たしつつハードエッジ問題を解決する方法を探していたところ、中国のテクニカルアーティストの方が公開しているオープンソースが非常に有用だったので、紹介を兼ねて使い方をまとめました。

キレイになりました

今回の方法を適用すると、こんな感じになります。

image.png

image.png

普通の方法だとアウトラインが割れまくるトゲトゲした3Dモデルに対しても、一定の安定感を持ったアウトラインを付けることができました。

どんな仕組み?

3Dモデルに対して、アウトライン用にスムース化した法線情報を自動生成して頂点カラーに書き込んでいます。
(頂点カラーではなく任意のUVチャンネルに書き込むことも可能)

image.png

法線のスムース化は、公開されているオープンソースのOutline Normal Smootherというエディタ拡張を利用します。

【Job/Toon Shading Workflow】自动生成硬表面模型Outline Normal

AssetPostprocessorのコールバックによって実行される機能で、特定のフォルダ内やファイル名のモデルに働きます。

シェーダーについては頂点カラーに書き込んだアウトライン用の法線データを使う以外は一般的な背面法のアウトラインシェーダーでOKなので、応用的な実装もやりやすそうです。

スムース法線アウトラインの生成

【手順1】Package Managerからパッケージをインポート

① Unity RegistryからCollectionsをインポート

image.png


② Add package from git URL...からOutline Normal Smootherをインポート
  → git URL: https://github.com/JasonMa0012/OutlineNormalSmoother.git

image.png


Outline Normal SmootherCollectionsMathematicsと依存関係にあるため、先にそれらをインポートします。
Collectionsをインポートしたら関連してMathematicsもインポートされるはずですが、もしインポートされていなければそちらも手動でインポートしましょう。

また、Collectionsはver2.1.0以上、Mathematicsはver1.2,0以上が必要です。

【手順2】スムース法線を使うアウトラインシェーダーを割り当て

以下のシェーダーをマテリアルに設定してください。

----------------------------------------
BuiltInRP用: SmoothOutline.shader(折りたたみ)
------------------------------------------
SmoothOutline.shader
Shader "Custom/SmoothOutline" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        _OutlineColor( "Outline Color", Color ) = ( 0, 0, 0, 0 )
        _OutlineWidth( "Outline Width", Range(0, 1) ) = 0.025
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass {
            Name "BASE"

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
        
        Pass {
            Name "OUTLINE"

            //描画面を裏側にする
            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            float4 _OutlineColor;
            float _OutlineWidth;

            struct appdata {
                float4 vertex : POSITION;
                float4 color : COLOR;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };

            struct v2f {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v) {
                v2f o;

                // 頂点カラーにベイクされた法線を格納(タンジェント空間)
                float3 smoothNormalTS = v.color.xyz * 2 - 1;

                // オブジェクト空間の情報
                float3 normalOS = v.normal;
                float3 tangentOS = v.tangent.xyz;
                float3 binormalOS = cross(normalOS, tangentOS) * v.tangent.w * unity_WorldTransformParams.w;

                // オブジェクト空間 → タンジェント空間 の変換行列
                float3x3 objectToTangentMatrix = float3x3(tangentOS.xyz, binormalOS, normalOS);
                // タンジェント空間 → オブジェクト空間 の変換行列
                float3x3 tangentToObjectMatrix = transpose(objectToTangentMatrix);

                // タンジェント空間のベクトルをオブジェクト空間に変換
                float3 smoothNormalOS = mul(tangentToObjectMatrix, smoothNormalTS);

                // 頂点座標をスムース法線の方向に押し出す
                float3 vertexOS = v.vertex.xyz + smoothNormalOS * _OutlineWidth;
                o.vertex = UnityObjectToClipPos(vertexOS);

                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                return _OutlineColor;
            }
            
            ENDCG
        }
    }
}

シェーダーを適用するとアウトラインが表示されますが、まだスムース法線の生成が済んでいないため下図のように荒れた線になっています。

image.png

【手順3】スムース法線を頂点カラーに書き込み・確認

スムース法線を生成したい3Dモデルのファイル名の末尾に_Outlineまたは_outlineを付けると、自動的に頂点カラーにスムース法線が書き込まれます。

image.png

これで完了です!

補足

◆ 対象ファイルのファイル名・ファイルパスルールの設定

デフォルトではファイル名の末尾に_Outlineまたは_outlineを付けることで処理対象になりますが、下記のようにOutlineNormalImporter.shouldBakeOutlineNormalのデリゲートを上書きすることでルールを変更できます。

CustomOutlineNormalImporter.cs
using System.Diagnostics.CodeAnalysis;
using System.IO;
using OutlineNormalSmoother;
using UnityEditor;

internal static class CustomOutlineNormalImporter {
    [InitializeOnLoadMethod]
    private static void RegisterEvent() {
        OutlineNormalImporter.shouldBakeOutlineNormal = ShouldBakeOutlineNormal;
    }

    private static bool ShouldBakeOutlineNormal(string assetPath, [MaybeNull] AssetPostprocessor assetPostprocessor) {
        bool shouldBakeOutlineNormal = false;

        // ファイル名の末尾が"_SmoothOutline"か否か
        shouldBakeOutlineNormal |= Path.GetFileNameWithoutExtension(assetPath).EndsWith("_SmoothOutline");

        // パスに"/SmoothOutline/"が含まれるか否か(場所を問わず"SmoothOutline"フォルダ内であるか否か)
        shouldBakeOutlineNormal |= assetPath.Contains("/SmoothOutline/");
        
        return shouldBakeOutlineNormal;
    }
}

InitializeOnLoadMethodのAttributeで、エディタ起動時にOutlineNormalImporter.shouldBakeOutlineNormalのデリゲートを上書きしています。
このデリゲートはファイルパスのstringとAssetPostprocessorを引数に持ち、返り値がtrueのときに対象のファイルをスムース法線生成の処理対象にします。

上記では新しく定義したShouldBakeOutlineNormal関数を登録していて、ファイル名の末尾が"_SmoothOutline"であるか、場所を問わず"SmoothOutline"フォルダ内にあるファイルがスムース法線生成の処理対象となるように設定しています。

◆ 頂点カラー以外にスムース法線を格納するには

下記のようにOutlineNormalBacker.onSaveToMeshのデリゲートを上書きすることでスムース法線の書き込み先を変更できます。

CustomOutlineNormalImporter.cs
using OutlineNormalSmoother;
using Unity.Collections;
using UnityEditor;
using UnityEngine;

internal static class CustomOutlineNormalImporter {
    [InitializeOnLoadMethod]
    private static void RegisterEvent() {
        OutlineNormalBacker.onSaveToMesh = SaveOutlineNormalToMesh;
    }

    private static void SaveOutlineNormalToMesh(Mesh mesh, ref NativeArray<Color> bakedColors, ref NativeArray<Vector3> smoothedNormalTangentSpace) {
        var smoothedNormalTS = smoothedNormalTangentSpace.ToArray();
        // UV2にスムージングされた法線を保存
        mesh.SetUVs(2, smoothedNormalTS);
    }
}

この例では、頂点カラーではなくUVチャンネル2にスムース化した法線を格納しています。

注意
スムース法線の格納先を変えた場合、シェーダーも編集が必要です

SmoothOutline.shader
struct appdata {
     float4 vertex : POSITION;
-    float4 color : COLOR;
+    float4 uv2 : TEXCOORD2;
     float3 normal : NORMAL;
     float4 tangent : TANGENT;
};
SmoothOutline.shader
v2f vert (appdata v) {
     v2f o;

-    // 頂点カラーにベイクされた法線を格納(タンジェント空間)
-    float3 smoothNormalTS = v.color.xyz * 2 - 1;
+    // UV2にベイクされた法線を格納(タンジェント空間)
+    float3 smoothNormalTS = v.uv2.xyz;

◆ スムース法線生成の結果はgit差分として出てこない

エディタ拡張でMeshに書き込まれた情報はfbxやmetaファイルではなく、エディタのキャッシュとして保存されているっぽいです。
そのため、ファイルに対しての差分は出てこないし、エディタを閉じるたびに揮発してしまうようです。

AssetPostprocessorで機能が実装してあるのはそのためで、これならインポートのコールバックで都度処理が施されるので安心です。たぶん

逆に [MenuItem] のAttributeを付けた関数を右クリックメニューから呼んで、ファイルパスやファイル名ではなく選択したファイルに処理を施すような仕組みにしてしまうと、生成結果が揮発してしまうためNGだと思います。

◆ SmoothOutline.shaderについて

このシェーダーのポイントは、2パス目の"OUTLINE"のパスです。

SmoothOutline.shader - vert関数
v2f vert (appdata v) {
    v2f o;

    // 頂点カラーにベイクされた法線を格納(タンジェント空間)
    float3 smoothNormalTS = v.color.xyz * 2 - 1;

    // オブジェクト空間の情報
    float3 normalOS = v.normal;
    float3 tangentOS = v.tangent.xyz;
    float3 binormalOS = cross(normalOS, tangentOS) * v.tangent.w * unity_WorldTransformParams.w;

    // オブジェクト空間 → タンジェント空間 の変換行列
    float3x3 objectToTangentMatrix = float3x3(tangentOS.xyz, binormalOS, normalOS);
    // タンジェント空間 → オブジェクト空間 の変換行列
    float3x3 tangentToObjectMatrix = transpose(objectToTangentMatrix);

    // タンジェント空間のベクトルをオブジェクト空間に変換
    float3 smoothNormalOS = mul(tangentToObjectMatrix, smoothNormalTS);

    // 頂点座標をスムース法線の方向に押し出す
    float3 vertexOS = v.vertex.xyz + smoothNormalOS * _OutlineWidth;
    o.vertex = UnityObjectToClipPos(vertexOS);

    return o;
}

appdataでCOLORセマンティクスから取得した頂点カラーを、法線として扱っています。
この法線はタンジェント空間での法線になっているため、変換行列を作ってオブジェクト空間に変換した後、頂点押し出しに使用しています。

※ パッケージにサンプルとして添付されているPackages/Outline Normal Smoother/Test内のShaderGraphに準拠して作っています

image.png

所感

ハードエッジのアウトライン破綻問題はたびたび困っていたものの、今回一つ手段が持てたことで心の余裕ができた気がします。よかった

Outline Normal Smootherは下記の講演アーカイブで知りました。

さらに、ちょうど今年の10月末に上記講演が参照していたOutline Normal Smootherがブラッシュアップ・オープンソース化されたようで、ありがたかったです。感謝‥‥!


調べる過程で、モデルの手直しもポスプロも使わない他の方法があることも知りました。

現時点では手法として枯れている背面法に乗っかれるOutline Normal Smootherがベターだと感じていますが、工夫次第でアウトラインはいろんな手段があるんだなと改めて感じました。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?