【Maya/MEL】法線をUVセットに格納し、Unityシェーダーでライティングに使う

【Maya/MEL】法線をUVセットに格納し、Unityシェーダーでライティングに使う

第二事業部 エンジニアの永留(ながとめ)と申します。

業務にて、Maya向けのスクリプト(MEL)を書く機会がありましたので紹介したいと思います。

今回紹介するMELスクリプトは、
「3Dモデルのライティング用法線をUVセットに格納する」
といったものになります。

 

環境

  • Autodesk MAYA 2023
  • Unity 2021.3.0f1
  • Built-in レンダーパイプライン

3Dモデルの法線をUVセットに格納したい

 

業務にて、3Dキャラクターのアウトラインを整えるために法線を調整するということが行われていたのですが、
その影響でキャラクターの陰影が汚くなるという問題が起きていました。

これを解決するため、
「3Dモデルのライティング用法線をUVセットに格納し、UVセットの法線を使ってライティングを行いたい」

という話になりました。

法線をUVセットに格納するという機能はMayaの標準機能には存在しないため、

Mayaのスクリプト(MEL)を書き、「法線をUVにコピーする」という処理を組む必要があります。

 

記事で紹介する手法

 

今回の記事では、法線ベクトルをUVに格納し、Unity上でUV内の法線を使ってライティングに使うまでの一連の流れを紹介したいと思います。

1. srcの法線をコピーし、dstのUVセットへ格納する
2. dstのUVセット内の法線を使ってライティングを行う

(※ライティング用の法線をもつ3Dモデルを src、アウトライン用に法線を調整した3Dモデルを dstとします)

 

「UVセット」は Mayaの言葉

 

以下は、Maya の画面になります。


Maya上では、UVのことをUVセットと呼び、Mayaユーザーが独自の名前を付けて管理することができます。

 

UVセットエディタ

「UVセット」を作成する

 

normal_XYnormal_Zという名前のUVセットを作成し、法線情報を格納することにします。

法線情報(x, y, z)を格納したい場合、UVセットを2つ用意する必要があります。
(UVセットは2つのfloat値しか持てないため)

MELスクリプト

MELスクリプトを実行する前に、Mayaのシーンにsrcオブジェクトとdstオブジェクトを作成しておきます。

 

MELを実行すると、srcオブジェクトの法線情報がコピーされ、
dstオブジェクトのUVセット「normal_XY」「normal_Z」に格納されます。

// *****************************************************************
// * $srcObject の法線を、$dstObject のUVセットに格納するスクリプト 
// *****************************************************************/

// 指定したオブジェクトの頂点数を取得
proc int getVertexCount(string $objName){
    string $obj_Vtx[] = `polyListComponentConversion -toVertex $objName`;
    string $vertices[] = `filterExpand -sm 31 $obj_Vtx`;
    int $vertexCount = size(`filterExpand -sm 31 $obj_Vtx`);
    return $vertexCount;
}

// $srcObjectName の法線を $dstObjectName へコピーする
proc transferNormalToUV(string $srcObjectName, string $dstObjectName, string $uvSetName) {

    // srcオブジェクト 選択
    string $targetObject = $srcObjectName; 
    select -r $targetObject;

    // 頂点数を取得
    int $vertexCount = getVertexCount($srcObjectName);
    int $dstVertexCount = getVertexCount($dstObjectName);

    if ($vertexCount != $dstVertexCount) {
        // print ("difference vertexCount \n" + "src: " $vertexCount + "\ndst:" + $dstVertexCount + "\n");
        print ("difference vertexCount");
        return;
    }

    // 法線をコピーして、配列にまとめる
    print("Copy normals from : " + $targetObject + "\n");
    vector $srcNormals[];
    for ($i = 0; $i < $vertexCount; $i++) {

        // 頂点を選択
        select -r $targetObject.vtxFace[$i];

        // 法線を取り出す
        vector $normals[] = `polyNormalPerVertex -q -normalXYZ`;
        vector $n = $normals[0];
        $srcNormals[$i] = $n;
    }
    print "Done\n";

    // オブジェクトを選択
    $targetObject = $dstObjectName;
    select -r $targetObject;

    print ("copy to : " + $targetObject + "\n");

    // UVセット名
    string $srcUvSet = "map1"; // コピー元
    string $uvSetName1 = $uvSetName + "_XY"; // UVセット名 (法線のXYを格納)
    string $uvSetName2 = $uvSetName + "_Z"; // UVセット名 (法線のZを格納)

    
    for ($u in `polyUVSet -q -auv`) {
        if ($u == $uvSetName1)
            polyUVSet -delete -uvSet $uvSetName1;
        if ($u == $uvSetName2)
            polyUVSet -delete -uvSet $uvSetName2;
    }


    // UVセットを作成する
    print ("Create UV Set: " + $uvSetName1);
    print ("Create UV Set: " + $uvSetName2);

    polyUVSet -copy -uvSet $srcUvSet -nuv $uvSetName1;
    polyUVSet -copy -uvSet $srcUvSet -nuv $uvSetName2;

    // UVセットを選択
    polyUVSet -cuv -uvSet $uvSetName1; 

    for ($i = 0; $i < $vertexCount; $i++) {
        // 頂点を選択
        select -r $targetObject.vtx[$i];
               
        vector $n = $srcNormals[$i];
        
        // 頂点UVに法線x,y を代入 (選択している頂点のUVに値が入る)
        // MayaとUnityの座標軸は、x軸が逆方向を向いているようなのでマイナスをつける
        polyEditUV -relative false -uValue (-$n.x)  -vValue ($n.y);   
    }

    // UVセットを選択
    polyUVSet -cuv -uvSet $uvSetName2; 

    for ($i = 0; $i < $vertexCount; $i++) {
        // 頂点を選択
        select -r $targetObject.vtx[$i];
               
        vector $n = $srcNormals[$i];
        
        // 頂点UVに法線z を代入 (選択している頂点のUVに値が入る)
        polyEditUV -relative false -uValue ($n.z) -vValue (0);  
    }

    print ("### Complete ###");
}

