[UE4]コンテンツブラウザのサムネイルをカスタマイズ

#概要
コンテンツブラウザでStaticMeshのサムネイルをみると、StaticMeshに使われているマテリアルによってはサムネイルに何も表示されないことがあります。
その問題を回避するため、この記事ではコンテンツブラウザのサムネイルをC++などを使って、カスタマイズする手法をまとめています。
なお、UE4.26で確認しています。

# 問題点
![disapear.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/e4b890d7-928a-af70-c6f2-aa299d54ee5b.png)
上記はコンテンツブラウザでStaticMeshのサムネイルを並べたところです。
この例の場合は、カメラの距離に応じてアルファディザで消す処理を入れているマテリアルが使われています。カメラが一定距離より近づくとアルファディザ(OpacityMask)で消えるようになっています。
ちょうどサムネイルを描画するカメラの位置では、完全に消えるパラメータになっており、メッシュが見えません。

また、WorldPositionOffsetで極端に位置をずらしている場合、デフォルトのカメラの外側に移動してしまい、その場合も映らなくなってしまいます。

このようにサムネイルにメッシュが映らないと、当然どういうメッシュか分からない為、名前から類推するしかなく確認に手間取ってしまいます。

#回避策１

https://historia.co.jp/archives/13548/

このページで紹介されている方法では、サムネイルのカメラを操作することで見やすい位置/向きに更新する事が出来ます。
しかし、今回問題になった状況では、

* メッシュのサイズとアルファディザのパラメータの都合で…
    * アルファディザ(OpacityMask)がかからない距離では、メッシュが小さくて見えない
    * メッシュが大きく見える距離まで近づくと、アルファディザ(OpacityMask)で消えてしまう
* RuntimeVirtualTextureを参照してWorldPositionOffsetを調整しているため…
    * どの位置に移動したかが分からず、サムネイル描画カメラの移動調整が難しい

という状態だったため、採用を断念しました。

前述のサイトの様に向きだけ変えればいい場合は、このほうが簡単なのでお勧めです。

#回避策2（本命）

サムネイルの描画をカスタマイズすることで、この問題を回避します。

コンテンツブラウザのサムネイルは、**UThumbnailRenderer**から派生したクラスで描画処理が行われています。
エンジンでは、UnrealEdモジュールのThumbnailRenderingに様々なアセットに対応したThumbnailRendererがありますので、これらはカスタマイズの参考になります。

今回はStaticMeshのサムネイルを調整したいので、**UStaticMeshThumbnailRenderer**をベースにカスタマイズします。
最後に紹介するiniファイルの設定で、独自に作成したクラスでもStaticMeshのThumbnailRendererを置き換える事は可能です。その為、エンジン改造は必要ありません。
実際に私が実装した時は、プロジェクト側にUThumbnailRenderer派生のクラスを用意しました。

## 描画処理の解説
サムネイルの描画処理は、Draw関数で行われています。UStaticMeshThumbnailRendererの関数は以下の通りですが、ちょっと長くなるので折り畳みました。
また、元のソースコードにはコメントはありませんが、それぞれの処理の内容をコメントで記載しています。

<details><summary>UStaticMeshThumbnailRendererのソースコードと解説</summary><div>

```cpp:StaticMeshThumbnailRenderer.cpp
void UStaticMeshThumbnailRenderer::Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget* RenderTarget, FCanvas* Canvas, bool bAdditionalViewFamily)
{
    //引数で渡されたUStaticMeshを取得する
    UStaticMesh* StaticMesh = Cast<UStaticMesh>(Object);
    if (StaticMesh != nullptr && !StaticMesh->IsPendingKill())
    {
        if (ThumbnailScene == nullptr || ensure(ThumbnailScene->GetWorld() != nullptr) == false)
        {
            if (ThumbnailScene)
            {
                FlushRenderingCommands();
                delete ThumbnailScene;
            }
            ThumbnailScene = new FStaticMeshThumbnailScene();
        }

        //ThumnailSceneに描画対象のStaticMeshを渡す
        ThumbnailScene->SetStaticMesh(StaticMesh);
        //シーンの設定を行う
        ThumbnailScene->GetScene()->UpdateSpeedTreeWind(0.0);
  
        //引数の情報などを元にシーンを構築する（時間など追加の設定も可能）
        FSceneViewFamilyContext ViewFamily( FSceneViewFamily::ConstructionValues( RenderTarget, ThumbnailScene->GetScene(), FEngineShowFlags(ESFIM_Game) )
            .SetWorldTimes(FApp::GetCurrentTime() - GStartTime, FApp::GetDeltaTime(), FApp::GetCurrentTime() - GStartTime)
            .SetAdditionalViewFamily(bAdditionalViewFamily));
  
        //ポストエフェクトなど高度な描画機能は不要なので無効にする
        ViewFamily.EngineShowFlags.DisableAdvancedFeatures();
        //加えて、モーションブラーもOFF
        ViewFamily.EngineShowFlags.MotionBlur = 0;
        //LOD機能もOFF（LOD0が必ず描画される）
        ViewFamily.EngineShowFlags.LOD = 0;
  
        //Viewの最終設定をして描画
        ThumbnailScene->GetView(&ViewFamily, X, Y, Width, Height);
        RenderViewFamily(Canvas,&ViewFamily);
 
        //後始末（ThumbnailSceneは永続オブジェクトなので、設定を外しておかないと参照が残ってしまう）
        ThumbnailScene->SetStaticMesh(nullptr);
    }
}
```
</div></details>

