yu00’s blog

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

VRChatでレティクルを作る

はじめに

通常のゲームでレティクルを表示する時は
2Dスクリーン上に投影する方法がありますが、
VRでは360°画面のためこの方法が使えません。
この記事ではVRChat UdonSharpを使い
レティクルを表示する方法を説明します。

考え方

  1. プレイヤーの頭(Head)の周りに球状のスクリーン(Screen)を配置する
    • ScreenのレイヤーはUIにする
  2. 発射位置(ShootPos)からレイキャストする
  3. 壁との衝突位置(HitPos)からHeadにレイキャストする
    • Screenだけと衝突するようにUIレイヤーにマスクをかける
  4. Screenとの衝突位置にレティクル(Reticle)を配置する
  5. そのままだとレティクルが振動するのでローパスフィルタをかける

コード

using UdonSharp;
using VRC.SDKBase;
using VRC.Udon;
using UnityEngine;

public class Reticle : UdonSharpBehaviour
{
    public Transform Head;
    public Transform ShootPos;
    public Transform HitPos;
    public Transform ReticleImage;

    public float RaycastPointTau = 0.5f;
    public float RaycastNormalTau = 0.5f;

    private Ray ReticleRay = new Ray();
    private Vector3 RaycastPoint = Vector3.zero;
    private Vector3 RaycastNormal = Vector3.zero;
    private Vector3 PrevRaycastPoint = Vector3.zero;
    private Vector3 PrevRaycastNormal = Vector3.zero;
    private int UiLayerMask;

    void Start()
    {
        UiLayerMask = LayerMask.GetMask("UI");
        RaycastPoint = ReticleImage.position;
        RaycastNormal = ReticleImage.forward;
        PrevRaycastPoint = ReticleImage.position;
        PrevRaycastNormal = ReticleImage.forward;
    }

    // Update is called once per frame
    void Update()
    {
        if (Networking.LocalPlayer == null) return;
        if (!Networking.LocalPlayer.IsValid()) return;

        // 発射位置(ShootPos)からレイキャストする
        ReticleRay.origin = ShootPos.position;
        ReticleRay.direction = ShootPos.TransformDirection(Vector3.forward);
        RaycastHit hit;
        if (Physics.Raycast(ReticleRay, out hit))
        {
            // 壁との衝突位置(HitPos)からHeadにレイキャストする
            var trackingHead = Networking.LocalPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
            Head.SetPositionAndRotation(
                trackingHead.position,
                trackingHead.rotation
            );
            HitPos.position = hit.point;
            HitPos.LookAt(Head);
            ReticleRay.origin = HitPos.position;
            ReticleRay.direction = HitPos.TransformDirection(Vector3.forward);
            if (Physics.Raycast(ReticleRay, out hit, Mathf.Infinity, UiLayerMask))
            {
                RaycastPoint = hit.point;
                RaycastNormal = hit.normal;
            }
        }
        // ローパスフィルタをかける
        ReticleImage.Translate((RaycastPoint - PrevRaycastPoint) * RaycastPointTau, Space.World);
        ReticleImage.forward = Vector3.Lerp(PrevRaycastNormal, RaycastNormal, RaycastNormalTau);
        for (int i = 0; i < 3; i++)
        {
            PrevRaycastPoint[i] = ReticleImage.position[i];
            PrevRaycastNormal[i] = ReticleImage.forward[i];
        }
    }
}

Unityプロジェクトは以下にあります。
https://github.com/hide00310/UdonSharp_Reticle

UnityEditor拡張でパスからC#スクリプトをAddComponentする

コード

以下はCubeゲームオブジェクトにAddComponentScriptPathScriptスクリプト
AddComponentする例です。

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class AddComponentScriptPathEditor : EditorWindow
{
    [MenuItem("My Window/Add Component Script")]
    public static void Execute()
    {
        // Cubeを検索
        var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
        GameObject Cube = null;
        foreach (var obj in scene.GetRootGameObjects())
        {
            if (obj.name == "Cube") Cube = obj;
        }
        // CubeにAddComponentScriptPathScriptコンポーネントを追加
        AddComponentScript(Cube, "Assets/UnityEditor_AddComponentScriptPath/AddComponentScriptPathScript.cs");
    }
    static MonoBehaviour AddComponentScript(GameObject obj, string path)
    {
        var script = AssetDatabase.LoadAssetAtPath<MonoScript>(path);
        Debug.Assert(script != null, $"{obj.name} {path}");
        MonoBehaviour component = (MonoBehaviour)Undo.AddComponent(obj, script.GetClass());
        return component;
    }
}

