SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

【UE5】コマンドレット活用術その1- マテリアルのパラメータ変更 -

はじめに


こんにちは、クライアントエンジニアの下野です。

今回はUE5のコマンドレットが結構便利だと布教したかったので、何回かに分けていろんなコードを紹介しようかなと思います。(気持ち的には3回位やりたいかな感)

第1回はマテリアルインスタンスのパラメーター変更です。

前提


エンジンのバージョンは5.6です。バージョンによってはコードが変わったりするので完全にコードを一致させても動かない場合があります。

コマンドレットとは


コマンドレットとは、Unreal Engine内で動作するコマンドラインを使うプログラムです。

エディターを起動することなく、コマンドラインから様々な処理を実行できるので、アセットに一括で処理を加えたりする際によく利用します。

他にも、ライトビルドやNavMeshビルド、クック処理をエディターを起動せずに動かせます。

そのためエディターからボタン押してビルドする場合と異なり、ゲーム画面の描画が重い、コンテンツの読み込みが重い等の問題が無くパッケージを作れたりして便利です。

また、batファイルなどを使った自動化とも相性がいいです。

他にエディター系のスクリプトだとPythonを使う方法がありますが、コマンドレットだとC++でコードを書けるので、UEC++触っている⼈であれば何も変わらない⽂法で処理書けて楽です。

マテリアルのパラメータとプロパティ


マテリアル、マテリアルインスタンスの説明は本題から外れるので適当にググって下さい。

今回使うマテリアルインスタンスで変更できる変数は大きく分けるとパラメータとプロパティの2つになります。

パラメータ


下の画像のようなものがパラメータです。

スカラーベクター、テクスチャーとかがあります。

BPとかC++とかの外部からマテリアルの値を変更できるため多⽤しますし、定数値と違って変更後に再コンパイルすること無く結果確認出来るのでデバックにも重宝します。

このマテリアルからインスタンスを作ると、下の画像のようにパラメータが出てきます。

顔とか服みたいな、キャラごとに⾒た⽬違うけど同じ処理で影つけたりする物はマテリアルインスタンスで作成すると便利です。

パラメータが灰色の状態ではクリックしても反応しませんし変更も出来ません。

値の変更をしたい場合は左側のトグルボタンをクリックすることで変更できるようになります。

パラメータはグループの⽂字列を変えることで、同じグループ同⼠まとまった位置に表⽰されるので、⾒づらくなることもありません。

直接グループの所を変える

同じグループはまとまって表示される

プロパティ


下の画像のようなものがプロパティです。

ドメインブレンドモードなど、マテリアルを作成したら最初から存在する変数です。

こちらは新しいものを追加するのにエンジン改造が必要になるので、特に深堀はしません。

マテリアルインスタンスでは下の画像のように「マテリアルプロパティのオーバーライド」の欄に出てきます。

一部プロパティは上書きできないようにマテリアルインスタンスに非公開だったりするので、その場合は表示されていません。

パラメータと同じで値を上書きする場合のフラグが左、変更後の値は右に入れます。

本題


最初に書いた通り、今回はマテリアルインスタンスをコマンドレットから変更してみます。

先程書いた通りマテリアルインスタンスは複数キャラやカラーバリエーションの対応をする際に非常に便利です。

キャラゲーみたいな感じだと同じマテリアルを継承して10個20個マテリアルインスタンスを作ったりするでしょう。

このマテリアルインスタンスを活⽤して制作が進んでいくと、下の図のようなマテリアルインスタンスたくさんある状態になったりします。

しかし制作中に、機能の追加とか仕様の変更で既存のパラメータの値に修正が入ることがあるかもしれません。 マテリアルインスタンス全てが値の上書きをしてないのであれば、大元のマテリアルのパラメータの値を変えるだけで子の値も全て変わるのですが、上書きをしている子には反映されません。

同じマテリアルを継承しているマテリアルインスタンスなら、マテリアルグループの中⾝も同じなので、右クリックでグループごとに値のコピペが可能です。

グループごとにコピペする場合は右クリック

