Blenderで他からボーンアニメーションのついたオブジェクトをインポートした場合に
スケールが1でない状態で読み込んでしまったもののスケールを適用した場合に
意図しない場所に吹っ飛んでいったり
Rigify等のリグシステムでスケールがかかった状態で作業してしまって
スケールを適用して「しっちゃかめっちゃか」になったことがあるかと思います。
これは Blenderのアニメーションはオブジェクトそのものに格納されているのではなく
Actionという別のデータを利用していて
オブジェクトスケールの「適用」ではそのデータやコンストレイントの設定が 変更されないために起こります
こういった不具合の対策について書き留めておきたいと思います
ボーンアニメーションスケールの適用スクリプト
Blenderにはアニメーションできるものが色々ありますが
選択したアーマチュアのスケールを適用した上で そのアーマチュアに付いていたアニメーションのスケールを調整するスクリプトを例示してみます
import bpy
import mathutils
obj = bpy.context.active_object
if obj.type == 'ARMATURE':
# アーマチュアオブジェクトのトランスフォームを取得
amt_scale = obj.matrix_local.to_scale()
##====================トランスフォームの適用========================##
# 子を取得
children = obj.children
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
for child in children:
child.select_set(True)
# 選択オブジェクトのスケールを適用
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
##====================アニメーションデータを調整========================##
# スケールのみを考慮したマトリクスを作成
mat_amt_scale = mathutils.Matrix.LocRotScale(None, None, amt_scale)
# 位置のアニメーションデータを調整
## アニメーションを取得
animation_data = obj.animation_data
if hasattr(animation_data,"action"):
fcurves = animation_data.action.fcurves
for curve in fcurves:
if curve.data_path[-8:] == 'location':
# array_indexで各データパスのチャンネルのインデックスが取得できる
scale = mathutils.Vector((1.0, amt_scale[curve.array_index]))
# 移動のキーフレームの値をスケーリングに合わせた値に設定
for point in curve.keyframe_points:
point.co = point.co * scale
point.handle_left = point.handle_left *scale
point.handle_right = point.handle_right *scale
適用したいアーマチュアオブジェクトをアクティブにした状態で実行するスクリプトです
スクリプトの前半は
アーマーチュアと子になるオブジェクトを選択してオブジェクトメニューの適用>スケールを実行したのと同じ動作
後半は
スケールを適用したことで ボーンの移動量が合わなくなる点の調整です
(このスクリプトではNLAを使ったりしたアニメーションには対応していません)
rigifyでスケールを適用した時の不具合対応
ボーンの移動の問題を解消した後にも
Rigify等のリグでは骨が伸び縮みした状態になるかと思います
これは ストレッチコンストレイントに記録されているボーンの長さがスケール適用前の値のままなのが原因のようです
手動で調整する場合には「元の長さ」の欄の横にある×マークをクリックすることで現在の値にリセットされるようですが
全てを調整するのは大変ですのでこれもスクリプト化してみました
import bpy
# アクティブオブジェクトのストレッチコンストレイントを調整
obj = bpy.context.active_object
if obj.type == 'ARMATURE':
for pose_bone in obj.pose.bones:
for constraint in pose_bone.constraints:
if constraint.type == 'STRETCH_TO':
# ストレッチの長さを初期化
constraint.rest_length = pose_bone.bone.length
ファイル内にあるアクションに スケーリング操作を適用するスクリプト
import bpy
import mathutils
# 適用するスケール
apply_scale = [0.01, 0.01, 0.01]
# リストに書いたアクションを処理対象に
actions_name = ['Action','Action.001','idleCrouch','idleCrouch_in','idleCrouch_out','Stay']
# ファイル内にあるアクション全てを対象にする場合
# actions = bpy.data.actions
actions = [bpy.data.actions[name] for name in actions_name if(name in bpy.data.actions.keys()) ]
amt_scale = mathutils.Vector(apply_scale)
for action in actions:
fcurves = action.fcurves
for curve in fcurves:
if curve.data_path[-8:] == 'location':
# array_indexで各データパスのチャンネルのインデックスが取得できる
scale = mathutils.Vector((1.0, amt_scale[curve.array_index]))
# 移動のキーフレームの値をスケーリングに合わせた値に設定
for point in curve.keyframe_points:
point.co = point.co *scale
point.handle_left = point.handle_left *scale
point.handle_right = point.handle_right *scale
対象にするアクションの一覧を書き出す準備に
ファイルの中にあるアクション一覧をCSVでクリップボードに書き出すスクリプト
import bpy
actions = ["'%s'" % a.name for action in bpy.data.actions]
bpy.context.window_manager.clipboard = ','.join(actions)
ちなみにこれはPythonコンソールでこんな感じに短縮表記できます
C.window_manager.clipboard = ','.join(["'%s'" % a.name for a in D.actions])
使うのは一回の作業では一回きりなのでこの方が楽かも?
オブジェクトのスケールをつけたままアニメーションを付けてしまった事例は
私がゲームやアニメでの業務に関わっていた中でも頻繁に遭遇しているものです
何かの手助けになれば幸いです
おまけ
スケールが適用されていないオブジェクトを探すスクリプト
(ファイル内のスケール1でないオブジェクトをコンソールにプリント)
import mathutils
for obj in bpy.data.objects:
normal_seze = mathutils.Vector((1.0, 1.0, 1.0))
if obj.scale != normal_seze:
if obj.name[0:3] != "WGT":
print(obj.name)
"WGT"で始まるオブジェクトを対象外にしています
スケールを適用するスクリプトの別バージョン
import bpy
import mathutils
obj = bpy.context.active_object
if obj.type == 'ARMATURE':
# アーマチュアオブジェクトのトランスフォームを取得
amt_scale = obj.matrix_local.to_scale()
# スケールのみを考慮したマトリクスを作成
mat_amt_scale = mathutils.Matrix.LocRotScale(None, None, amt_scale)
# オブジェクトのスケールをボーンに適用
obj.data.transform(mat_amt_scale)
# 情報の更新で一旦エディットモードに
bpy.ops.object.editmode_toggle()
bpy.ops.object.editmode_toggle()
# 位置のアニメーションデータを調整
## アニメーションを取得
animation_data = obj.animation_data
if hasattr(animation_data,"action"):
fcurves = animation_data.action.fcurves
for curve in fcurves:
if curve.data_path[-8:] == 'location':
# array_indexで各データパスのチャンネルのインデックスが取得できる
scale = mathutils.Vector((1.0, amt_scale[curve.array_index]))
# 移動のキーフレームの値をスケーリングに合わせた値に設定
for point in curve.keyframe_points:
point.co = point.co * scale
point.handle_left = point.handle_left *scale
point.handle_right = point.handle_right *scale
# 子になっているオブジェクトのスケールを全て1に
# 子を取得
children = obj.children
for child in obj.children:
mat_scale_child = mathutils.Matrix.LocRotScale(None, None, child.matrix_local.to_scale()) @ mat_amt_scale
child.data.transform(mat_scale_child)
new_loc = child.matrix_basis.to_translation() @ mat_amt_scale
child.location = new_loc
child.scale = (1 ,1, 1)
obj.scale = (1 ,1, 1)
基本的には先のスクリプトと同じ挙動ですが 少し書き方が異なっているバージョンです