Unityプロジェクトは以下にあります。

https://github.com/hide00310/UnityEditor_AddComponentScriptPath

バージョン

Unity : 2019.4.31f1

UdonSharpのボタンクリックイベントをUnityEditor拡張で登録する

コード

以下はゲームオブジェクトCanvasコンポーネントEditorButtonScript
の関数OnClickを、ゲームオブジェクトButtonのボタンクリックイベントに
登録するコードです。

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using UnityEditor.Events;
using UdonSharpEditor;
using VRC.Udon;

public class EditorButtonEditor : EditorWindow
{
    [MenuItem("My Window/Add Button Function")]
    public static void AddButtonFunction()
    {
        // ゲームオブジェクトCanvasを検索
        var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
        GameObject Canvas = null;
        foreach (var obj in scene.GetRootGameObjects())
        {
            if (obj.name == "Canvas") Canvas = obj;
        }
        // スクリプトEditorButtonScriptを取得
        var EditorButtonScript = Canvas.GetComponent<EditorButtonScript>();
        // ゲームオブジェクトButtonを検索
        var Button = Canvas.transform.Find("Button").gameObject;
        // ボタンイベントを登録
        SetButtonClickEvent(Canvas, Button, EditorButtonScript, "OnClick");
    }

    static void SetButtonClickEvent(GameObject obj, GameObject buttonObj, MonoBehaviour script, string eventName)
    {
        var button = buttonObj.GetComponent<UnityEngine.UI.Button>();
        UdonBehaviour backingBehaviour = UdonSharpEditorUtility.GetBackingUdonBehaviour((UdonSharp.UdonSharpBehaviour)script);
        UnityEventTools.RemovePersistentListener<string>(button.onClick, backingBehaviour.SendCustomEvent);
        UnityEventTools.AddStringPersistentListener(button.onClick, backingBehaviour.SendCustomEvent, eventName);
    }
}
#endif

Unityプロジェクトは以下にあります。

https://github.com/hide00310/UdonSharp_EditorButton

バージョン

VRChat SDK : 3.1.10
UdonSharp : 1.1.6

UdonSharpのイベント同期で複数の引数を渡す

はじめに

VRChat UdonSharpの同期手法であるSendCustomNetworkEvent
は関数に引数を渡すことができません。
そこで、変数同期を使い疑似的に、複数引数を持つ関数を
同期する手法について説明します。

コード

以下は関数SendMyEvent1, SendMyEvent2を同期する例です。

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using UnityEngine.UI;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class EventMultiArgs : UdonSharpBehaviour
{
    public Text Text1;
    public Text Text2;

    [UdonSynced]
    public bool EventFlag1;
    [UdonSynced]
    public bool EventFlag2;
    [UdonSynced]
    public int Arg1;
    [UdonSynced]
    public int Arg2;
    [UdonSynced]
    public int Arg3;

    bool EventFlag1Sub;
    bool EventFlag2Sub;
    int Arg1Sub;
    int Arg2Sub;
    int Arg3Sub;

    bool PrevEventFlag1;
    bool PrevEventFlag2;

    int Cnt;
    int Cnt2;

    public void OnClick1()
    {
        if (!Networking.LocalPlayer.IsOwner(gameObject))
        {
            Networking.SetOwner(Networking.LocalPlayer, gameObject);
        }
        Cnt++;
        SendMyEvent1(Cnt, Cnt+1);
    }
    public void OnClick2()
    {
        if (!Networking.LocalPlayer.IsOwner(gameObject))
        {
            Networking.SetOwner(Networking.LocalPlayer, gameObject);
        }
        Cnt2++;
        SendMyEvent2(Cnt2);
    }

    public void SendMyEvent1(int arg1, int arg2)
    {
        Arg1Sub = arg1;
        Arg2Sub = arg2;
        EventFlag1Sub = !EventFlag1Sub;
        RequestSerialization();
    }
    public void SendMyEvent2(int arg3)
    {
        Arg3Sub = arg3;
        EventFlag2Sub = !EventFlag2Sub;
        RequestSerialization();
    }
    public override void OnPreSerialization()
    {
        EventFlag1 = EventFlag1Sub;
        EventFlag2 = EventFlag2Sub;
        Arg1 = Arg1Sub;
        Arg2 = Arg2Sub;
        Arg3 = Arg3Sub;
        UpdateText();
    }
    public override void OnDeserialization()
    {
        UpdateText();
    }
    private void UpdateText()
    {
        if (EventFlag1 != PrevEventFlag1)
        {
            Text1.text = $"Arg1:{Arg1}, Arg2:{Arg2}";
            PrevEventFlag1 = EventFlag1;
        }
        if (EventFlag2 != PrevEventFlag2)
        {
            Text2.text = $"Arg3:{Arg3}";
            PrevEventFlag2 = EventFlag2;
        }
    }
}