## マテリアルパラメータの変更
FStaticMeshThumbnailSceneには、**FStaticMeshThumbnailScene::SetOverrideMaterials()**という関数があり、一時的にマテリアルの差し替えが可能です。

UStaticMeshがあるので、Materialを取得して**UMaterial::SetScalarParameterValueEditorOnly()**や **UMaterialInstanceConstant::SetScalarParameterValueEditorOnly()**での書き換えも検討しました。
しかし、この方法でパラメータを更新すると、アセットのパラメータが更新されてしまい、サムネイルだけではなくスタティックメッシュエディタやシーンビューポート上の描画にも影響を与えてしまいます。

その為、一時的なマテリアルの差し替えで済む**FStaticMeshThumbnailScene::SetOverrideMaterials()**を使います。

```cpp
        ThumbnailScene->SetStaticMesh(StaticMesh);
        ThumbnailScene->GetScene()->UpdateSpeedTreeWind(0.0);
 
        //上書きするマテリアルを収集する
        TArray<class UMaterialInterface*> OverrideMaterials;
        int32 MaterialIndex = 0;
        while(true)
        {
            //StaticMeshに設定されているマテリアルを１つづつ取得
            UMaterialInterface* BaseMaterial = StaticMesh->GetMaterial(MaterialIndex++);
            if (BaseMaterial == nullptr)
                break;
 
            //取得したマテリアルをベースにMaterialInstanceDynamicを作成
            UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(BaseMaterial, nullptr);
            check(MID != nullptr);
 
            //スカラーとベクターのパラメータをMIDにコピー
            MID->CopyScalarAndVectorParameters(*BaseMaterial, ThumbnailScene->GetScene()->GetFeatureLevel());
            //MID->CopyMaterialUniformParameters(BaseMaterial);      // Use this if you need TextureParameter.
             
            //上書きするパラメータ名と値を指定
            MID->SetScalarParameterValue("CameraClipMin", 1.0f);
            MID->SetScalarParameterValue("CameraClipMax", 1.0f);
            MID->SetVectorParameterValue("BaseColor", FVector(1.f, 0.0f, 0.0f, 0.0f));
 
            //上書きマテリアルに登録
            OverrideMaterials.Add(MID);
        }
        //上書きマテリアルを設定
        ThumbnailScene->SetOverrideMaterials(OverrideMaterials);

//(…中略…)

        //後始末（ThumbnailSceneは永続オブジェクトなので、設定を外しておかないと参照が残ってしまう）
        ThumbnailScene->SetStaticMesh(nullptr);
        ThumbnailScene->SetOverrideMaterials(TArray<class UMaterialInterface*>());
```

さきほどの、**UStaticMeshThumbnailRenderer::Draw()**から追加する部分を中心に掲載しました。

ここで気を付ける必要があるのは、大きく2か所です。

* **UMaterialInstanceDynamic::CopyScalarAndVectorParameters()**の呼び出し
今回作成したマテリアルインスタンスでパラメータを設定するには、そのマテリアルインスタンスでパラメータがオーバーライド状態（マテリアルエディタだと、チェックが付いた状態）にする必要があります。
この関数の呼び出して、親マテリアルにある全てのスカラー/ベクターパラメータを派生マテリアルでオーバーライド状態にします

* 後始末の追加
StaticMeshと同じで、**FStaticMeshThumbnailScene::SetOverrideMaterials()**も最後に空の値で設定を外す必要があります。そうしないと、同じようにMaterialInstanceDynamicの参照が残ってしまいます。

なお、このサンプルで上書きしているパラメータは適当な物を指定しています。
あまり変更が無ければ、固定値でも良いと思いますし、私はエディタ設定にパラメータリストを追加して、その設定値を参照する様にしました。

## StaticMeshのThumbnailRendererを差し替える
エンジン側の設定はBaseEditor.iniに書かれています。

```ini:BaseEditor.ini
[/Script/UnrealEd.ThumbnailManager]
（…省略…）
+RenderableThumbnailTypes=(ClassNeedingThumbnailName="/Script/Engine.StaticMesh",RendererClassName="/Script/UnrealEd.StaticMeshThumbnailRenderer")
（…省略…）
```

という事は、プロジェクト側ではDefaultEditor.iniに設定を追加すれば、独自設定で上書きする事が可能です。

以下の設定は私が実装した時の物ですが、RendererClassNameにプロジェクト側で追加したThumbnailRendererを指定するだけです。

```ini:DefaultEditor.ini
[/Script/UnrealEd.ThumbnailManager]
+RenderableThumbnailTypes=(ClassNeedingThumbnailName="/Script/Engine.StaticMesh",RendererClassName="/Script/GameEditor.MyStaticMeshThumbnailRenderer")
```

