UE4でのPlugin開発記録メモ(マテリアルノードを繋ぐ沼編)

  • 3
    いいね
  • 0
    コメント

この記事は、裏 Unreal Engine 4 (UE4) Advent Calendar 2016 22日目の記事です。

今回はUE4にてC++@プラグインでMaterialのAssetを生成しマテリアルノードを繋ぐ為の実装技術について基本編のみ解説します。
※一年ぶりの記事とは言ってはいけない…

昨年の3部作:
- UE4でのPlugin開発記録メモ(バイナリ配布前に確認するべきこと)
- UE4でのPlugin開発記録メモ(FMessageDialogでダイアログ表示編)
- UE4でのPlugin開発記録メモ(PluginBrowserのTemplate作成機能を使って簡単にpluginを作るよ)

※今回は昨年三部作のうち、「MessageDialog」と「Template作成機能」の内容が暗黙で使われています。

徒然なる前置き(くだらないので忙しい人は、飛ばしてください)

今更没ネタを引っ張り出してきた理由は、1年半以上たってもUE4界隈にエディタ拡張でマテリアルノードのPin接続とかの技術が予想通りまったく出てこない。
マテリアルを拡張してバッファ云々とかはありますが、今回のようにC++でマテリアル接続したソースはないのでは?と思い裏ネタで記事にしました。

相変わらず、手探りでやっていることのため、誤った使い方をしている場合もありますので参考程度として見ていただくようお願いします。
実はVer4.7.x系時代(2015/5付近)に調査したころネタがベースになってますが、Materialノード接続に関しては4.14の最新版(2016/12/21)でもほとんど変わった記憶がないのでまだ使える感じです。記事自体は4.14向けに再度書き直しています。

で、C++拡張でマテリアルノードのPin接続できるようになると何がいいの?

  1. 公式がサポートしていないフォーマットのモデル・マテリアルインポートプラグインを自前で作れます。(気まぐれ開発中だけど実績あり)
  2. 第一人者?になれるかも(資料がまともにないのでソースの実装例から推測するしかないです(もっと資料を…))
  3. ゲーム開発ができなくなるエディタ拡張の保守沼に浸れます(うれしくない)
  4. 公式エンジンVerUpによるAPI変更でメッシュ周りは毎回死にますが、マテリアルは1年半以上たっても(Verが7つ程上がっても)大改造は殆どなかった(これはありがたい)
  5. 最終的には、ベースマテリアルをPlugin内Assetとして提供して、マテリアル複製とマテリアルインスタンスが確実なのでは?という境地にたどり着きます(保守性と拡張性が向上します。その話はおいおい。)
  6. 結果、C++で手動Pin接続は需要がない技術では?と気が付きます。(HAHAHA・・・)

とまあ、くだらない前置きはここまでにして、本題です。



本編:MaterialノードのPin接続のはなし

サンプルコードとして、Editorのプラグインにして実装しました。
/Game/フォルダ配下に「M_TempMaterial」assetをランダム作成する例です。
なお、Materialの中身はテキトーなので意味はありません。

使い方は下記の画像通り、「TempMatCreate」ボタンを押すとMaterial assetが生成されます。
adcl_ent.jpg

※Github
https://github.com/bm9/PluginSample_TempMatCreate

もしパッケージビルドに失敗する場合は本プラグインを無効にしてください

マテリアルノードのクラスとか

ざっくりですが、
Materialノードの基底クラスは「UMaterialExpression」クラスで定義されています。
その中で、OutputPinのクラスは「FExpressionOutput」クラスで、
InputPinのクラスは「FExpressionInput」クラスになります。
ストレートに言えば、FExpressionOutputとFExpressionInputで定義されたPinを接続すればいいだけです。
訂正):簡単に言うと、接続させるにはFExpressionInputのIn側(接続先)PinのConnect関数を使って、Output側のPin番号とOut側(接続元)のUMaterialExpressionを紐づけさせるだけです。