Unityプロジェクトは以下にあります。
https://github.com/hide00310/UdonSharp_EventMultiArgs

以下順番でコードが実行されます。

  1. ボタンを押すとOnClick1, OnClick2が呼ばれる
  2. ローカルでSendMyEvent1, SendMyEvent2が呼ばれる
  3. 関数の引数に対応する同期変数Arg1Sub\~Arg3Subを設定する
  4. イベント発火用の同期変数EventFlag1Sub, EventFlag2Subを設定する
  5. ローカルで同期時にOnPreSerializationが呼ばれる
  6. Arg1\~Arg3,EventFlag1, EventFlag2を設定し同期する
  7. ローカルで設定された引数をテキストに表示
  8. (ローカル以外の)ネットワークで同期時にOnDeserializationが呼ばれる
  9. ネットワークで設定された引数をテキストに表示

バージョン

Unity : 2019.4.31f1
VRChat SDK : 3.1.10
UdonSharp : 1.1.6

Blender Python API Tips

検証 Blender Version : 3.2.1

コマンドラインからスクリプトを実行する

sample.blendを起動し、sample.pyを実行する例です

"C:\Program Files\Blender Foundation\Blender 3.2\blender.exe" sample.blend --python sample.py
import bpy

def _override_context():
    idx = bpy.context.window_manager.windows[:].index(bpy.context.window)
    window = bpy.context.window_manager.windows[idx]        
    screen = window.screen
    area = [
        area for area in screen.areas
        if area.type == 'VIEW_3D'
    ][0]
    region = [
        region for region in area.regions 
        if region.type == 'WINDOW'
    ][0]
    return bpy.context.temp_override(window=window, area=area, region=region)

if __name__ == '__main__':
    with _override_context():
        # ここにスクリプトを記述
        # 以下はCubeオブジェクトのUVをスケールする例
        bpy.data.objects['Cube'].select_set(True)
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.context.area.ui_type = 'UV'
        bpy.context.scene.tool_settings.use_uv_select_sync = True
        bpy.ops.uv.select_all(action='SELECT')
        bpy.ops.transform.resize(value=(0.5, 1, 1))

Context Overrideについて

コマンドラインから実行するとbpy.context.areaなどがNoneなので、
それらを使用する場合はbpy.context.temp_overrideが必要になります。

参考 : https://stackoverflow.com/questions/70958391/execute-script-after-blender-is-fully-loaded

オブジェクトを結合する

オブジェクトCube, Cube.001を結合してCube_Joinにリネームする例です。

import bpy

def join_objects(join_name, object_names):
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    for object_name in object_names:
        bpy.data.objects[object_name].select_set(True)
    bpy.context.view_layer.objects.active = bpy.data.objects[object_names[0]]
    bpy.ops.object.join()
    bpy.data.objects[object_names[0]].name = join_name

if __name__ == '__main__':
    join_objects('Cube_Join', ['Cube', 'Cube.001'])

マテリアルを結合して1つのマテリアルにする

オブジェクトCubeのマテリアルMaterial, Material.001(に割り当てられた面)を
結合してMaterial_Joinにリネームする例です。

import bpy

def _set_active_material_slot(object, material):
    for i, material_slot in enumerate(object.material_slots):
        if material is material_slot.material:
            object.active_material_index = i

def join_materials(object_name, join_name, material_names):
    object = bpy.data.objects[object_name]
    object.select_set(True)
    bpy.ops.object.mode_set(mode='EDIT')
    materials = bpy.data.materials
    bpy.ops.mesh.select_all(action='DESELECT')

    for material_name in material_names:
        material = materials[material_name]
        _set_active_material_slot(object, material)
        bpy.ops.object.material_slot_select()
    
    material = materials[material_name]
    _set_active_material_slot(object, material)
    bpy.ops.object.material_slot_assign()

    bpy.ops.object.mode_set(mode='OBJECT')
    for material_name in material_names[1:]:
        material = materials[material_name]
        _set_active_material_slot(object, material)
        bpy.ops.object.material_slot_remove()
    
    materials[material_names[0]].name = join_name

if __name__ == '__main__':
    join_materials('Cube', 'Material_Join', ['Material', 'Material.001'])

マテリアルのテクスチャを差し替える

マテリアルMaterialのテクスチャをtexture2.pngに差し替える例です。

import bpy

