最終的にシェイプキーは使わず、オブジェクトに生やしたカスタムプロパティとドライバーで制御するのでblenderの外には持っていけません。
あと最近はジオメトリノードとか頂点数変わってもクレイアニメっぽくできるアドオン1とかあるのでもはや使い道はないに等しいかもしれない…
シェイプキーをモディファイアーで再現する
フックモディファイアーと頂点ウェイトを使って、各軸ごとに頂点を引っ張ることでシェイプキーを再現します。シェイプキーとして適用の逆みたいなことをします。
シェイプキーのキーブロックに入っているデータは各頂点の移動先の座標なので、基本形状のシェイプキーとの差分をとって移動ベクトルを生成します。
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のウェイト設定、フックモディファイアーのターゲット(エンプティ)の生成とその設定、ドライバーの設定をします。
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)
を使ってドライバーを作ります。
bpy.app.driver_namespace["evaluate"] = lambda p, n, v: p.find(n).evaluate(v)
# 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式」の警告が出ます。低速です。
カーブオブジェクトに追従させて並べる
大量に増えるオブジェクトのコレクション整理用、脳筋実装です。
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
パスに追従のコンストレイントをつけたオブジェクトにドライバーを追加し、これを複製してカスタムプロパティを調整することでいい感じに並べます。
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カーブを動かすと形状補間しながらいい感じに並べられます。アドオンにすればもうちょっとすっきりできそうですね。
追記
どうしてわざわざシェイプキーをモディファイアーに置き換えるのか書いていませんでしたがこれはリンク複製して編集できるようにするためです。シェイプキーの値に直接ドライバーを刺して複製しても同じように形状補間はできますがメッシュデータはバラバラになって編集しにくくなり、リンク複製をするとシェイプキーの値もリンクされてしまい形状補間はできなくなります。そこでメッシュデータに関係しない場所でシェイプキーを再現すればリンク複製をしても形状補間をしながら同一のメッシュデータを編集できるようになるので、シェイプキーをモディファイアーで再現しました。