Blenderのリグやボーンのスケールを適用した時の不具合対策

Blenderで他からボーンアニメーションのついたオブジェクトをインポートした場合に
スケールが1でない状態で読み込んでしまったもののスケールを適用した場合に
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/6e87a8b6-4fe0-49a5-927e-858c0d97c32b.png)
意図しない場所に吹っ飛んでいったり
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/5436defe-27d5-4edf-9efe-f5d8feb9f0ee.png)

Rigify等のリグシステムでスケールがかかった状態で作業してしまって
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/4d388a0f-9d29-4049-843f-fe59a9edb406.png)
スケールを適用して「しっちゃかめっちゃか」になったことがあるかと思います。

これは　Blenderのアニメーションはオブジェクトそのものに格納されているのではなく
Actionという別のデータを利用していて
オブジェクトスケールの「適用」ではそのデータやコンストレイントの設定が 変更されないために起こります

こういった不具合の対策について書き留めておきたいと思います
## ボーンアニメーションスケールの適用スクリプト
Blenderにはアニメーションできるものが色々ありますが
選択したアーマチュアのスケールを適用した上で　そのアーマチュアに付いていたアニメーションのスケールを調整するスクリプトを例示してみます
```py:apply_armatureobject_scaling.py
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等のリグでは骨が伸び縮みした状態になるかと思います
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/63e48c33-daff-4184-b426-939d419b09c2.png)
これは　ストレッチコンストレイントに記録されているボーンの長さがスケール適用前の値のままなのが原因のようです
手動で調整する場合には「元の長さ」の欄の横にある×マークをクリックすることで現在の値にリセットされるようですが
全てを調整するのは大変ですのでこれもスクリプト化してみました

```py:stretchto_reset.py
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
```



## ファイル内にあるアクションに　スケーリング操作を適用するスクリプト
```py: apply_action_scaling.py
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でクリップボードに書き出すスクリプト
```py
import bpy
actions = ["'%s'" % a.name for action in bpy.data.actions]
bpy.context.window_manager.clipboard = ','.join(actions)
```
ちなみにこれはPythonコンソールでこんな感じに短縮表記できます
```py
C.window_manager.clipboard = ','.join(["'%s'" % a.name for a in D.actions])
```
使うのは一回の作業では一回きりなのでこの方が楽かも？

---
オブジェクトのスケールをつけたままアニメーションを付けてしまった事例は
私がゲームやアニメでの業務に関わっていた中でも頻繁に遭遇しているものです
何かの手助けになれば幸いです

## おまけ
スケールが適用されていないオブジェクトを探すスクリプト
（ファイル内のスケール1でないオブジェクトをコンソールにプリント）
```py:check_scale.py
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"で始まるオブジェクトを対象外にしています


スケールを適用するスクリプトの別バージョン
```apply_armatureobject_scaling2.py
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)
```
基本的には先のスクリプトと同じ挙動ですが　少し書き方が異なっているバージョンです