def _load_image(image_path, image_name):
    if image_name not in bpy.data.images:
        image = bpy.data.images.load(image_path)
        image.name = image_name

def relink_material_texture(material_name, image_path):
    image_name = bpy.path.basename(image_path)
    _load_image(image_path, image_name)
    material = bpy.data.materials[material_name]
    node = [link.from_node for link in material.node_tree.links
        if (
            link.from_node.type == 'TEX_IMAGE' and
            link.to_node.type == 'BSDF_PRINCIPLED' and
            link.from_socket.name == 'Color' and
            link.to_socket.name == 'Base Color'
        )
    ][0]
    node.image = bpy.data.images[image_name]

if __name__ == '__main__':
    relink_material_texture('Material', '//texture2.png')

UVを結合して1つのテクスチャにする

オブジェクトPlane, Plane.001, Plane.002のUV(512x512pixel)を変形し、
オブジェクト・マテリアルをPlane_Join, Material_Joinに結合し、
テクスチャをtexture_join.png(1024x1024pixel)に差し替える例です。

import bpy
import numpy as np

def transform_uv(object_name, join_image_name, src_image_name, pos):
    """
    UVを変形する
    pos : 左上を原点としたイメージの位置(pixel)
    """

    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    object = bpy.data.objects[object_name]
    object.select_set(True)
    bpy.context.view_layer.objects.active = object

    bpy.ops.object.mode_set(mode='EDIT')
    bpy.context.area.ui_type = 'UV'
    image_editor = bpy.context.space_data
    bpy.context.scene.tool_settings.use_uv_select_sync = True

    join_image = bpy.data.images[join_image_name]
    join_image_size = np.array(join_image.size[:])

    src_image = bpy.data.images[src_image_name]
    src_image_size = np.array(src_image.size[:])

    bpy.ops.uv.select_all(action='SELECT')
    image_editor.cursor_location = [0, 1]
    image_editor.pivot_point = 'CURSOR'

    resize = src_image_size / join_image_size
    bpy.ops.transform.resize(
        value=(resize[0], resize[1], 1),
        orient_type='GLOBAL',
        orient_matrix=np.eye(3), 
        constraint_axis=(True, True, True)
    )
    
    translate = np.array(pos) / join_image_size
    bpy.ops.transform.translate(
        value=(translate[0], -translate[1], 0),
        orient_matrix=np.eye(3), 
        constraint_axis=(True, True, True)
    )

infos = {
    'join_object_name' : 'Plane_Join',
    'join_material_name' : 'Material_Join',
    'join_image_name' : 'texture_join.png',
    'objects' : [
        {
            'name' : 'Plane',
            'image' : 'texture1.png',
            'material' : 'Material',
            'pos' : (0, 0),
        },
        {
            'name' : 'Plane.001',
            'image' : 'texture2.png',
            'material' : 'Material.001',
            'pos' : (512, 0),
        },
        {
            'name' : 'Plane.002',
            'image' : 'texture3.png',
            'material' : 'Material.002',
            'pos' : (0, 512),
        },
    ]
}

if __name__ == '__main__':
    for info in infos['objects']:
        transform_uv(
            info['name'],
            infos['join_image_name'],
            info['image'],
            info['pos']
        )

    join_objects(
        infos['join_object_name'],
        [info['name'] for info in infos['objects']]
    )
    join_materials(
        infos['join_object_name'],
        infos['join_material_name'],
        [info['material'] for info in infos['objects']]
    )
    relink_material_texture(
        infos['join_material_name'],
        '//'+infos['join_image_name']
    )

MixamoアニメーションをBlenderで調整しUnityで読み込む

はじめに

Mixamoは大量の3Dモデルのアニメーションが無料で利用できるサイトです。
またMixamo公式からControl Rig(IK)を設定するBlenderアドオンが公開されており、
簡単にアニメーションを作成できます。
今回はMixamoアニメーションを自作のモデルに適用し、
Blenderでアニメーションを調整し、
Unityでアニメーションを読み込む方法を説明します。

手順概要

以下のような例で手順を説明します。手順2まではすでに終わっているものとし、
手順3から説明します。

  1. Blenderで自作モデルを作成
  2. Unityで自作モデルををセットアップ
  3. Mixamoセットアップ ← ここから説明
  4. Mixamoから「歩く、振り返る」アニメーションをダウンロード
  5. Blenderでアニメーションをインポート
  6. Blenderでアニメーションを「歩く→振り返る」に合成
  7. Blenderでアニメーションをエクスポート
  8. Unityでアニメーションをインポート
  9. Unityでモデルにアニメーションを適用

