Blenderのパーティクルシステム、ダイナミックペイント、ジオメトリノードの連携手法の検討 「Python API 編」

# はじめに
<!-- 発端や概要を記載 -->
本記事は前回の記事(https://qiita.com/Kai0731200/items/fac62eea3ee78ad6ace8) の続きです。
今回は自作アドオン「Particle Tracker」を用いて、Blenderのパーティクルシステム、ダイナミックペイント、ジオメトリノードを連携する方法について解説します。
基本的にはBlender Python APIの話です。

アドオンのURL(https://github.com/Kai0731200/Particle-Tracker)

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [おさらい](#おさらい)
1. [目標](#目標)
1. [解説](#解説)
1. [パーティクルの位置情報の取得](#パーティクルの位置情報の取得)
1. [handlerによる追跡](#handlerによる追跡)
1. [雨のシーンで使ってみる](#雨のシーンで使ってみる)
1. [まとめと考察](#まとめと考察)
1. [参考文献](#参考文献)


# おさらい
<a id="#おさらい"></a>
Blenderでアニメの１シーンのようなリッチな雨の風景をつくりたい！というモチベーションから始まり、**パーティクルシステム、ダイナミックペイント、ジオメトリノードの３つを連携する**方法を模索することになりました。
前回はこの解決策として
・水を揺らすダイナミックペイント「波」と重みを表すダイナミックペイント「ウェイト」を使う
・ジオメトリノードで頂点グループの重みを使った頂点のマージを行う
という２つの方法を用いて解決しました。


今回は
**Python APIを用いてジオメトリの様々な情報にアクセスしてパーティクルシステムとジオメトリノードを連携させました。**
そして一連の処理を **自作アドオン「Particle Tracker」** としてまとめました。
「Particle Tracker」のPythonの処理と実際のシーンにどう活用したかの２点について解説します。
今回はあくまでPythonでの処理の解説になります。アドオンの構築方法などについては他の方が解説した記事がたくさんありますのでそちらをご覧ください。

<iframe width="1236" height="695" src="https://www.youtube.com/embed/sg0AbyenwQ8" title="Rain like a Scene from an Anime | Blender" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<a id="#目標"></a>
# 目標
**雨粒(パーティクル)が落ちた位置で水面を揺らして更に水飛沫オブジェクトを配置する**


<!-- 各チャプター -->
<a id="#解説"></a>
# 解説
まず具体的に達成しなけらばならない目標は、前回と同様に水面にヒットしたときのパーティクルの位置の取得です。
パーティクルシステムから出たパーティークルはジオメトリになっておらず属性を保持することができません。よってそのままではジオメトリノードで扱うことができません。
これを解決するために今回は**パーティクルの位置にポイントを作成、ポイントの位置を追跡する**ことにします。このポイントの位置をジオメトリノードで使って水飛沫のアニメーションを配置します。

ではどうやってパーティクルの位置にポイントを作成するのか？ここでPython APIが活躍します！
下の図はPython APIの処理でパーティクルの位置にポイントを作成、追跡した様子です。
![329653071-d9b8d2a9-1b3d-4e1c-80e7-f2ba2ea4d126.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3788547/d79ef547-2ff4-7c17-b959-c6943148fd71.png)

今回の処理のポイントは
**・Blenderのデータの内部構造に注意したパーティクルの位置情報の取得**
**・handlerを用いたパーティクルの追跡**
の２つです。
この記事で紹介するコードは自作したアドオンで書かれたソースコードを説明のため一部改変したコードです。その点だけよろしくお願いします。

<a id="#"></a>
# パーティクルの位置情報の取得
とりあえずパーティクルシステムを用意します。
立方体を発生源としてパーティークル数は10個、終了フレームは10に設定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3788547/5b72daa5-ee30-be00-189d-2be92205fd67.png)



自分が最初に思いついたパーティクルの位置情報の取得する処理手順は下記のようなものでした。
**・1 アクティブなオブジェクトのパーティクルシステム一覧を取得**
**・2 パーティクルシステム一覧から使いたいパーティクルシステムを取得**
**・3 パーティクルシステムから各パーティークルの位置を取得**

これをコードにすると下記のようになります。
```python
import bpy

# アクティブなオブジェクトが存在するかを確認
if bpy.context.active_object:
    obj = bpy.context.active_object
    
    # パーティクルシステムが存在するかを確認
    if obj.particle_systems:
        print("This object has particle system !")
        particle_system = obj.particle_systems.active

        # 各パーティクルの位置を取得
        particles = particle_system.particles
        print("Number of particles:", len(particles))
        for p in particles:
            print("Particle Index:", p.index)
            print("Location:", p.location)
    else:
        print("The active object has no particle system.")
else:
    print("There are no active objects.")
```
一見このコードで上手くいっている気もします。
このコードを実行して、コンソールで出力を見てみます。
```
This object has particle system !
Number of particles: 0
```
**パーティクルシステムは取得できているが、取得したパーティークルの個数が0になっている！？**
このようにパーティークルが取得できていなことが分かります。

これはパーティクルシステムがモディファイアで処理されていることとBlenderの内部処理が関係しています。
パーティクルシステムの編集は専用のパーティークルタブで実装されていますが内部的な扱いは他のモディファイアと変わりません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3788547/4ae563be-157a-945a-ff46-fd90247beac2.png)

内部管理について説明します。
**Blenderはシーン内のオブジェクトの状態やデータの更新を依存関係グラフ Depsgraph で管理しています。**
これにはモディファイアも含まれます。
また、Blender内のデータブロック(オブジェクト、メッシュ、マテリアルなど)はIDと呼ばれるデータ構造を管理する識別子を持っています。
簡略化のため今は「ID = オブジェクトとそのデータ」と考えて大丈夫です。

ここで重要なのが
現在の処理は、**IDが依存関係グラフで評価された状態になっていない、つまり内部的にオブジェクトのメッシュやデータにモディファイアの処理が適用されていない**ということです。

詳しく知りたいという方は公式のリファレンスを読んでください。

これを解決するために**依存関係グラフの取得と評価済みIDの取得を行います。**
依存関係グラフの取得にはcontext.evaluated_depsgraph_get()
評価済みIDの取得にはID.evaluated_get()
を使います。

ここで、ID.evaluated_get()は指定された依存関係グラフから対応する評価済みIDを取得するだけで依存関係グラフが完全に評価するわけではありません。つまり評価の結果を返すだけであることに注意してください。

下記が完成したソースコードです。

```python
import bpy

ps_list=[]
# アクティブなオブジェクトを取得
obj = bpy.context.active_object

# アクティブなオブジェクトが存在するか確認
if obj is None:
    self.report({'ERROR'}, "No active object found. Please select an object.")
    

# オブジェクトがパーティクルシステムを持っているか確認
if obj.particle_systems:
    print("This object has particle system !")
    depsgraph = bpy.context.evaluated_depsgraph_get()
    modified = obj.evaluated_get(depsgraph)
    # オブジェクトごとに存在するパーティクルシステムの情報を取得
    for ps in obj.particle_systems:
        ps_list.append(modified.particle_systems[0])
        
else:
    self.report({'INFO'}, "No particle system found in the active object.")

if not ps_list:
    self.report({'ERROR'}, "No particle system found in the active object.")
    

ps=ps_list[0]
#パーティクルの位置を取得
particles = ps.particles
p_list=[]
for i, p in enumerate(particles):
    p_list.append(p.location)
    print("Particle", i, "Location:", p.location)



# ポイントの作成
edges = []
faces = []
plotDataPoint = bpy.data.meshes.new('plot_data')
plotDataPoint.from_pydata(p_list, edges, faces)
attr = plotDataPoint.attributes.new("p_list", 'FLOAT_VECTOR', 'POINT')
attr.data.foreach_set("vector", [val for vec in p_list for val in vec])  # ベクトルのリストをフラット化して設定

plotDataPoint.update()

# ポイントからオブジェクトを作成
plotDataObject = bpy.data.objects.new(ps.name+'_Tracker', plotDataPoint)

curScene = bpy.context.scene
curScene.collection.objects.link(plotDataObject)
```

このコードは
**・1 アクティブなオブジェクトのパーティクルシステム一覧を取得**
**・2 パーティクルシステムモディファイアが適用されたときのメッシュの状態にする**
**・3 インデックスが0(パーティクルシステム一覧で一番上のもの)のパーティクルシステムを取得**
**・4 パーティクルシステムから各パーティークルの位置を取得**
**・5 パーティークルの位置情報からポイントを作成**
という処理をしています。

実行結果を見てみましょう。
```
This object has particle system !
Particle 0 Location: <Vector (-0.8473, -1.3200, 0.2622)>
Particle 1 Location: <Vector (-1.3200, -0.3300, -0.3737)>
Particle 2 Location: <Vector (-0.6282, 1.3200, 0.1363)>
Particle 3 Location: <Vector (-0.8473, 1.3200, -1.2667)>
Particle 4 Location: <Vector (1.3200, 0.3300, -0.3737)>
Particle 5 Location: <Vector (0.6385, -0.6282, 0.8177)>
Particle 6 Location: <Vector (-1.3200, 0.8473, 0.2622)>
Particle 7 Location: <Vector (0.1286, 0.3300, -1.8223)>
Particle 8 Location: <Vector (-0.6282, 1.3200, 0.1363)>
Particle 9 Location: <Vector (-0.8473, -1.3200, 0.2622)>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3788547/1fa4273e-ffae-7fe9-ec6d-4695bd2a48c6.png)

コンソールを見ると10個のパーティークルの位置が取得できているのが分かります。
3Dビューポートを見るとパーティークルと同じ位置にポイントのオブジェクト「パーティクルシステム_Tracker」が作成されています。(白色のパーティークルの中に黄色のポイントが見える。)

問題点
ソースコードから分かるように残念ながら今のところ一つのパーティクルシステムだけしか対応にしていません。
複数のパーティクルシステムへの対応も実装したのですが、パーティークルの追跡をするhandlerの処理が複雑になり、Blenderでの作業中にこれを意識するのは大変だと判断してアドオンには実装しませんでした。
こちらについては複数のパーティクルシステムに対応できるキーフレーム方式を現在試しているので更新した場合はまた記事を書きます。

<a id="handlerによる追跡"></a>
# handlerによる追跡
前述の通りパーティクルの追跡処理は、近々キーフレーム方式に更新予定ですが備忘録としてhandler方式を解説します。ここではhandlerによるパーティクルの追跡処理のみを解説します。handlerそのものの詳しい解説は他の方の記事をご覧ください。
handlerとは簡単に言うと**指定した処理を毎フレーム行う**という機能です。
handlerを用いて**作成したポイントの位置をパーティクルの位置に毎フレーム移動します。**

・工夫点
Blenderの仕様上、特に何も処理をしない場合はhandlerの処理はシーンの更新より先になります。つまりポイントの位置が１フレーム前のパーティクルの位置に更新されるということで。
これをbpy.context.view_layer.update()で明示的にシーンの更新がhandlerの処理より先になるようにします。

下記が完成したソースコードです。

```python
def my_handler(scene):
    bpy.context.view_layer.update() 
    vts = plotDataPoint.vertices
    for i in range(len(particles)):
        vts[i].co = particles[i].location    
        bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
            
bpy.app.handlers.frame_change_pre.clear()
bpy.app.handlers.frame_change_pre.append(my_handler)
self.report({'INFO'}, "Particle tracking completed successfully.")
```

vts = plotDataPoint.verticesで作成したポイントの位置情報にアクセスしています。それをforループでパーティークルの位置に更新します。

1フレーム進むとパーティークルとポイントがどっちも動いている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3788547/80d985f6-54d3-1ec7-f448-e58080169509.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3788547/e93f2b53-94be-c627-2ecf-c0e1e430acf2.png)


<a id="#雨のシーンで使ってみる"></a>
# 雨のシーンで使ってみる
ようやく雨のシーンに戻ります。最終的な見た目は前回と全く同じですが、作成したポイントにジオメトリノードを適用して水飛沫のアニメーションを配置したいと思います。
ポイントのz座標が0付近になったらそのフレームと位置でアニメーションを開始します。(ノードの他の部分は前回の記事をご覧ください。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3788547/745df071-1707-e832-6ca5-14c33945c524.png)


これに関してはパーティークルのz座標が＋から－になるフレームでアニメーションを開始するようにノードを調整中です。もしやり方を知っている方がいたら教えてください！

<a id="#まとめと考察"></a>
# まとめと考察
以上よりPython APIを使ったダイナミックペイントとジオメトリノードの連携ができました。
Python APIを使うとHoudiniっぽいことができるので開発していてとても楽しかったです。
ただ正直handlerはシーン全体に依存するし、開き直したら消えてしまうので扱いが難しいです。

次回はパーティークルの追跡をキーフレーム方式で行う手法を補足として解説します。
読んでいただきありがとうございます。

<a id="#参考文献"></a>
# 参考文献
Blender Python API 公式リファレンス  
https://docs.blender.org/api/current/bpy.types.Depsgraph.html  
https://docs.blender.org/api/current/bpy.types.ParticleSystem.html  
https://docs.blender.org/api/current/bpy.types.Particle.html