proc main(){
    // 法線をUVセットにコピーする
    transferNormalToUV("src", "dst", "normal");
}

{
    main();
}

使用上の注意点

【注意点1】srcオブジェクトは UVセット「map1」を持っている必要があります。

【注意点2】srcとdstのモデルの構造が一致している必要があります。

(例えば、立方体モデルの法線を球モデルにコピーするといったことはできません)

 

「法線をアウトライン用に調整したけど、ライティングは調整前のものを使いたい」

といったケースでの使用を想定しています。

 

実行結果

MELを実行すると、dstオブジェクトにUVセット「normal_XY」と「normal_Z」が作成されます。

 

これらのUVセットに、法線情報が格納されます。

FBXをUnityに取り込む

FBXエクスポートし、Unityにインポートします。
UVを確認すると、法線情報が格納されていることがわかります。

Maya上で2番目にあるUVセットは Channel 1 に入り、
3番目のUVセットは Channel 2 に入ります。

 

 

UVセットに入った法線を取り出す

今回のケースでは、法線のXY成分は UVのChannel 1、法線のZ成分は UV の Channel 2 に入っています。

appdata構造体にて、 TEXCOORD1TEXCOORD2セマンティクスをつけることで、
これらの情報を取り出すことができます。

// 頂点シェーダーへの入力
struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0; // UVのChannel 0 がここに入る (テクスチャ座標)
    float2 uvNormalXY : TEXCOORD1; // UVのChannel 1 がここに入る (法線のXY成分)
    float2 uvNormalZ : TEXCOORD2; // UVのChannel 2 がここに入る (法線のZ成分)
};

UVセットに入った法線を確認するシェーダーコード

法線が正しくUVに格納されているのか確認するため、法線を表示するようなシェーダーを書いてみました。

Shader "Unlit/Show UvSet Normal"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            // vertへの入力
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;

                // UVセットに格納されている法線情報
                float2 uvNormalXY : TEXCOORD1;
                float2 uvNormalZ : TEXCOORD2;
            };

            // fragシェーダーへの入力
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION; 
                float3 normal : TEXCOORD1; // 法線
            };


            v2f vert (appdata v)
            {
                v2f o;

                // 頂点座標の変換
                o.vertex = UnityObjectToClipPos(v.vertex);

                // UVセットから法線を取り出す
                float3 normal = float3(v.uvNormalXY.xy, v.uvNormalZ.x);

                // 座標変換 (オブジェクト空間 -> ワールド空間)
                o.normal = UnityObjectToWorldNormal(normal);

                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                return float4(i.normal, 1);
            }
            ENDCG
        }
    }
}

結果

以下のように法線情報が表示されます。

 

 

UVセットの法線をライティングに使う

法線とライトベクトルの内積を計算するようなシェーダーを組んでみました。

Shader "Unlit/UV Normal Lighting"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc" // _LightColor0を使うために必要

            // vertへの入力
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;

                // UVセットに格納されている法線情報
                float2 uvNormalXY : TEXCOORD1;
                float2 uvNormalZ : TEXCOORD2;
            };

            // fragシェーダーへの入力
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION; 
                float3 normal : TEXCOORD1; // 法線
            };


            v2f vert (appdata v)
            {
                v2f o;

                // 頂点座標の変換
                o.vertex = UnityObjectToClipPos(v.vertex);

                // UVセットから法線を取り出す
                float3 normal = float3(v.uvNormalXY.xy, v.uvNormalZ.x);

                // 座標変換 (オブジェクト空間 -> ワールド空間)
                o.normal = UnityObjectToWorldNormal(normal);

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                float dotNL = dot(i.normal, _WorldSpaceLightPos0.xyz);
                return half4(dotNL * _LightColor0.xyz, 1);
            }
            ENDCG
        }
    }
}

 

おわりに

Mayaで法線をUVに格納するという情報は、ネット上を探しても全くと言って良いほど出てきません。
この記事の内容がどなたかのお役に立てればと思います。