ここまでの対応の結果、以下の画像の様にメッシュが見えるようになりました。
![disapear_fix.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/aa0b9cc9-f69a-7195-bccd-669978313e92.png)

## MaterialInterface(MaterialやMaterialInstance)の場合
ほとんど、StaticMeshの場合と一緒ですが、何点か気を付けるポイントがあるので紹介しておきます。

基本は、StaticMeshと同じで**UMaterialInstanceThumbnailRenderer**というクラスがエンジンにありますので、それをベースにカスタマイズします。
追加するコードも同じようなもので、以下がその一例です。

```cpp

//対象のマテリアルをベースにMaterialInstanceDynamicを作成
UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(MatInst, nullptr);
if (MID != nullptr)
{
    // サムネイルのカスタマイズを反映する
    MID->ThumbnailInfo = MatInst->ThumbnailInfo;
 
    //スカラーとベクターのパラメータをMIDにコピー
    MID->CopyScalarAndVectorParameters(*MatInst, ThumbnailScene->GetScene()->GetFeatureLevel());
    //OverrideMaterial->CopyMaterialUniformParameters(BaseMaterial);     // Use this if you need TextureParameter.
 
    //上書きするパラメータ名と値を指定
    MID->SetScalarParameterValue("CameraClipMin", 1.0f);
    MID->SetScalarParameterValue("CameraClipMax", 1.0f);
    MID->SetVectorParameterValue("BaseColor", FVector(1.f, 0.0f, 0.0f, 0.0f));
 
    //使用するマテリアルを差し替える
    MatInst = MID;
}
```
注意すべき点は、マテリアルのThumbnailInfoをコピーしている所です。
これが無いと、マテリアルで行ったサムネイルのカスタマイズ（回避策１で紹介されている内容）が反映されません。

DefaultEditor.iniへの追加もほぼ同じですが、対象がMaterialとMaterialInstanceの二つありますので、2行追加します。

```ini:DefaultEditor.ini
[/Script/UnrealEd.ThumbnailManager]
+RenderableThumbnailTypes=(ClassNeedingThumbnailName="/Script/Engine.Material",RendererClassName="/Script/GameEditor.MyMaterialThumbnailRenderer")
+RenderableThumbnailTypes=(ClassNeedingThumbnailName="/Script/Engine.MaterialInstance",RendererClassName="/Script/GameEditor.MyMaterialThumbnailRenderer")
```

以上で、MaterialやMaterialInstanceのサムネイルもカスタマイズできました。

# 注意点
この実装方法について、いくつか注意点があります。

## 「マテリアルパラメータの変更」という手法の限界
マテリアルパラメータの変更で対応しているため、マテリアルにThumbnail用の処理を追加する必要があるかもしれません。
実際の事例では、RuntimeVirtualTetxureを元にWorldPositionOffsetで描画位置を調整していました。しかし、元々WorldPositionOffsetを無効にするような処理は無かった為、RuntimeVirtualTetxureの値を無効化するような処理を追加しています。

StaticSwitchが使えればよかったのですが、MaterialInstanceDynamicではStaticSwitchの切り替えはできませんので、StaticSwitchは使えません。
処理を追加する場合は、サムネイルのために負荷が微増する事を織り込んで、出来るだけ処理負荷が増えないような処理を追加する事をお勧めします。

##ThumbnailRendererの指定単位
指定の単位は**アセット種別ごと**という点に注意してください。つまり、全てのStaticMeshに同じThumbnailRendererが使われますので、**マテリアルパラメータの変更**の項目で追加した上書きするパラメータと値は、全てのStaticMeshで使われているマテリアルに適用されます。

その為、**「マテリアルによって、同じパラメータ名で意味が異なる」**と、スタティックメッシュによって（使われているマテリアルによって）意図しないサムネイルが描画される可能性があります。
また、**「スタティックメッシュによって値を変える必要がある」**場合も対応が難しい（不可能ではない）ので、注意が必要です。

## ThumbnailRendererが上書きできない場合も…
エンジンが用意するアセット種別でも、ThumbnailRendererの登録がiniファイルではなくプログラム(C++)から行われている物があります。
今回修正したStaticMeshと同じように、FoliageType_InstancedStaticMeshを差し替えようと思ったんですが、出来ませんでした。原因はFoliageType_InstancedStaticMeshのThumbnailRenderer登録が、iniファイルによる登録より後に行われているせいです。
LoadingPhaseが遅いモジュールを作って登録すれば上書きできそうな気もしますが、試していません。


# あとがき
C++を使ったカスタマイズなので、一般的には少しハードルが高いかもしれません。ですが、このサムネイルカスタマイズが使えると色々と応用が利くと思います。

さきほどTwitterでみかけましたが、Widgetのサムネイルを変更するプラグインがあるそうです。

https://twitter.com/_benui/status/1400616625670221825

おそらく今回紹介した方法の応用例で、独自のThumbnailRendererを実装してるんじゃないかなと思います。