しかし、上記はすべてのパラメータをコピーしてしまうので、変更したく無いものが中途半端に混ざったりしている場合に対応できません。

1個のマテリアルを変更するだけなら⼿作業でいいですが、何個も変更するとなるとヒューマンエラーが発⽣するし遅いので、コマンドレットを使って⾃動化をします。

実装


UCommandletクラスを継承してUMaterialParameterChangeCommandletクラスを作成します。

#pragma once

#include "Commandlets/Commandlet.h"
#include "MaterialParameterChangeCommandlet.generated.h"

UCLASS()
class UMaterialParameterChangeCommandlet : public UCommandlet
{
    GENERATED_BODY()

public:
    UMaterialParameterChangeCommandlet(const FObjectInitializer& ObjectInitializer);
    virtual int32 Main(const FString& CmdLineParams) override;
};

ヘッダー側はあまり説明することは無いです。

コマンドレットは実⾏するとoverrideしたMain関数が呼ばれます。

引数のFString& CmdLineParamsコマンドライン引数の⽂字列で、今回はファイルパスを受け取ったりするのに使⽤します。

次にC++ファイル側です。

#include "MaterialParameterChangeCommandlet.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/AssetRegistryHelpers.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Misc/FileHelper.h"
#include "MaterialEditingLibrary.h"
#include "UObject/SavePackage.h"

UMaterialParameterChangeCommandlet::UMaterialParameterChangeCommandlet(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
}

int32 UMaterialParameterChangeCommandlet::Main(const FString& CmdLineParams)
{
    TArray<FAssetData> AssetList;
        
    // コマンドライン引数のパース
    FName SearchFilePath; // 検索するフォルダの場所
    if (!FParse::Value(*CmdLineParams, TEXT("FilePath="), SearchFilePath)) // 値取れんかった場合
    {
        // NOTE: Contents以下すべて検索なので激重。なるべくファイルパスは指定してほしい    
        SearchFilePath = TEXT("/Game");
    }

    FName MaterialInstanceName; // 変更するマテリアルインスタンス名
    if (!FParse::Value(*CmdLineParams, TEXT("MIName="), MaterialInstanceName)) // 値取れんかった場合
    {
        // NOTE: 全変更は重すぎるので、名前とれない場合は即終了
        return 0;
    }

    // アセットリストの取得
    {
        FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(FName("AssetRegistry"));

        IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
        AssetRegistry.SearchAllAssets(true);

        FARFilter Filter;
        Filter.PackagePaths.Add(SarchFilePath);
        Filter.ClassPaths.Add(UMaterialInstance::StaticClass()->GetClassPathName());
        Filter.bRecursiveClasses = true;
        Filter.bRecursivePaths = true;
        AssetRegistry.GetAssets(Filter, AssetList);
    }

    // NOTE: 現状だと名前完全⼀致以外動かない想定
    for (FAssetData& Asset : AssetList)
    {
        // ToString().Containsで部分⼀致にしても良い説
        if (Asset.AssetName.IsEqual(MaterialInstanceName))
        {
            if (UMaterialInstanceConstant* MaterialInstance = Cast<UMaterialInstanceConstant>(Asset.GetAsset()))
            {
                // パラメータの変更
                FName ParamName = TEXT("Param1"); // パラメータ名
                float Value = 1.0;                  // 変更後の値
                UMaterialEditingLibrary::SetMaterialInstanceScalarParameterValue(MaterialInstance, ParamName, Value);
                UMaterialEditingLibrary::RebuildMaterialInstanceEditors(MaterialInstance->GetBaseMaterial());          // パラメータの変更が反映され無い可能性あるので強制リビルドも入れておく  
                
                // プロパティの変更
                MaterialInstance->BasePropertyOverrides.bOverride_TwoSided = false; // 上書き設定を変える
                MaterialInstance->BasePropertyOverrides.TwoSided = false;           // 中身の方を変える
                
                // パッケージ保存
                UPackage* Package = Asset.GetPackage();
                FSavePackageArgs SaveArgs;
                SaveArgs.SaveFlags = EObjectFlags::RF_Public | EObjectFlags::RF_Standalone;
                const FString FileName = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension());
                UPackage::SavePackage(Package, nullptr, *FileName, SaveArgs);
            }
        }
    }
    
    return 0;
}