※UMaterialExpressionリファレンス
https://docs.unrealengine.com/latest/INT/API/Runtime/Engine/Materials/UMaterialExpression/index.html
※FExpressionOutputリファレンス
https://docs.unrealengine.com/latest/INT/API/Runtime/Engine/FExpressionOutput/index.html
※FExpressionInputリファレンス
https://docs.unrealengine.com/latest/INT/API/Runtime/Engine/FExpressionInput/index.html

Material assetをつくるよ

UMaterial クラスで新規のマテリアルアセットを作成します。
下記のコードは一例ですが、こんな感じでassetを作ります。
asset登録とかについては別の機会があればその時解説で。

FactoryMaterialAsset(一部のみ抜粋)
    /*Assetのファイル名 */
    FString MaterialFullName = FString("M_") + TargetBasdName;

    //禁止文字を削除する(もし禁止文字が含まれていた場合のフェイルセーフ)
    MaterialFullName = ObjectTools::SanitizeObjectName(MaterialFullName);

    /*ディレクトリ名とファイル名*/
    FString BasePackageName = FPackageName::GetLongPackagePath(ParentObjName) / MaterialFullName;
    BasePackageName = PackageTools::SanitizePackageName(BasePackageName);

    // The material could already exist in the project
    FName ObjectPath = *(BasePackageName + TEXT(".") + MaterialFullName);

    /* ターゲットMaterial */
    UMaterial* UnrealMaterial = NULL;

    const FString Suffix(TEXT(""));
    FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
    FString FinalPackageName;
    AssetToolsModule.Get().CreateUniqueAssetName(BasePackageName, Suffix, FinalPackageName, MaterialFullName);

    UPackage* Package = CreatePackage(NULL, *FinalPackageName);

    // create an unreal material asset
    auto MaterialFactory = NewObject<UMaterialFactoryNew>();

    UnrealMaterial = (UMaterial*)MaterialFactory->FactoryCreateNew(
        UMaterial::StaticClass(), Package, *MaterialFullName, RF_Standalone | RF_Public, NULL, GWarn);

    if (UnrealMaterial != NULL)
    {
        // Notify the asset registry
        FAssetRegistryModule::AssetCreated(UnrealMaterial);

        // Set the dirty flag so this package will get saved later
        Package->SetDirtyFlag(true);
    }

※UMaterialクラスについては公式APIリファレンスを参照
https://docs.unrealengine.com/latest/INT/API/Runtime/Engine/Materials/UMaterial/index.html

そのマテリアルのアセットに対し、Nodeを包含させるように追加していくと、
普段、UE4のエディタ上でマテリアルアセットを編集している時のように、ノードがエディタ上に表示されるようになります。

UmaterialクラスでNode接続するために必要な2つのこと@本題

以下の2つを行えば最低限のPin接続はできます。
なお実際のC++実装例は最後に記載しています(例としてプラグインにも実装済み)。

  • UnrealMaterial->Expressions.Add(ノード変数);でノードをMaterialアセット内に追加する

  • UnrealMaterial->BaseColor.Expression = ノード変数;でInput先(例:BaseColor)にOut元(ノード変数)とリンク接続する
     上記の場合、ノード変数の最上位Pinとつなぐことになります。
    adcl_mat_node_out-in.jpg
    ※AddやMultiノードのInputピンに接続したい場合は、Addノード変数.A.Expression = ノード変数;って定義でもできます。結局のところ、使うnode(クラス)によって変数が違うのでそこは臨機応変に。
     
    捕捉):厳密には「ノード変数①.Inputピン.Connect(ノード②Out側の接続元Pin番号, ノード変数②);」が良いと思います。(※ノード変数①をInput側、ノード変数②をOutput側とした場合。)
    理由はソースコードを読む限り「Expression = ノード変数;」相当の処理も実装しつつ、Output側PinのMask情報?を接続先(In側Pin)のMask情報に代入しているので、多分こっちがベストのはず。
    この記事を書いた当初は、まだ確証がないので多分Connect関数を使う方がいいかも?ってレベルしかわかってないですので注意。いずれにせよ、「Expression = ノード変数;」が少なくとも必須なのは変わりないかと。
     
    なお、Vec4などPinが複数ある場合は個別指定が必要(下記)。
    ※上記以外の手段として、ノード変数①.Connect(ノード①Out側のPin番号, ノード変数②);で接続する事もできます。(図中の左がノード①、右がノード②とした例)

    訂正):記載誤りがあったので削除。
     
     ちなみに、後者のConnect関数を使うことが必要なケースはTextureSampleNodeのαピンとリンク接続したい場合にはこちらが有効と思います。
    ※なお、どちらでもいいのか後者のみ推奨されているのか?そこは調査不足で不明。

    訂正):恐らくべた書きよりかは、Input側PinのConnect関数を使う方がよいかと。

