概要
- 撮影した動画やiOSで読み込んだ動画をPlask APIに送信し、出力した後
- Plask API outputをUnityで3Dモデルのアニメーションに変換
手順
3Dモデルの設定
-
- Unityシーンへモデルをインポートする前に、Plask API outputのローカル軸と3Dモデルのローカル軸を揃えることが必要です。
- 使用する3Dモデルの軸を揃えるリギングは、以下のドキュメントを参考に行ってください。
モデルのインポートとUnity上でのシーン構築
-
3Dモデルをインポートして、UnityからPlask API outputを適用してください。
-
サンプルモデル
-
-
Writing Unity script to apply API outputに則って書かれたスクリプトが、3Dモデルのコンポーネントとして追加されます。
-
Unity SceneをiOSプラットフォームにビルドし、iOS projectに追加してください。
API output 構造
-
API outputは、下記の例のような構造を持つjsonファイルとなります
{
"result": [
{
"motionNumber": 0,
"trackData": [
{
"boneName": "hips",
"fps": 30,
"property": "position",
"transformKeys": [
{
"frame": 0,
"time": 0,
"value": [
0.06301826238632202,
0,
0.2913147509098053
]
}
]
}
]
}
],
"workingtime": 20.022384643554688
}-
例
{
"result": [
{
"motionNumber": 0,
"trackData": [
{
"boneName": "hips",
"fps": 30,
"property": "position",
"transformKeys": [
{
"frame": 0,
"time": 0,
"value": [
0.06301826238632202,
0,
0.2913147509098053
]
}
]
}
]
}
],
"workingtime": 20.022384643554688
}
-
-
Motion capture dataは、“result”>”trackData”に含まれています。中には、24個のボーンからなるスケルトンをベースにした位置データ [[[(x, y, z)]]] や回転データ (x, y, z, w) が保存されています。ボーンは次のような構造になっています。
ソースのボーン構造。
-
“result”>”trackData” の配列のうち、hipsは位置とクォータニオン型の値を持ち、hipsを除く23個のボーンはクォータニオン型の値のみを持つため、合計25個の要素が存在することになります。各要素は、boneName・fps・property・transformKeysから構成されます。
"trackData": [
{
"boneName": "hips",
"fps": 30,
"property": "position",
"transformKeys": [
{
"frame": 0,
"time": 0,
"value": [
0.0630182 6238632202,
0,
0.2913147509098053
]
},
...
]
},
{
"boneName": "hips",
"fps": 30,
"property": "rotationQuaternion",
"transformKeys": [ ...
]
},
{
"boneName": "leftUpLeg",
"fps": 30,
"property": "rotationQuaternion",
"transformKeys": [ ...
]
},
...
{
"boneName": "rightHandIndex1",
"fps": 30,
"property": "rotationQuaternion",
"transformKeys": [ ...
]
},- boneName はモーションキャプチャデータを有するボーンの名称です。
- fps は、動きを抽出した動画のフレームレート(fps)です。
- property は、変換値のタイプを表します。
- transformKeys は、各ポーズに対応した時間と値からなる配列です。各ポーズには、フレーム・時間・値の情報が存在します。
- frame は、ポーズのフレーム順序を表します。
- time は、ポーズをする時間を表します。単位は秒です。
- value はポーズの変換値を表します。プロパティの種類が位置の場合は3つの値 (x, y, z)、rotationQuaternionの場合は4つの値 (x, y, z, w) を持ちます。
API output networking: iOS > Unity
-
でビルドしたUnity画面を呼び出してください。
class AppDelegate: UIResponder, UIApplicationDelegate {
//method to call unity screen
func initAndShowUnity() -> Void {
if let framework = self.unityFrameworkLoad() {
self.unityFramework = framework
self.unityFramework?.setDataBundleId("com.unity3d.framework")
self.unityFramework?.runEmbedded(withArgc: CommandLine.argc,
argv: CommandLine.unsafeArgv,
appLaunchOpts: [:])
self.unityFramework?.showUnityWindow()
print("windows: \\(application?.windows)")
}
}
}
class VideoPreviewViewController: UIViewController, AVAudioPlayerDelegate {
private func pushSceneViewController(mocapJsonString: String?,
duration: Double) {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.initAndShowUnity()
unityViewController = appDelegate.unityFramework?.appController()?.rootViewController
viewModel.send(event: VideoPreviewEvent.setUnityNotification)
viewModel.send(event: VideoPreviewEvent.handleUnityReady)
}
}
} -
iOS上でPlask APIとのネットワーク接続が完了したら、応答データをjsonからStringに変換し、Unity script (unityFramework)]に送信します。
class VideoPreviewViewModel: NSObject {
private func handleUnityReady() {
guard let jsonString = self.jsonString else { return } //API output converted to String
unityManager.makeAnimationClip(jsonString: jsonString)
}
}
class UnityManager {
private var appDelegate: AppDelegate? {
return UIApplication.shared.delegate as? AppDelegate
}
func makeAnimationClip(jsonString: String?) {
appDelegate?.unityFramework?.sendMessageToGO(withName: "vangaurd_t_choonyung", //3d model
functionName: "MakeAnimationClip", //method
message: jsonString) //API output converted to String
}
}
Unity scriptを作成し、API outputを適用する
-
- API outputに含まれる24個のソースボーンと、それに対応する3Dモデルのターゲットボーンに対して、{<ターゲットボーン>:<ソースボーン>}の形式でbonesDictionaryを定義します。
- ディクショナリーのキーを使用する3Dモデルのボーンに置き換えます。
public class MocapAnimation : MonoBehaviour {
Dictionary<string, string> bonesDictionary = new Dictionary<string, string>()
{
{ "mixamorig:Hips", "hips" },
{ "mixamorig:LeftUpLeg", "leftUpLeg" },
{ "mixamorig:RightUpLeg", "rightUpLeg" },
{ "mixamorig:Spine", "spine" },
{ "mixamorig:LeftLeg", "leftLeg" },
{ "mixamorig:RightLeg", "rightLeg" },
{ "mixamorig:Spine1", "spine1" },
{ "mixamorig:LeftFoot", "leftFoot" },
{ "mixamorig:RightFoot", "rightFoot" },
{ "mixamorig:Spine2", "spine2" },
{ "mixamorig:LeftToeBase", "leftToeBase" },
{ "mixamorig:RightToeBase", "rightToeBase" },
{ "mixamorig:Neck", "neck" },
{ "mixamorig:LeftShoulder", "leftShoulder" },
{ "mixamorig:RightShoulder", "rightShoulder" },
{ "mixamorig:Head", "head" },
{ "mixamorig:LeftArm", "leftArm" },
{ "mixamorig:RightArm", "rightArm" },
{ "mixamorig:RightForeArm", "rightForeArm" },
{ "mixamorig:LeftForeArm", "leftForeArm" },
{ "mixamorig:LeftHand", "leftHand" },
{ "mixamorig:RightHand", "rightHand" },
{ "mixamorig:LeftHandMiddle1", "leftHandIndex1" },
{ "mixamorig:RightHandMiddle1", "rightHandIndex1" }
};
...
}-
サンプルモデル
-
JSON stringをデシリアライズした後、<string, trackData>の形式でディクショナリーを作成し、各データをパースして簡単に検索できるようにします。
-
string: “<source bone name>.<transform type>”
- Source bone: API outputの24個のジョイントを表す名称
- Transform type: :位置やrotationQuaternionなどのプロパティ
-
trackData : boneName, fps, property, transformKeys (custom defined)を含んだクラス
- boneName: “<source bone>”
- fps: 1秒当たりのフレーム数(フレームレート)
- property: “<transform type>”
- transformKeys: フレームデータ(任意で定義)
- frame: フレーム数
- times: キーフレームを含んだ時間の配列
- values: 時間と一致する位置座標またはクォータニオン座標の値
-
<string, trackData> 例
// 1
"hips.position"
: trackData(boneName: "hips", fps: 30, property: "position", transformKeys: [(frame: 0, time: 0, values: [0.0, 1.0, 3.0]), ...])
// 2
"rightHandIndex1.rotationQuaternion"
: mocapData(boneName: "rightHandIndex1", fps: 30, property: "rotationQuaternion", transformKeys: [(frame: 3, time: 0.166, values: [0.0, 15.0, 30.0, 45.0]), ...)
[Serializable]
public class MocapResult {
public string id;
public MocapData[] result;
public float workingtime;
}
[Serializable]
public class MocapData {
public int motionNumber;
public TrackData[] trackData;
}
[Serializable]
public class TrackData {
public string boneName;
public int fps;
public string property;
public TransformKeys[] transformKeys;
}
[Serializable]
public class TransformKeys {
public int frame;
public float time;
public float[] value;
}
public class MocapAnimation : MonoBehaviour {
// ...
void MakeAnimationClip(string jsonString) {
MocapResult mocapResult = JsonUtility.FromJson<MocapResult>(jsonString);
Dictionary<String, TrackData> mocapDictionary = new Dictionary<string, TrackData>();
MocapData result = mocapResult.result[0];
for (int i = 0; i < result.trackData.Length; i++) {
string key = result.trackData[i].boneName + "." + result.trackData[i].property;
mocapDictionary.Add(key, result.trackData[i]);
}
if (transform.childCount > 0) {
ApplyAnimationFromTransform(transform, mocapDictionary);
}
}
// ...
} -
-
<target bone>:<source bone>}に従い、API output内のbonesDictionary, trackDataのペアをターゲットボーンのAnimation componentに適用する関数に引き渡します。
- アニメーションを実行するには、3DモデルにAnimation componentを含める必要があります。
- 3Dモデルのすべてのボーンを調べて、ボーンがboneDictionary内に含まれているかを確認してください。
- ボーンがboneDictionaryに存在する場合、アニメーションコンポーネントにtrackDataを適用するための関数に引き渡されます。
public class MocapAnimation : MonoBehaviour {
// ...
private void ApplyAnimationFromTransform(Transform parent, Dictionary<String, TrackData> trackDataDictionary) {
if (parent.childCount > 0) {
foreach (Transform child in parent) {
child.gameObject.AddComponent(typeof(Animation));
string childName = child.gameObject.name;
if (bonesDictionary.ContainsKey(childName) == true) {
string boneName = bonesDictionary[childName];
if (boneName == "hips") {
SetObjectPositionAndQuaternionAnimation(child, trackDataDictionary);
} else {
TrackData trackData = trackDataDictionary[boneName + ".rotationQuaternion"];
SetObjectQuaternionAnimation(child, trackData);
}
}
ApplyAnimationFromTransform(child, trackDataDictionary);
}
}
}
//...
} -
コンポーネントを作成し、適用するための関数
- SetObjectPositionAndQuaternionAnimationは、ソースボーンがhipsに対応するターゲットボーンのアニメーションクリップを作成する際に使用する関数です。
- SetObjectQuaternionAnimationは、ソースボーンがnot hipsである23個のジョイントに対応する、ターゲットボーンのアニメーションコンポーネントを作成するために使用される関数です。
- 手順
- 上記で作成した3Dモデルのアニメーションコンポーネントを呼び出します。
- 位置座標 (x, y, z)、クォータニオン座標 (x, y, z, w) のプロパティごとにキーフレーム配列を作成し、API outputの値を順番に挿入していきます。
- キーフレーム配列を反映したAnimationCurveを作成します
- AnimationCurveを反映したAnimationClipを作成します
- キーワード
- Keyframe Array: 位置と値の数値を順番に並べた配列
- AnimationCurve: AnimationClipを作成する際に、時間経過によって評価できるキーフレームの集合を保存
public class MocapAnimation : MonoBehaviour {
// ...
//Create and apply position and quaternion animation of hips bones
private void SetObjectPositionAndQuaternionAnimation(Transform transform, Dictionary<String, TrackData> trackDataDictionary) {
TrackData positionData = trackDataDictionary["hips.position"];
TrackData quaternionData = trackDataDictionary["hips.rotationQuaternion"];
Animation anim = transform.GetComponent<Animation>();
Keyframe[] positionKeysX = new Keyframe[positionData.transformKeys.Length];
Keyframe[] positionKeysY = new Keyframe[positionData.transformKeys.Length];
Keyframe[] positionKeysZ = new Keyframe[positionData.transformKeys.Length];
Keyframe[] quaternionKeysX = new Keyframe[quaternionData.transformKeys.Length];
Keyframe[] quaternionKeysY = new Keyframe[quaternionData.transformKeys.Length];
Keyframe[] quaternionKeysZ = new Keyframe[quaternionData.transformKeys.Length];
Keyframe[] quaternionKeysW = new Keyframe[quaternionData.transformKeys.Length];
for (int i = 0; i < positionData.transformKeys.Length; i++) {
TransformKeys positionTransformKey = positionData.transformKeys[i];
float positionTime = positionTransformKey.time;
positionKeysX[i] = new Keyframe(positionTime, positionTransformKey.value[0]);
positionKeysY[i] = new Keyframe(positionTime, positionTransformKey.value[1]);
positionKeysZ[i] = new Keyframe(positionTime, positionTransformKey.value[2]);
TransformKeys quaternionTransformKey = quaternionData.transformKeys[i];
float quaternionTime = quaternionTransformKey.time;
quaternionKeysX[i] = new Keyframe(quaternionTime, quaternionTransformKey.value[0]);
quaternionKeysY[i] = new Keyframe(quaternionTime, -quaternionTransformKey.value[1]);
quaternionKeysZ[i] = new Keyframe(quaternionTime, -quaternionTransformKey.value[2]);
quaternionKeysW[i] = new Keyframe(quaternionTime, quaternionTransformKey.value[3]);
}
var positionCurveX = new AnimationCurve(positionKeysX);
var positionCurveY = new AnimationCurve(positionKeysY);
var positionCurveZ = new AnimationCurve(positionKeysZ);
var quaternionCurveX = new AnimationCurve(quaternionKeysX);
var quaternionCurveY = new AnimationCurve(quaternionKeysY);
var quaternionCurveZ = new AnimationCurve(quaternionKeysZ);
var quaternionCurveW = new AnimationCurve(quaternionKeysW);
AnimationClip animationClip = new AnimationClip();
animationClip.legacy = true;
animationClip.SetCurve("", typeof(Transform), "localPosition.x", positionCurveX);
animationClip.SetCurve("", typeof(Transform), "localPosition.y", positionCurveY);
animationClip.SetCurve("", typeof(Transform), "localPosition.z", positionCurveZ);
animationClip.SetCurve("", typeof(Transform), "localRotation.x", quaternionCurveX);
animationClip.SetCurve("", typeof(Transform), "localRotation.y", quaternionCurveY);
animationClip.SetCurve("", typeof(Transform), "localRotation.z", quaternionCurveZ);
animationClip.SetCurve("", typeof(Transform), "localRotation.w", quaternionCurveW);
anim.AddClip(animationClip, "mocapAnimation");
anim.Play("mocapAnimation");
}
//Create and apply bones quaternion animation except for hips
private void SetObjectQuaternionAnimation(Transform child, TrackData trackData) {
Animation anim = child.GetComponent<Animation>();
int frameLength = trackData.transformKeys.Length;
Keyframe[] keysX = new Keyframe[frameLength];
Keyframe[] keysY = new Keyframe[frameLength];
Keyframe[] keysZ = new Keyframe[frameLength];
Keyframe[] keysW = new Keyframe[frameLength];
for (int i = 0; i < frameLength; i++) {
TransformKeys transformKey = trackData.transformKeys[i];
float time = transformKey.time;
keysX[i] = new Keyframe(time, transformKey.value[0]);
keysY[i] = new Keyframe(time, -transformKey.value[1]);
keysZ[i] = new Keyframe(time, -transformKey.value[2]);
keysW[i] = new Keyframe(time, transformKey.value[3]);
}
var curveX = new AnimationCurve(keysX);
var curveY = new AnimationCurve(keysY);
var curveZ = new AnimationCurve(keysZ);
var curveW = new AnimationCurve(keysW);
AnimationClip animationClip = new AnimationClip();
animationClip.legacy = true;
animationClip.SetCurve("", typeof(Transform), "localRotation.x", curveX);
animationClip.SetCurve("", typeof(Transform), "localRotation.y", curveY);
animationClip.SetCurve("", typeof(Transform), "localRotation.z", curveZ);
animationClip.SetCurve("", typeof(Transform), "localRotation.w", curveW);
anim.AddClip(animationClip, "mocapAnimation");
anim.Play("mocapAnimation");
}
}
ソースコード:Unity script
MocapAnimation.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
[Serializable]
public class MocapResult {
public string id;
public MocapData[] result;
public float workingtime;
}
[Serializable]
public class MocapData {
public int motionNumber;
public TrackData[] trackData;
}
[Serializable]
public class TrackData {
public string boneName;
public int fps;
public string property;
public TransformKeys[] transformKeys;
}
[Serializable]
public class TransformKeys {
public int frame;
public float time;
public float[] value;
}
public class MocapAnimation : MonoBehaviour {
Dictionary<string, string> bonesDictionary = new Dictionary<string, string>()
{
{ "mixamorig:Hips", "hips" },
{ "mixamorig:LeftUpLeg", "leftUpLeg" },
{ "mixamorig:RightUpLeg", "rightUpLeg" },
{ "mixamorig:Spine", "spine" },
{ "mixamorig:LeftLeg", "leftLeg" },
{ "mixamorig:RightLeg", "rightLeg" },
{ "mixamorig:Spine1", "spine1" },
{ "mixamorig:LeftFoot", "leftFoot" },
{ "mixamorig:RightFoot", "rightFoot" },
{ "mixamorig:Spine2", "spine2" },
{ "mixamorig:LeftToeBase", "leftToeBase" },
{ "mixamorig:RightToeBase", "rightToeBase" },
{ "mixamorig:Neck", "neck" },
{ "mixamorig:LeftShoulder", "leftShoulder" },
{ "mixamorig:RightShoulder", "rightShoulder" },
{ "mixamorig:Head", "head" },
{ "mixamorig:LeftArm", "leftArm" },
{ "mixamorig:RightArm", "rightArm" },
{ "mixamorig:RightForeArm", "rightForeArm" },
{ "mixamorig:LeftForeArm", "leftForeArm" },
{ "mixamorig:LeftHand", "leftHand" },
{ "mixamorig:RightHand", "rightHand" },
{ "mixamorig:LeftHandMiddle1", "leftHandIndex1" },
{ "mixamorig:RightHandMiddle1", "rightHandIndex1" }
};
// iOS에서 받아온 jsonString -> deserialize
void MakeAnimationClip(string jsonString) {
MocapResult mocapResult = JsonUtility.FromJson<MocapResult>(jsonString);
Dictionary<String, TrackData> mocapDictionary = new Dictionary<string, TrackData>();
MocapData result = mocapResult.result[0];
for (int i = 0; i < result.trackData.Length; i++) {
string key = result.trackData[i].boneName + "." + result.trackData[i].property;
mocapDictionary.Add(key, result.trackData[i]);
}
if (transform.childCount > 0) {
ApplyAnimationFromTransform(transform, mocapDictionary);
}
}
// bones 비교
private void ApplyAnimationFromTransform(Transform parent, Dictionary<String, TrackData> trackDataDictionary) {
if (parent.childCount > 0) {
foreach (Transform child in parent) {
child.gameObject.AddComponent(typeof(Animation));
string childName = child.gameObject.name;
if (bonesDictionary.ContainsKey(childName) == true) {
string boneName = bonesDictionary[childName];
if (boneName == "hips") {
SetObjectPositionAndQuaternionAnimation(child, trackDataDictionary);
} else {
TrackData trackData = trackDataDictionary[boneName + ".rotationQuaternion"];
SetObjectQuaternionAnimation(child, trackData);
}
}
ApplyAnimationFromTransform(child, trackDataDictionary);
}
}
}
// hips의 position, rotation Animation 생성 및 적용
private void SetObjectPositionAndQuaternionAnimation(Transform transform, Dictionary<String, TrackData> trackDataDictionary) {
TrackData positionData = trackDataDictionary["hips.position"];
TrackData quaternionData = trackDataDictionary["hips.rotationQuaternion"];
Animation anim = transform.GetComponent<Animation>();
Keyframe[] positionKeysX = new Keyframe[positionData.transformKeys.Length];
Keyframe[] positionKeysY = new Keyframe[positionData.transformKeys.Length];
Keyframe[] positionKeysZ = new Keyframe[positionData.transformKeys.Length];
Keyframe[] quaternionKeysX = new Keyframe[quaternionData.transformKeys.Length];
Keyframe[] quaternionKeysY = new Keyframe[quaternionData.transformKeys.Length];
Keyframe[] quaternionKeysZ = new Keyframe[quaternionData.transformKeys.Length];
Keyframe[] quaternionKeysW = new Keyframe[quaternionData.transformKeys.Length];
for (int i = 0; i < positionData.transformKeys.Length; i++) {
TransformKeys positionTransformKey = positionData.transformKeys[i];
float positionTime = positionTransformKey.time;
positionKeysX[i] = new Keyframe(positionTime, positionTransformKey.value[0]);
positionKeysY[i] = new Keyframe(positionTime, positionTransformKey.value[1]);
positionKeysZ[i] = new Keyframe(positionTime, positionTransformKey.value[2]);
TransformKeys quaternionTransformKey = quaternionData.transformKeys[i];
float quaternionTime = quaternionTransformKey.time;
quaternionKeysX[i] = new Keyframe(quaternionTime, quaternionTransformKey.value[0]);
quaternionKeysY[i] = new Keyframe(quaternionTime, -quaternionTransformKey.value[1]);
quaternionKeysZ[i] = new Keyframe(quaternionTime, -quaternionTransformKey.value[2]);
quaternionKeysW[i] = new Keyframe(quaternionTime, quaternionTransformKey.value[3]);
}
var positionCurveX = new AnimationCurve(positionKeysX);
var positionCurveY = new AnimationCurve(positionKeysY);
var positionCurveZ = new AnimationCurve(positionKeysZ);
var quaternionCurveX = new AnimationCurve(quaternionKeysX);
var quaternionCurveY = new AnimationCurve(quaternionKeysY);
var quaternionCurveZ = new AnimationCurve(quaternionKeysZ);
var quaternionCurveW = new AnimationCurve(quaternionKeysW);
AnimationClip animationClip = new AnimationClip();
animationClip.legacy = true;
animationClip.SetCurve("", typeof(Transform), "localPosition.x", positionCurveX);
animationClip.SetCurve("", typeof(Transform), "localPosition.y", positionCurveY);
animationClip.SetCurve("", typeof(Transform), "localPosition.z", positionCurveZ);
animationClip.SetCurve("", typeof(Transform), "localRotation.x", quaternionCurveX);
animationClip.SetCurve("", typeof(Transform), "localRotation.y", quaternionCurveY);
animationClip.SetCurve("", typeof(Transform), "localRotation.z", quaternionCurveZ);
animationClip.SetCurve("", typeof(Transform), "localRotation.w", quaternionCurveW);
anim.AddClip(animationClip, "mocapAnimation");
anim.Play("mocapAnimation");
}
// hips 외의 bones rotation Animation 생성 및 적용
private void SetObjectQuaternionAnimation(Transform child, TrackData trackData) {
Animation anim = child.GetComponent<Animation>();
int frameLength = trackData.transformKeys.Length;
Keyframe[] keysX = new Keyframe[frameLength];
Keyframe[] keysY = new Keyframe[frameLength];
Keyframe[] keysZ = new Keyframe[frameLength];
Keyframe[] keysW = new Keyframe[frameLength];
for (int i = 0; i < frameLength; i++) {
TransformKeys transformKey = trackData.transformKeys[i];
float time = transformKey.time;
keysX[i] = new Keyframe(time, transformKey.value[0]);
keysY[i] = new Keyframe(time, -transformKey.value[1]);
keysZ[i] = new Keyframe(time, -transformKey.value[2]);
keysW[i] = new Keyframe(time, transformKey.value[3]);
}
var curveX = new AnimationCurve(keysX);
var curveY = new AnimationCurve(keysY);
var curveZ = new AnimationCurve(keysZ);
var curveW = new AnimationCurve(keysW);
AnimationClip animationClip = new AnimationClip();
animationClip.legacy = true;
animationClip.SetCurve("", typeof(Transform), "localRotation.x", curveX);
animationClip.SetCurve("", typeof(Transform), "localRotation.y", curveY);
animationClip.SetCurve("", typeof(Transform), "localRotation.z", curveZ);
animationClip.SetCurve("", typeof(Transform), "localRotation.w", curveW);
anim.AddClip(animationClip, "mocapAnimation");
anim.Play("mocapAnimation");
}
}