ポーズ・ライブラリを作りたいな

mmToolにもう一つだけ

漫画漫文ツール(mmTool)はほぼ完成ですが、もう一つだけ、こだわっている機能があります。ポーズ・ライブラリを用意して、どのプロボーションのキャラでも(8頭身でも3頭身でも)自由にポーズが適用できる機能です。

poseMyArtのOBJを利用…?

poseMyArtの有料サービスではモデルをOBJ形式でエクスポートできます。このOBJの頂点の中で、ボーンの始点と終点になりそうな頂点のインデックス番号を調べ、つなげてボーンを作った後に親子関係を設定すれば、ポーズ可能なアーマチュアができるはずです。

① OBJからスケルトンを作る

② OBJのポーズをターゲットのポーズボーンに適用

しかし、この方法で作ったスケルトンは、poseMyArtのアセットである3Dモデルに依存しており、ポーズライブラリを公開することは利用規約に違反する懸念があります。

ご参考までに、bpyスクリプトを掲載します。poseMyArtの出力OBJがなければ役に立たないスクリプトですが、ボーンのマトリクス計算が使えることがわかって勉強になりました。

① OBJからスケルトンを作る

# poseMyArtのOBJデータからスケルトン用の頂点を取り出し、座標からスケルトンを作成

import bpy
import bmesh
from mathutils import Vector

# Auto_Skeletonがあれば消しておく
deleteMe = bpy.data.objects.get('Auto_Skeleton')
if deleteMe:
    bpy.data.objects.remove(deleteMe, do_unlink=True)

# PMAのOBJをインポート.............................................................
bpy.ops.wm.obj_import(filepath="/path/to/outputFile/PoseMyArt_Scene.obj", directory="/path/to/outputFile/", global_scale=1, forward_axis='NEGATIVE_Z', up_axis='Y')

obj = bpy.context.active_object
me = obj.data

bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(me)
bpy.ops.mesh.select_all(action='DESELECT')

edgeIndex1 = 22313
edgeIndex2 = 44900

# インデックステーブルを更新
bm.edges.ensure_lookup_table()
# エッジを二等分
bm.edges[edgeIndex1].select = True
bm.edges[edgeIndex2].select = True
bpy.ops.mesh.subdivide()
bpy.ops.mesh.select_all(action='DESELECT')

# メッシュ更新
bmesh.update_edit_mesh(me)

# オブジェクトモードに戻る
bpy.ops.object.mode_set(mode='OBJECT')

# 構造の定義(ボーン名:[頂点インデックス, 親ボーン名])
bone_defs = {
    "Hip":        [44923, "Root"],
    "Stomach":    [43365, "Hip"],
    "Chest":      [18237, "Stomach"],
    "Neck":       [12439, "Chest"],
    "Head":       [43141, "Neck"],
    "Head_leaf":  [44922, "Head"],
    # 左腕
    "Clavicle_L": [7060,  "Chest"],
    "Arm_L":      [7058,  "Clavicle_L"],
    "Forearm_L":  [9171,  "Arm_L"],
    "Hand_L":     [19262, "Forearm_L"],
    "Hand_L_leaf":[26726, "Hand_L"],
    # 右腕
    "Clavicle_R": [6533,  "Chest"],
    "Arm_R":      [6529,  "Clavicle_R"],
    "Forearm_R":  [8115,  "Arm_R"],
    "Hand_R":     [31657, "Forearm_R"],
    "Hand_R_leaf":[38666, "Hand_R"],
    # 左脚
    "Pelvis_L":   [3879,  "Hip"],
    "Thigh_L":    [3707,  "Pelvis_L"],
    "Calf_L":     [12123, "Thigh_L"],
    "Foot_L":     [959,   "Calf_L"],
    "Foot_L_leaf":[5990,   "Foot_L"],
    # 右脚
    "Pelvis_R":   [3675,  "Hip"],
    "Thigh_R":    [18093, "Pelvis_R"],
    "Calf_R":     [11067, "Thigh_R"],
    "Foot_R":     [478,   "Calf_R"],
    "Foot_R_leaf":[4933,   "Foot_R"],
}