コード解説


処理の流れ順に説明します。

Main


まずコンストラクタは今回何もしていないので無視して、最初にMain関数が呼ばれます。

Main関数内では、最初に引数のパース処理をします。

詳しくはこの後の起動の項目で分かると思いますが、コマンドレットは起動時に⽂字列を⼀緒に渡すことが出来ます。

渡した文字列から、ファイルパスなど必要な情報を判別しようというわけです。

検索するフォルダのパス、変更するマテリアルインスタンスの名前を1つの⽂字列に格納するので、FParseを使⽤して分解し取得します。

FStringとかの定数で定義してしまうと、名前変更するたびにビルド⾛らせないといけないので、今回みたいに引数にするか、jsonとかcsvから読み込むのが⼀般的な気がします。

アセットデータの取得


次にGetAssetListでマテリアルインスタンスのアセットを取得します。

FAssetDataがStaticMeshやMaterialとかのアセットデータを持っているので最初に取得します。 FAssetRegistryModuleがエディター上のアセット全部管理しているモジュールなので変数を⽤意し、FARFilterを使ってファイルパスとか、どのクラスを探すかとかのフィルタリングを⾏います。

今回はマテリアルインスタンスだけ欲しいので、ClassPathsにMaterialInstanceのパスを突っ込んでいます。(5.1まではクラス名で取ってましたが、名前の重複恐れてなのか⾮推奨になってます)

今回はコマンドライン引数から受け取ったパスの中のファイルを全検索して、マテリアルインスタンスなアセットを全取得します。

マテリアルインスタンスの取得


アセットデータが取り終わったら次はFAssetDataをUMaterialInstanceConstantに変換します。

for⽂使ってコマンドライン引数から受け取ったアセット名と同じものを探します。

今回は名前完全⼀致のみの取得です。部分⼀致にしたい場合はAssetNameをFStringに変換した 後、Contains関数使えば出来ます。

名前が合っているアセットが⾒つかった場合、GetAsset関数でMaterialInstanceが基底クラスのUObjectとして取得できるのでCastして使⽤します。

値の変更


次に本題である値の変更です。

パラメータの値の変更にはUMaterialEditingLibrary::SetMaterialInstanceScalarParameterValue を使用しています。

アセットの変更を明記するMarkPackageDirty() や、変更した値を反映するPostEditChange() とかがラッピングされている関数なので、コマンドレットやプラグイン等のゲーム実行時以外で値を変更する場合はこれを使うのが良いと思います。

今回は関係ありませんが、プロパティの変更だけをする場合や、SetMaterialInstanceScalarParameterValue を使用しない場合は、上記の変更用の関数を忘れないようにしてください。

注意点としては、上記関数はMaterialEditorモジュールのstatic関数なので、PrivateDependencyModuleNamesにMaterialEditor を追加しないとリンクエラーが出ます。

dev.epicgames.com

これ系のエラーは、エラー出てる関数名をUE公式のUnreal Engine C++ API Referenceに突っ込んだらほとんどの場合はページが出るので、そこに書いてあるヘッダーをインクルードして.csにモジュールを突っ込んだら⼤体動きます。

注意点としては、MaterialEditorモジュールは名前の通りエディター機能なので、パッケージビルドを通そうとするとプラットフォームによってはエラーが出てきます。

使い終わったらモジュールを外して、エディター機能を使用している箇所をコメントアウトするか#if WITH_EDITOR で括って下さい。

プロパティの変更は変数を直に弄っています。

この際、MaterialInstance->TwoSided だと変更出来ないので注意してください。

マテリアルインスタンス直下のTwoSidedはゲーム中に変更した値とかを保持する用途で使用するので、エディター起動時にBasePropertyOverridesの中の値で上書きされてしまいます。

変更したアセットの保存