前提知識

  • BlenderでFBXモデルをエクスポートしUnityでインポートできる
  • BlenderでNonlinear Animationを使い既存のアニメーションを合成できる
  • Unityで既存のアニメーションクリップを使いモデルにアニメーションを適用できる

手順

開始状態

以下のように自作Unity用モデルがセットアップされた状態で始めます。

今回は受付嬢さんを使用します。

ボーン名は次のようにUnityの名前に合わせます。

Blender : アドオンのインストール

以下2つのアドオンをインストールします

Blender : Mixamo元モデルのダウンロード

Unity用モデルをMixamoモデルに変換するため、
Mixamoから元モデルをダウンロードします。

  • https://www.mixamo.com/
  • Charactersタブ
  • 適当なキャラクターを選択
    • 注意 : 標準のx-bot, y-botはMixamo add-on for Blenderに対応していないため別キャラクターにしてください
    • 今回はMichellにします
  • DOWNLOAD

Blender : Mixamoアップロード用モデルに変換

Mixamoにアップロードするためのモデルを作成します

  • Armatureを複製します。ここではArm_MixamoUploadとします
  • Mixamo元モデルをインポートします。ここではArm_MixamoOrgとします
  • Miscタブ > PyBone Convertor から以下設定でConvertします
    • Arm_MixamoUploadを選択
    • Script : ${.}/unity_to_mixamo.py
    • Source Armature : Arm_MixamoOrg
    • Align Roll : Check
    • Add not existing bones : Check
    • Remove not existing bones : Check

Arm_MixamoUploadのボーンがMixamo用に変換されます

  • Arm_MixamoUploadのアーマチュアだけをArm_MixamoUpload.fbxとしてエクスポートします。設定はUnityと同じです

Mixamo : アニメーションのダウンロード

  • Mixamoサイト > UPLOAD CHARACTER でArm_MixamoUpload.fbxをアップロードします
  • Animationタブで検索し、WalkingとRight Turnをダウンロードします

Blender : Control Rigの作成

Control Rigを適用したモデルを作成します

  • 自作モデルのArmatureとメッシュを複製します。ここではArm_MixamoCtrlとします
  • Miscタブ > PyBone Convertor から以下設定でConvertします
    • Arm_MixamoCtrlを選択
    • Script : ${.}/unity_to_mixamo.py
    • Source Armature : Arm_MixamoOrg
    • Align Roll : Check
    • Add not existing bones : Check
  • Mixamoタブ > Control Rig > Create Control Rig でControl Rigを作成します

Blender : アニメーションの合成

アニメーション作成
  • WalkingとRight Turnをインポートします
  • 分かりやすいようにアーマチュアとアニメーションをOrgWalking, OrgTurnにリネームします

  • Arm_MixamoCtrlを選択します
  • Mixamoタブ
    • Source Skeleton : Arm_OrgWalking
    • Apply Animation to Control Rig
  • Animation Workspace > Nonlinear Animation からArm_MixamoCtrlのActionをPush Downします
  • OrgTurnも同様に行います

  • Unityで取り込むために1フレーム目にTPoseアニメーションを作成します
    • Poseモード
    • Armature > Clear Tramsformする
    • 全ボーンを選択
    • Iキーでキーを挿入
    • ActionをPush Down
  • Nonlinear AnimationでWalkingとTurnを合成します

アニメーションベイク
  • Pose Modeにします
  • ボーンを全選択解除します
  • Object Data Property > Layers で2番目を選択します
  • ボーンを全選択します
  • Nonlinear Animation > Edit > Bake Action で以下をチェックしてベイクします
    • Only Selected Bones
    • Visual Keying
    • Clean Curves

  • Action名を分かりやすいものに変えます。ここではWalkingTurnとします
  • Arm_MixamoCtrlのアーマチュアだけをArm_MixamoCtrl.fbxとしてエクスポートします

Unity : アニメーションのインポート

  • Arm_MixamoCtrl.fbxを選択します
  • Rig
    • Animation Type : Humanoid
    • Apply
  • Arm_MixamoCtrl.fbxの中からArm_MixamoCtrl|WalkingTurnアニメーションクリップを選択します
  • Ctrl+Dでアニメーションクリップを複製します
    • 複製後はArm_MixamoCtrl.fbxを削除しても問題ありません
  • Arm_MixamoCtrl_WalkingTurnアニメーションクリップの設定を以下のようにします
    • Root Transform Rotation/Position(Y)/Position(XY)
      • Bake into Pose : Check
      • Based Upon(at Start) : Original

  • AnimatorにArm_MixamoCtrl_WalkingTurnを設定して完了です


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.