def create_skeleton_from_obj(obj_name):
    obj = bpy.data.objects.get(obj_name)
    if not obj: return

    # 頂点座標の取得(ワールド座標系)
    matrix_world = obj.matrix_world
    vert_coords = {i: matrix_world @ v.co for i, v in enumerate(obj.data.vertices)}

    # アーマチュアの作成
    arm_data = bpy.data.armatures.new("Skeleton_Data")
    arm_obj = bpy.data.objects.new("Auto_Skeleton", arm_data)
    bpy.context.collection.objects.link(arm_obj)

    bpy.context.view_layer.objects.active = arm_obj
    bpy.ops.object.mode_set(mode='EDIT')

    # つなげたくない(根元を離しておきたい)親ボーンのリスト
    no_connect_parents = ["Chest", "Hip"]

    for b_name, (v_idx, p_name) in bone_defs.items():
        bone = arm_data.edit_bones.new(b_name)
        bone.head = vert_coords[v_idx]
        bone.tail = bone.head + Vector((0, 0.05, 0)) # 仮の長さ

        if p_name:
            parent = arm_data.edit_bones.get(p_name)
            if parent:
                bone.parent = parent

                # 親が Chest または Hip の場合はコネクト(use_connect)しない
                if p_name in no_connect_parents:
                    bone.use_connect = False
                    # 親の tail を書き換えない(あるいは親のデフォルトの向きを維持する)
                else:
                    # それ以外のボーンは親とピッタリつなげる
                    parent.tail = bone.head
                    bone.use_connect = True

    # Hip の先端を Stomach のポジションに向ける
    arm_data.edit_bones["Hip"].tail = arm_data.edit_bones["Stomach"].head

    # Chest の先端を Neck のポジションに向ける
    arm_data.edit_bones["Chest"].tail = arm_data.edit_bones["Neck"].head

    bpy.ops.object.mode_set(mode='OBJECT')

    # Delete OBJ
    bpy.data.objects.remove(obj, do_unlink=True)

    # リーフボーン削除 & 見た目をスティックに変更
    boneObj = bpy.data.objects.get('Auto_Skeleton')
    if boneObj and boneObj.type == 'ARMATURE':
        # オブジェクトを選択してアクティブにする
        bpy.context.view_layer.objects.active = boneObj
        bpy.ops.object.select_all(action='DESELECT')
        boneObj.select_set(True)
        boneObj.show_in_front = True
        boneObj.data.display_type = 'STICK'

        # 編集モードに切り替え
        bpy.ops.object.mode_set(mode='EDIT')

        # edit_bonesは編集モードに入った後、アクティブオブジェクトのdataから取得
        edit_bones = bpy.context.object.data.edit_bones

        # _leafで終わるボーンをリストアップ
        bones_to_remove = [bone for bone in edit_bones if bone.name.endswith('_leaf')]

        # ボーンを削除
        for bone in bones_to_remove:
            edit_bones.remove(bone)

    # オブジェクトモードに戻る
    bpy.ops.object.mode_set(mode='OBJECT')

# インポート後のアクティブなオブジェクト(OBJ)に対して実行
create_skeleton_from_obj(bpy.context.active_object.name)

② OBJのポーズをターゲットのポーズボーンに適用

import bpy
import mathutils

# オブジェクトモードで何も選択されていない状態にする
def refresh():
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')

refresh()

# 共通のボーン名
boneNames = ["Hip", "Stomach", "Chest", "Neck", "Head",
             "Clavicle_L", "Arm_L", "Forearm_L", "Hand_L",
             "Clavicle_R", "Arm_R", "Forearm_R", "Hand_R",
             "Pelvis_L", "Thigh_L", "Calf_L", "Foot_L",
             "Pelvis_R", "Thigh_R", "Calf_R", "Foot_R"
            ]

# 既存のアーマチュアを取得
src = bpy.data.objects.get('Auto_Skeleton')    #レストポーズがいろいろなスケルトン
tgt = bpy.data.objects.get('Target')           #レストポーズがT字形のスケルトン

def poseTransfer():
    if not src or not tgt:
        print("Objects not found")
        return

    for nm in boneNames:
        src_eb = src.data.bones[nm]
        tgt_pb = tgt.pose.bones[nm]

        # ターゲットのポーズボーンマトリクスにソースのマトリクス_ローカルを代入
        tgt_pb.matrix = src_eb.matrix_local

        bpy.context.view_layer.update()

    # srcは不要になったので消す
    bpy.data.objects.remove(src, do_unlink=True)

# Run
poseTransfer()

ポーズのソースをCC0のものに変える

GitHubに素晴らしいポーズ写真集がCC0で公開されています。

この画像を読み取ってスケルトンが作れたらいいですね。たぶん、OpenCVとMediaPipeが使えると思います。

mmToolへの実装はまだ先になりそうです。



関連記事