エンジンコードでのサンプル@実装例

FBXファイルのInport処理でMaterial生成があります。
本記事はそこの処理をベースに作成しています。
例;)
UnrealEngine-4.14.0-release\Engine\Source\Editor\UnrealEd\Private\Fbx\FbxMaterialImport.cpp
・CreateAndLinkExpressionForMaterialProperty関数
・FixupMaterial関数

ノード関係@UMaterialExpressionの派生クラスについて

AddやMultiノード、Vector4ParamやTextureSampleノードなどを実際に追加し
Pin接続することになりますが、全て先ほど記載したUMaterialExpressionを基底クラスとして定義されています。

UMaterialExpressionVectorParameter

よく見るノードの一つである、ベクターパラメータのノードです。
DefaultValue.RやG,B,Aに値(0-1.0f)を入れる必要があります。

/* パラメータ設定が面倒なのでランダムでRGBを設定する*/
vec4Expression->DefaultValue.R = 0.5f + (0.5f*FMath::Rand()) / RAND_MAX;
vec4Expression->DefaultValue.G = 0.5f + (0.5f*FMath::Rand()) / RAND_MAX;
vec4Expression->DefaultValue.B = 0.5f + (0.5f*FMath::Rand()) / RAND_MAX;
vec4Expression->DefaultValue.A = 1.0f;

UMaterialExpressionMultiply

よく見るnodeの一つである、Multiです。
下記の感じで接続定義ができます(たぶんあっているはず)

/*Multi nodeのAピンにVec4 Nodeを接続*/
 multiExpression->A.Expression = vec4Expression;

捕捉):※すでに記載したように、Connect関数を使う方が良いと思われる。その場合の例は以下。

/*Multi nodeのAピンにVec4 Nodeの3Pin(B:青)を接続する例*/
 multiExpression->A.Connect(3, vec4Expression);

UMaterialExpressionTextureSample

よく見るnodeの一つである、TextureSampleです。
テクスチャassetを登録するコードは以下になります。

UTexture* UnrealTexture = TexAsset;
/* Textureを割り当てる*/
UnrealTextureExpression->Texture = UnrealTexture;

マテリアルノードをつなぐサンプル

adcl_material_create.jpg

上の画像のMaterialノードを作成するためのC++コード例。

ちなみに、Vec4系ノードの接続は一番上RGBAから接続していますが、
Bのみとかでつなぎたい場合はもう少し書き換え必要になります。
まずは簡単にデフォルト結線のみならば、接続先のピンにノードを設定する方が楽。

下記は4.14で動作確認済みコードの一部です。
詳細についてはサンプルとして作成したプラグインを参照。
捕捉):※Connect関数を使っていないので、半分参考程度で。。。

