[UE4]Pythonによるインスタンシング

GTMF2019にてPython / BlueprintによるUnreal Engineの自動化という内容で講演いたしました。
本記事はその中で紹介した実装例、Pythonによるインスタンシングのサンプル公開用記事です。

発表内容については以下のスライドをご覧ください。
[GTMF2019] Python / BlueprintによるUnreal Engineの自動化
https://www.slideshare.net/EpicGamesJapan/gtmf2019-python-blueprintunreal-engine/1

Unreal EngineではInstanced Static Meshを使うことでインスタンスを作成することができます。
Merge Actorsというツールを使うことで選択アクタを一気にインスタンス化することもできますが、いずれにしても大量のアクタをグループ分けしつつインスタンス化していくのは非常に手間です。

そこで、Pythonの外部ライブラリに含まれるK-meansを使ってクラスタリング&インスタンシングをしてみました。

##事前準備
###Python側
Python2.7が入っていない場合は公式サイトからダウンロード、インストールしておいてください。
インストール時、Add python.exe to Pathを有効にしておきます。
![2019-07-16_22h56_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369485/03ab3c74-930f-448a-9666-2efc288364cf.png)

Python外部ライブラリのインストールはコマンドプロンプトでpipを使って行います。
ややこしいですが、まずはそのpipをインストールします。

コマンドプロンプトを起動して"python -m pip install pyinstaller"と入力
![2019-07-16_23h01_59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369485/25ba75a5-69eb-b7b4-b0bd-371263c0a38d.png)


色々ログが流れてインストールされたようなメッセージが出ればOKです。

これで外部ライブラリをインストールする準備ができたので、今度は以下のように入力してscikit learnをインストールします。

"python -m pip install scikit-learn"

インストールされたライブラリはC:\Python2.7\lib\site-packagesに入ります。
同じ階層がEngine\Binaries\ThirdParty\Python\Win64\Lib内にもあるので、site-packagesフォルダを丸ごとコピーしてください。
これで、Unreal Pythonで外部ライブラリを使えるようになります。

###Unreal Engine側
検証バージョンはUE4.22です。

Python Editor Script Pluginを有効にしておく必要があります。
![2019-07-04_18h47_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369485/702c6999-2424-9c9c-908e-d6c47c48e8db.png)


次にインスタンスとして使うアクタを用意しておきます。
Blueprintを新規作成。
![2019-07-16_23h12_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369485/7c5b2477-43b2-3af7-69b8-02f63aa7362f.png)
Actorクラスを親にします。
![2019-07-16_23h12_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369485/e563f8f0-b4f6-024e-a410-435ff2bd42f1.png)

名前はとりあえずBP_Instanceにしてください。
作成したらBlueprintを開いて、Add ComponentからInstanced Static Meshを追加しておきます。
![2019-07-16_23h15_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369485/a83a1d14-52bc-342f-289b-22a20363a80f.png)



##K-meansによるインスタンシング
こちらが実際に動作している結果になります。
![Cluster.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369485/61286269-baf7-9798-cb38-0fcd3690ce96.gif)

コードはこちら