最後に変更した値の保存です。

先程保存しておいたFAssetDataからパッケージを取得し、SavePackage関数を呼びます。

以上で説明は終わりです。

起動方法


コマンドレットは起動が若⼲特殊です。(コマンドライン使って起動するので)

⽅法は様々(bat使ったり、コマンドプロンプト使ったり、uprojectから開いたり)ですが、VisualStudioでコードを書いたり弄った後に別のファイルをわざわざ開くのは⾯倒なので、VisualStudioで完結する⽅法で開きます。

  1. プロジェクト->プロパティでプロパティページを開きます。
  2. 構成をDevelopment Editor、プラットフォームをx64にします。
  3. デバック->コマンド引数を以下に従った形で変更します。(コマンドレット使い終わったら元の引数に戻すので、どこかに保存しておいてください。

"$(SolutionDir)$(ProjectName).uproject" -run=コマンドレット名 -引数=値

$(SolutionDir)や$(ProjectName)はSlnを作成した際に、自動でuprojectファイル名やSlnファイルの場所を認識して作られるマクロなのですが、バージョンによっては名前やパスが若干違うかもしれないので、動かない場合は適切なパスを突っ込んであげて下さい。

これでデバックを開始するとUEのエディターとは違うウィンドウが出て処理が始まります。

コマンドレットの処理が終わったら勝⼿に終了します。

VisualStudio上から起動するメリットは、ブレークポイント置いて値を⾒たりデバック出来るところだと思います。

謎に値が正しくないとか、動かんとか思ったら簡単にデバックで値見れて便利です。

デバック


デバックします。

今回はTestフォルダ内にあるMI_Testのパラメータを変更してみます。

適当に以下のようなマテリアルとマテリアルインスタンスを作りました。

上記のColorを水色から緑に変更し、TwoSidedの上書きを無くして値をFalseにします。

最初に、コマンドレットのパラメータ変更のコードを以下のように変更します。

// パラメータの変更
FName ParamName = TEXT("Color"); // パラメータ名
FLinearColor Color = FLinearColor::Green;
UMaterialEditingLibrary::SetMaterialInstanceVectorParameterValue(MaterialInstance, ParamName, Color); // ScalarからVectorに代わってるので注意

// プロパティの変更
MaterialInstance->BasePropertyOverrides.bOverride_TwoSided = false; // 親マテリアルと同じ値にする
MaterialInstance->BasePropertyOverrides.TwoSided = false;           // falseにする

次に起動方法の手順を踏み、コマンドライン引数に以下を指定し、起動します。

"$(SolutionDir)$(ProjectName).uproject" -run=MaterialParameterChangeCommandlet -FilePath=/Game/Test -MIName=MI_Test

処理が終わったら引数を戻り、プロジェクトを開きなおすと…

無事値の変更が出来ました。

しかし、値が変わっているのにアセットを開くまでサムネイルに変更がありません。

これはコマンドレットに描画機能が入っていないため、変更後の見た目をサムネイルに反映する処理が走ってないからです。

これの修正は結構面倒で、これで1本ブログ作れる+これの修正に時間割くなら他の事した方が良いかなと思ったので今回割愛します。(気が向いたらブログになるかも)

無視しても値の変更自体はされていて、問題なく動くので大丈夫です。

気になる場合は、アセットを開いたらエディター起動中は見た目治ります。エディターを再起すると戻りますが、アセットを保存するとサムネイルも更新入って完全に治ります。

終わり


いかがでしたか?

コマンドレットを使うと⼤量なアセットの変更を⾃動化できるので、非常に便利です。

⼤きいプロジェクトになるほど扱うマテリアルやマテリアルインスタンス数も多くなるとので、どこまで効率化出来るかは⼤事だと思います。

最後に私事ですが、ブログの下書きをメモ帳からNotionに変えたら、MarkDown形式でコピペ出来るようになったので、見た目差分減ってちょっと楽になりました。 わざわざ見出しとか目次とか付け直すの地味に面倒だったので助かってます。

©2025 SPARKCREATIVE Inc.