yu00’s blog

プログラミングに関する備忘録です

UnityボーンポーズをBlenderで読み込む(Unity,Blender座標変換まとめ)

はじめに

Unityでつけたボーンポーズを出力して、
Blenderのボーンポーズに適用するスクリプトを作成しました。

Blenderのボーンウェイトの調整の時、
Unityの物理演算で設定されたポーズにしたかったのが目的です。

手順

以下方針で行います。

次のようなサンプルで説明します。

注意

rootボーンのポーズを設定しても無視されます

Unity

  • MiniJSON.csをダウンロード
  • AssertsにPluginsフォルダを作成し、MiniJSON.csを配置
  • 以下スクリプトを作成しGameObjectにAddComponentする
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System;
using MiniJSON;

public class ExportUnityBones : MonoBehaviour
{
    [SerializeField]
    List<GameObject> bones = null;
    [SerializeField]
    string filename = "UnityBonePoses.json";

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // localToWorldMatrix -> List
            var bonesList = new Dictionary<string, List<List<float>>>();
            foreach (GameObject bone in bones)
            {
                Matrix4x4 H = bone.transform.localToWorldMatrix;
                var rows = new List<List<float>>();
                foreach (int i in Enumerable.Range(0, 4))
                {
                    var row = new List<float>();
                    foreach (int j in Enumerable.Range(0, 4))
                    {
                        row.Add(H[i, j]);
                    }
                    rows.Add(row);
                }
                bonesList.Add(bone.name, rows);
            }

            Debug.Log($"Export:{System.IO.Path.GetFullPath(filename)}");
            string json = Json.Serialize(bonesList);
            using (var f = new System.IO.StreamWriter(filename))
            {
                f.Write(json);
            }
        }
    }
}
  • 出力したいボーン親を右クリック>Select Children で子を選択
  • ExportUnityBones.csのBonesドラッグアンドドロップ
  • ExportUnityBones.csのFilenameに出力ファイル名を指定

  • Playしてポーズを設定した後スペースキーを押すと出力

Blender

import bpy
from mathutils import Matrix
import json
import os

def main():
    filename = 'UnityBonePoses.json'

    H_BBld_BUnt = Matrix([
        [-1, 0, 0, 0],
        [0, 1, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1]
    ])

    H_BUnt_BBld = H_BBld_BUnt.inverted()

    print('import '+ os.path.abspath(filename))
    with open(filename) as f:
        unity_bones = json.load(f)

    bpy.context.view_layer.objects.active = bpy.data.objects['Armature']
    bpy.ops.object.mode_set(mode='EDIT')
    edit_bones = bpy.data.objects['Armature'].data.edit_bones
    pose_bones = bpy.data.objects['Armature'].pose.bones

    for bone_name, unity_bone_matrix in unity_bones.items():
        if bone_name not in pose_bones:
            continue
        bone = pose_bones[bone_name]
        parent_bone = bone.parent
        if not parent_bone or parent_bone.name not in unity_bones:
            continue

        child_edit_bone = edit_bones[bone_name]
        parent_edit_bone = child_edit_bone.parent

        H_WBld_BBldEdtChdHed = child_edit_bone.matrix
        H_BBldEdtChdHed_WBld = H_WBld_BBldEdtChdHed.inverted()
        H_WBld_BBldEdtPntHed = parent_edit_bone.matrix
        H_BBldEdtPntHed_WBld = H_WBld_BBldEdtPntHed.inverted()

        H_BBldEdtPntHed_BBldEdtChdHed = H_BBldEdtPntHed_WBld @ H_WBld_BBldEdtChdHed
        T_BBldEdtPntHed_BBldEdtPntTil = Matrix.Translation(H_BBldEdtPntHed_BBldEdtChdHed.to_translation())

        H_BBldPosChdOrg_BBldPosPntTil = H_BBldEdtChdHed_WBld @ H_WBld_BBldEdtPntHed @ T_BBldEdtPntHed_BBldEdtPntTil

        H_WUnt_BUntPosChdHed = Matrix(unity_bone_matrix)
        H_BUntPosPntHed_WUnt = Matrix(unity_bones[parent_bone.name]).inverted()

        H_BUntPosPntHed_BUntPosChdHed = H_BUntPosPntHed_WUnt @ H_WUnt_BUntPosChdHed
        T_BUntPosPntTil_BUntPosPntHed = Matrix.Translation(-1 * H_BUntPosPntHed_BUntPosChdHed.to_translation())            

        H_BBldPosPntTil_BBldPosChdHed = H_BBld_BUnt @ T_BUntPosPntTil_BUntPosPntHed @ H_BUntPosPntHed_BUntPosChdHed @ H_BUnt_BBld
        
        H_BBldPosChdOrg_BBldPosChdHed = H_BBldPosChdOrg_BBldPosPntTil @ H_BBldPosPntTil_BBldPosChdHed

        bone.matrix_basis = H_BBldPosChdOrg_BBldPosChdHed
        bpy.context.view_layer.update() # update bone pose

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

main()
print('end\n')
  • filenameにUnityで出力したファイルを指定
  • Run Scriptを押すとPose ModeのボーンがUnityで設定したポーズになる

解説

UnityとBlender間の座標変換式について解説します。
座標変換については、https://yu00.hatenablog.com/entry/2022/07/11/103954
を参照ください。

略字

  • : ワールド座標
  • : ボーンローカル座標
  • : Blender
  • : Unity
  • : Blender Edit Mode
  • : Blender Pose Mode
  • : Parent Bone
  • : Child Bone
  • : Bone Head
  • : Bone Tail

基本方針

適用順によってポーズが変わらないように、
Blenderbpy.types.PoseBone.matrix_basisに適用します。
Pose Mode Parent Tail Bone座標 から見て
ポーズ適用前の座標をPose Mode Child Origin Bone座標
ポーズ適用後の座標をPose Mode Child Head Bone座標 とすると、
matrix_basisは、Pose Mode Child Head Bone座標 から
Pose Mode Child Origin Bone座標 への同次変換行列
です。

ただしPose Mode Parent Tail Bone座標Pose Mode Parent Head Bone座標 から
Pose Mode Child Head Bone座標 原点へ平行移動した座標とします。

また、
は一致します。

また、Unityで出力したtransform.localToWorldMatrixは、
, に対応します。

Unity, Blenderボーン座標の基底変換

  • : Blenderボーン座標から見たUnityボーン座標の基底
  • : Blenderボーン座標 の基底
  • : Blenderボーン座標から見た点p
  • : Unityボーン座標から見た点p
  • : Blenderボーン座標からUnityボーン座標への同次変換行列

Unity, Blenderボーン座標の座標変換

(備考)Unity, Blenderワールド座標の基底変換

  • : Blenderワールド座標から見たUnityワールド座標の基底
  • : Blenderワールド座標 の基底
  • : Blenderワールド座標から見た点p
  • : Unityワールド座標から見た点p
  • : Unityワールド座標からBlenderワールド座標への同次変換行列

(備考)Unity同次変換行列

  • : Unityローカル座標からUnityワールド座標への同次変換行列(transform.localToWorldMatrix)
  • : 移動同次変換行列(transform.position)
  • : 回転同次変換行列(transform.rotation)
  • : スケール同次変換行列(transform.localScale)

参考


Powered by MathJax

This page is based on MathJax technology.