```python:qiita.py
import unreal
import numpy as np
import sklearn
from sklearn.cluster import KMeans

#用意したインスタンス用Blueprintクラスをロード
bp_instance = unreal.EditorAssetLibrary.load_blueprint_class('/Game/BP_Instance.BP_Instance')

#選択したStatic Mesh Actorを取得
list_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
list_static_mesh_actors = unreal.EditorFilterLibrary.by_class(list_actors,unreal.StaticMeshActor)

list_unique = np.array([])

#選択したアクタに含まれるStatic Meshの種類を調べる
for lsm in list_static_mesh_actors:
    static_mesh = lsm.get_component_by_class(unreal.StaticMeshComponent).get_editor_property("StaticMesh")
    list_unique = np.append(list_unique,static_mesh)
list_unique = np.unique(list_unique)

for lu in list_unique:
    list_transform = np.array([])
    list_locations = np.array([[0,0,0]])

#クラスタリング用の位置情報と、インスタンスに追加するトランスフォームの配列を作成
    for lsm in list_static_mesh_actors:
        if lsm.get_component_by_class(unreal.StaticMeshComponent).get_editor_property("StaticMesh") == lu:
            list_transform = np.append(list_transform,lsm.get_actor_transform())
            location = np.array([[lsm.get_actor_location().x,lsm.get_actor_location().y,lsm.get_actor_location().z]])
            list_locations = np.append(list_locations,location,axis=0)

    list_locations = np.delete(list_locations,0,axis=0)

#クラスタ数を指定
    num_clusters = 5

#位置情報を元にクラスタリング
    pred = KMeans(n_clusters=num_clusters).fit_predict(list_locations)

    instanced_components = np.array([])

#インスタンス用アクタをスポーン
    for i in range(num_clusters):
        instanced_actor = unreal.EditorLevelLibrary.spawn_actor_from_class(bp_instance,(0,0,0),(0,0,0))
        instanced_component = instanced_actor.get_component_by_class(unreal.InstancedStaticMeshComponent)
        instanced_components = np.append(instanced_components,instanced_component)

#クラスタ番号を元にインスタンスを追加
    for j, pd in enumerate(pred):
        instanced_components[pd].add_instance(list_transform[j])

#インスタンスにStatic Meshを割り当て
    for k in range(num_clusters):
        instanced_components[k].set_editor_property("StaticMesh",lu)

#最初に選択したアクタを削除
for lsm in list_static_mesh_actors:
    lsm.destroy_actor()
```
※Scikit-learnライブラリがインストールされていない場合は動作しません。
※実行時にnum_clusters以上の数のアクタを選択していないと動作しません。

6行目、load_blueprint_class('/Game/BP_Instance.BP_Instance')の()内には事前準備したBlueprintのパスを入れます。

```python:qiita.py
bp_instance = unreal.EditorAssetLibrary.load_blueprint_class('/Game/BP_Instance.BP_Instance')
```

アセット右クリックからCopy Referenceでコピーして貼り付けてください。
![2019-07-10_10h49_47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369485/27660cd3-9920-494e-daba-1786039c050c.png)




##インスタンスを個別のStatic Meshに変換
一度インスタンス化してしまうと再調整ができないので、インスタンスからバラバラな状態に戻すスクリプトも作ってみました。
![Break.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369485/ce8c26dd-cfc3-9e8e-b173-db878112364f.gif)

コードはこちら。

```python:qiita.py
import unreal
import numpy as np

#選択したアクタを取得
selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()

#インスタンスコンポーネントを取得
for sa in selected_actors:
    instanced_components = np.array([])
    instanced_component = sa.get_components_by_class(unreal.InstancedStaticMeshComponent)
    instanced_components = np.append(instanced_components,instanced_component)

#コンポーネントに含まれるインスタンス数を取得
    for ic in instanced_components:
        instance_transform = np.array([])
        instance_count = ic.get_instance_count()

#インスタンスの数分Static Mesh Actorをスポーンし、トランスフォーム割り当て、Static Mesh割り当てを繰り返す
        for j in range(instance_count):
            instance_transform = np.append(instance_transform,ic.get_instance_transform(j,1))
            spawned_actor = unreal.EditorLevelLibrary.spawn_actor_from_class(unreal.StaticMeshActor,(0,0,0),(0,0,0))
            spawned_actor.set_actor_transform(instance_transform[j],0,0)
            smc = spawned_actor.get_component_by_class(unreal.StaticMeshComponent)
            smc.set_editor_property("StaticMesh",ic.get_editor_property("StaticMesh"))

#最初に選択していたアクタを削除
for sa in selected_actors:
    sa.destroy_actor()
```
※大量のインスタンスが含まれる場合、処理時間がかなり長くなる場合があります。

こちらのスクリプトはフォリッジに対しても実行可能です。

##参考
Python を使用したエディタのスクリプティング
https://api.unrealengine.com/JPN/Engine/Editor/ScriptingAndAutomation/Python/index.html

Unreal Python API Documentation
https://api.unrealengine.com/INT/PythonAPI/