1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-06

最終的にシェイプキーは使わず、オブジェクトに生やしたカスタムプロパティとドライバーで制御するので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カーブを動かすと形状補間しながらいい感じに並べられます。アドオンにすればもうちょっとすっきりできそうですね。

追記

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

  1. pepe-school-land / Keymesh · GitLab

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?