material
void MaterialFactory::CreateUnrealMaterial(
    UMaterial* UnrealMaterial)
{

    //Blenmd Modeを Maskedに設定
    UnrealMaterial->BlendMode = BLEND_Masked;

    // MaterialのBaseColorに何も接続されていない場合
    if (UnrealMaterial->BaseColor.Expression == NULL)
    {
        //Vector4 Nodeを定義
        UMaterialExpressionVectorParameter* vec4Expression
            = NewObject<UMaterialExpressionVectorParameter>(UnrealMaterial);

        /* 対象MaterialにVec4 Nodeを追加する*/
        UnrealMaterial->Expressions.Add(vec4Expression);


        /* Vec4 Nodeの配置位置とNode名を設定する*/
        vec4Expression->MaterialExpressionEditorX = -300;
        vec4Expression->MaterialExpressionEditorY = 0;
        vec4Expression->SetEditableName("BaseColor");

        /* パラメータ設定が面倒なのでランダムでRGBを設定する*/
        vec4Expression->DefaultValue.R = 0.5f + (0.5f*FMath::Rand()) / RAND_MAX;
        vec4Expression->DefaultValue.G = 0.5f + (0.5f*FMath::Rand()) / RAND_MAX;
        vec4Expression->DefaultValue.B = 0.5f + (0.5f*FMath::Rand()) / RAND_MAX;
        vec4Expression->DefaultValue.A = 1.0f;

        /* 対象MaterialのBaseColorピンに先ほど定義したVec4Nodeを接続する */
        UnrealMaterial->BaseColor.Expression = vec4Expression;

        /* 多分Material側BaseColorの更新だと思うが、資料がないのでさっぱりわからない。おまじないで必要のはず。*/
        TArray<FExpressionOutput> Outputs = UnrealMaterial->BaseColor.Expression->GetOutputs();
        FExpressionOutput* Output = Outputs.GetData();
        UnrealMaterial->BaseColor.Mask = Output->Mask;
        UnrealMaterial->BaseColor.MaskR = Output->MaskR;
        UnrealMaterial->BaseColor.MaskG = Output->MaskG;
        UnrealMaterial->BaseColor.MaskB = Output->MaskB;
        UnrealMaterial->BaseColor.MaskA = Output->MaskA;
    }

    // MaterialのMetallicに何も接続されていない場合
    if (UnrealMaterial->Metallic.Expression == NULL)
    {

        //Vector4 Nodeを定義
        UMaterialExpressionScalarParameter* sclrExpression
            = NewObject<UMaterialExpressionScalarParameter>(UnrealMaterial);

        /* 対象MaterialにscalarParam Nodeを追加する*/
        UnrealMaterial->Expressions.Add(sclrExpression);

        /* scalarParam Nodeの配置位置とNode名を設定する*/
        sclrExpression->MaterialExpressionEditorX = -300;
        sclrExpression->MaterialExpressionEditorY = 200;
        sclrExpression->SetEditableName("Metallic");

        /* パラメータ設定が面倒なのでランダムでRを設定する*/
        sclrExpression->DefaultValue = 0.5f + (0.5f*FMath::Rand()) / RAND_MAX;

        /* 対象MaterialのMetallicピンに先ほど定義したVec4Nodeを接続する */
        UnrealMaterial->Metallic.Expression = sclrExpression;

        /* 多分Material側Metallicの更新だと思うが、資料がないのでさっぱりわからない。おまじないで必要のはず。*/
        TArray<FExpressionOutput> Outputs = UnrealMaterial->Metallic.Expression->GetOutputs();
        FExpressionOutput* Output = Outputs.GetData();
        UnrealMaterial->Metallic.Mask = Output->Mask;
        UnrealMaterial->Metallic.MaskR = Output->MaskR;
        UnrealMaterial->Metallic.MaskG = Output->MaskG;
        UnrealMaterial->Metallic.MaskB = Output->MaskB;
        UnrealMaterial->Metallic.MaskA = Output->MaskA;
    }

    // MaterialのEmissiveColorに何も接続されていない場合
    if (UnrealMaterial->EmissiveColor.Expression == NULL)
    {
        /********************/
        /* Vec4定義         */
        /********************/
        //Vector4 Nodeを定義
        UMaterialExpressionVectorParameter* vec4Expression
            = NewObject<UMaterialExpressionVectorParameter>(UnrealMaterial);

        /* 対象MaterialにVec4 Nodeを追加する*/
        UnrealMaterial->Expressions.Add(vec4Expression);

        /* Vec4 Nodeの配置位置とNode名を設定する*/
        vec4Expression->MaterialExpressionEditorX = -500;
        vec4Expression->MaterialExpressionEditorY = 300;
        vec4Expression->SetEditableName("EmissiveColor");

        /* パラメータ設定RGBを設定する*/
        vec4Expression->DefaultValue.R = 1.0f;
        vec4Expression->DefaultValue.G = 0.5f;
        vec4Expression->DefaultValue.B = 0.0f;
        vec4Expression->DefaultValue.A = 1.0f;

        /********************/
        /* scaler定義       */
        /********************/
        //Vector4 Nodeを定義
        UMaterialExpressionScalarParameter* sclrExpression
            = NewObject<UMaterialExpressionScalarParameter>(UnrealMaterial);

        /* 対象MaterialにscalarParam Nodeを追加する*/
        UnrealMaterial->Expressions.Add(sclrExpression);

        /* scalarParam Nodeの配置位置とNode名を設定する*/
        sclrExpression->MaterialExpressionEditorX = -500;
        sclrExpression->MaterialExpressionEditorY = 500;
        sclrExpression->SetEditableName("EmissivePower");

        /* パラメータ設定*/
        sclrExpression->DefaultValue = 4.0f;

        /********************/
        /* Vec4とscalerのMlt*/
        /********************/
        //Add Nodeを定義
        UMaterialExpressionMultiply* multiExpression
            = NewObject<UMaterialExpressionMultiply>(UnrealMaterial);

        /* 対象MaterialにMulti Nodeを追加する*/
        UnrealMaterial->Expressions.Add(multiExpression);

        /* Multi Nodeの配置位置を設定する*/
        multiExpression->MaterialExpressionEditorX = -200;
        multiExpression->MaterialExpressionEditorY = 400;

        /*Multi nodeのAピンにVec4 Nodeを接続*/
        multiExpression->A.Expression = vec4Expression;
        /*Multi NodeのBピンにScaler Nodeを接続*/
        multiExpression->B.Expression = sclrExpression;

        /* 対象MaterialのEmissiveColorピンに先ほど定義したMulti Nodeを接続する */
        UnrealMaterial->EmissiveColor.Expression = multiExpression;

        /* 多分Material側EmissiveColorの更新だと思うが、資料がないのでさっぱりわからない。おまじないで必要のはず。*/
        TArray<FExpressionOutput> Outputs = UnrealMaterial->EmissiveColor.Expression->GetOutputs();
        FExpressionOutput* Output = Outputs.GetData();
        UnrealMaterial->EmissiveColor.Mask = Output->Mask;
        UnrealMaterial->EmissiveColor.MaskR = Output->MaskR;
        UnrealMaterial->EmissiveColor.MaskG = Output->MaskG;
        UnrealMaterial->EmissiveColor.MaskB = Output->MaskB;
        UnrealMaterial->EmissiveColor.MaskA = Output->MaskA;
    }
    return;
}

やったぜ!

あとがき

これで皆さんも自前マテリアルノード接続ができますね。
とはいえ、これができてもあまりうま味はないです。ハイ。
むしろ、エディタで作成したMaterialのテンプレassetを複製してマテリアルインスタンス化の作成コードを書いた方がいいです。
圧倒的に作るマテリアルのテンプレ構成が把握しやすいし調整もしやすいです。
という検証を自前のIM4Uプラグインに仮組していたのですが、途中で投げてしまいました。
きっと来年には。。。

さて、明日はKaaaaiさんの「よく使われる処理をUnrealC++で書いてみようの巻。」です。