blenderでシェイプキーの形状補間をして並べたい

最終的にシェイプキーは使わず、オブジェクトに生やしたカスタムプロパティとドライバーで制御するのでblenderの外には持っていけません。
あと最近はジオメトリノードとか頂点数変わってもクレイアニメっぽくできるアドオン[^1]とかあるのでもはや使い道はないに等しいかもしれない…
[^1]: [pepe-school-land / Keymesh · GitLab](https://gitlab.com/pepe-school-land/keymesh)
#シェイプキーをモディファイアーで再現する
フックモディファイアーと頂点ウェイトを使って、各軸ごとに頂点を引っ張ることでシェイプキーを再現します。シェイプキーとして適用の逆みたいなことをします。

シェイプキーのキーブロックに入っているデータは各頂点の移動先の座標なので、基本形状のシェイプキーとの差分をとって移動ベクトルを生成します。

```python:
import bpy
import numpy as np
id = "_hoge" #識別用

sv = np.array([v for v in [vs.co for vs in ShapeKey.data]])-np.array([v for v in [vs.co for vs in ShapeKey.relative_key.data]])
er = np.abs(sv).max()
mod = len(Object.modifiers) #モディファイアーが他に予め設定されていた場合に、これから設定するものを一番上に持っていくための変数
```
この移動ベクトルをもとに頂点ウェイトを設定し、これとエンプティを使ってフックモディファイアーを設定し、その強さをカスタムプロパティとドライバーで一括制御することでシェイプキーを再現します。
blenderのウェイトは[0, 1]でクリップされてマイナス値は設定出来ないので、各軸x2のウェイト設定、フックモディファイアーのターゲット(エンプティ)の生成とその設定、ドライバーの設定をします。

```python
Object[ShapeKey.name+"_value"+id] = ShapeKey.value
for i in range(6):
    axis_name = ShapeKey.name+"_"+["+X", "-X", "+Y", "-Y", "+Z", "-Z"][i]+id

    #頂点ウェイト
    g = Object.vertex_groups.new(name=axis_name)
    for idx in range(len(sv)):
        g.add([idx], sv[idx, int(np.floor(i/2))]/er*(1 if i%2==0 else -1), "REPLACE")
    
    #エンプティ
    bpy.ops.object.empty_add(type="PLAIN_AXES")
    axis = bpy.context.active_object
    axis.name = axis_name
    axis.parent = Object
    
    #モディファイアー
    m = Object.modifiers.new(name=axis_name, type="HOOK")
    m.falloff_type = "LINEAR"
    m.object = axis
    m.vertex_group = axis_name
    m.show_in_editmode = True
    m.show_on_cage = True
    m.show_expanded = False

    #ドライバー
    d = m.driver_add("strength").driver
    d.type = "SCRIPTED"
    v = d.variables.new()
    v.name = "var"
    v.targets[0].id = Object
    v.targets[0].data_path = '["'+ShapeKey.name+"_value"+id+'"]'
    d.expression = "var"
    
    #エンプティの移動とモディファイアーの順序設定
    axis.location[int(np.floor(i/2))] = er*(1 if i%2==0 else -1)
    bpy.context.view_layer.objects.active = Object
    for j in range(mod):
        bpy.ops.object.modifier_move_up(modifier=axis_name)
```
あとはシェイプキーをミュートするとか削除するとかしておきます。
#Fカーブで補間する
##ドライバーの設定
便利な関数`Object.animation_data.drivers.find(data_path).evaluate(frame)`を使ってドライバーを作ります。

```python
bpy.app.driver_namespace["evaluate"] = lambda p, n, v: p.find(n).evaluate(v)
```
```python:
#target_*: Fカーブを追加するオブジェクト、id、path
#drive_*: Fカーブによって評価されるプロパティのid、path
#dvar_*: 制御用プロパティのid、path
def fcurve_eval_add(target_obj, target_id, target_path, drive_id, drive_path, dvar_id, dvar_path):
    f = target_id.driver_add(target_path)
    f.keyframe_points.add(2)
    f.modifiers.remove(f.modifiers[0])

    #Fカーブの制御点
    for i in range(2):
        p = f.keyframe_points[i]
        p.co[0] = float(i)
        p.co[1] = float(i)
        p.handle_left = [i-0.2, i-0.2]
        p.handle_right = [i+0.2, i+0.2]
        
    d = f.driver
    d.type = "SCRIPTED"
    d.expression = "1.0"
    
    d = drive_id.driver_add(drive_path).driver
    d.type = "SCRIPTED"
    v = d.variables.new()
    v.name = "var"
    v.targets[0].id = dvar_id
    v.targets[0].data_path = dvar_path
    
    #target_pathを直接ドライバーの式に打ち込めないので一旦target_objにカスタムプロパティを設定して
    #そこにアクセスすることでtarget_pathをドライバーに渡しています
    id = "driver_attribute_"+drive_path.replace("[", "").replace("]", "").replace('"', "")
    target_obj[id] = target_path
    n = d.variables.new()
    n.name = "name"
    n.targets[0].id = target_obj
    n.targets[0].data_path = 'original["'+id+'"]'
    
    p = d.variables.new()
    p.name = "parent"
    p.targets[0].id = target_obj
    p.targets[0].data_path = "animation_data.drivers"
    
    d.expression = "evaluate(parent, name, var)"
```
わかりにくいですが`target_obj.animation_data.drivers.find(target_path)`が`target_id.target_path`に設定したFカーブにアクセスし、`FCurve.evaluate(dvar_id.dvar_path)`でFカーブを評価し、その結果を`drive_id.drive_path`に渡しています。「低速なpython式」の警告が出ます。低速です。
##カーブオブジェクトに追従させて並べる
大量に増えるオブジェクトのコレクション整理用、脳筋実装です。

```python
def get_collection(target_object, collection_name):
    def oec(lc, o):
        for c in lc:
            for t in c.collection.objects:
                if(t == o):
                    return c
            oec(c.children, o)
    
    lc = oec(bpy.context.view_layer.layer_collection.children, target_object)
    lc.collection.children.link(bpy.data.collections.new(collection_name))
    return lc
```
パスに追従のコンストレイントをつけたオブジェクトにドライバーを追加し、これを複製してカスタムプロパティを調整することでいい感じに並べます。

```python
Object["offset"+id] = 0.0
Object_curve["offset"+id] = 1.0
Object_curve["count"+id] = 10 #適当に

#コンストレイント
c = Object.constraints.new(type="FOLLOW_PATH")
c.name = id
c.target = Object_curve
c.forward_axis = "FORWARD_Z"
c.up_axis = "UP_Y"
c.use_curve_follow = True
c.use_fixed_location = True

#間隔の調整用
fcurve_eval_add(Object_curve, Object_curve, '["offset'+id+'"]', c, "offset_factor", Object, '["offset'+id+'"]')
for n in Object.keys():
    if n.find(id) < 0 or type(Object.get(n)) is not str:
        continue
    Object_curve[n] = 1.0

    #メッシュオブジェクトのシェイプキー調整用
    fcurve_eval_add(Object_curve, Object_curve, '["'+n+'"]', Object, '["'+n.split(id)[0]+"_value"+id+'"]', Object, '["offset'+id+'"]')

Object.name = Object.name+id
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = Object
Object.select_set(True)
bpy.ops.object.select_hierarchy(direction="CHILD", extend=True)
bpy.context.view_layer.objects.active = Object

#カウントの分だけオブジェクトを複製、整列
for i in range(Object_curve["count"+id]):
    bpy.ops.object.duplicate(linked=True)
    for o in bpy.data.objects:
        if not o.select_get():
            continue
        o.name = o.name.split(id)[0]+id+"_"+str(i)
    o = bpy.context.active_object
    o["offset"+id] = i/Object_curve["count"+id]

bpy.ops.object.select_all(action="DESELECT")

#コレクション整理
co = get_collection(Object, "objects"+id)
get_collection(Object, "empties"+id)
for o in bpy.data.objects:
    if o.name.find(id) < 0:
        continue
    if len(o.name.split(id)[1].split("_")) < 2:
        continue
    co.collection.objects.unlink(o)
    if o.type == "MESH":
        co.children["objects"+id].collection.objects.link(o)
    elif o.type == "EMPTY":
        co.children["empties"+id].collection.objects.link(o)

co.children["empties"+id].exclude = True
Object.constraints[id].mute = True
```
最後にドライバー編集画面でカーブオブジェクトのFカーブを動かすと形状補間しながらいい感じに並べられます。アドオンにすればもうちょっとすっきりできそうですね。

#追記
どうしてわざわざシェイプキーをモディファイアーに置き換えるのか書いていませんでしたがこれはリンク複製して編集できるようにするためです。シェイプキーの値に直接ドライバーを刺して複製しても同じように形状補間はできますがメッシュデータはバラバラになって編集しにくくなり、リンク複製をするとシェイプキーの値もリンクされてしまい形状補間はできなくなります。そこでメッシュデータに関係しない場所でシェイプキーを再現すればリンク複製をしても形状補間をしながら同一のメッシュデータを編集できるようになるので、シェイプキーをモディファイアーで再現しました。