├── Assets └── pose_unity │ ├── BVHRecorder.cs │ ├── PosTxtReader.cs │ ├── pos_sample1.txt │ ├── pos_sample2-1.txt │ ├── pos_sample2-2.txt │ └── sample_description.txt ├── LICENSE ├── LICENSES └── LICENSE_BVHRecorder.txt └── README.md /Assets/pose_unity/BVHRecorder.cs: -------------------------------------------------------------------------------- 1 | // Things to fix: 2 | // - Handle non-zero-rotation bones without producing a stupid rest pose 3 | // - Add support recording translation too 4 | // - Update API documentation 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Globalization; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Text; 12 | using UnityEngine; 13 | 14 | public class BVHRecorder : MonoBehaviour { 15 | [Header("Recorder settings")] 16 | [Tooltip("The bone rotations will be recorded this many times per second. Bone locations are recorded when this script starts running or genHierarchy() is called.")] 17 | public float frameRate = 60.0f; 18 | [Tooltip("This is the directory into which BVH files are written. If left empty, it will be initialized to the standard Unity persistant data path, unless the filename field contains a slash or a backslash, in which case this field will be ignored completely instead.")] 19 | public string directory; 20 | [Tooltip("This is the filename to which the BVH file will be saved. If no filename is given, a new one will be generated based on a timestamp. If the file already exists, a number will be appended.")] 21 | public string filename; 22 | [Tooltip("When this flag is set, existing files will be overwritten and no number will be appended at the end to avoid this.")] 23 | public bool overwrite = false; 24 | [Tooltip("When this option is set, the BVH file will have the Z axis as up and the Y axis as forward instead of the normal BVH conventions.")] 25 | public bool blender = true; 26 | [Tooltip("When this box is checked, motion data will be recorded. It is possible to uncheck and check this box to pause and resume the capturing process.")] 27 | public bool capturing = false; 28 | [Header("Advanced settings")] 29 | [Tooltip("When this option is enabled, only humanoid bones will be targeted for detecting bones. This means that things like hair bones will not be added to the list of bones when detecting bones.")] 30 | public bool enforceHumanoidBones = false; 31 | [Tooltip("This option can be used to rename humanoid bones to standard bone names. If you don't know what this means, just leave it unticked.")] 32 | public bool renameBones = false; 33 | [Tooltip("When this is enabled, after a drop in frame rate, multiple frames may be recorded in quick succession. When it is disabled, at least frame time milliseconds will pass before the next frame is recorded. Enabling it will help ensure that your recorded clip has the correct duration.")] 34 | public bool catchUp = true; 35 | //[Tooltip("Coordinates are recorded with six decimals. If you require a BVH file where only two decimals are required, you can turn this on.")] 36 | //public bool lowPrecision = false; 37 | [Tooltip("This should be checked when BVHRecorder is used through its API. It will disable its Start() and Update() functions. If you don't know what this means, just leave it unticked.")] 38 | public bool scripted = false; 39 | 40 | [Header("Motion target")] 41 | [Tooltip("This is the avatar for which motion should be captured. All skinned meshes that are part of the avatar should be children of this object. All bones should be initialized with zero rotations. This is usually the case for VRM avatars.")] 42 | public Animator targetAvatar = null; 43 | [Tooltip("This is the root bone for the avatar, usually the hips. If this is not set, it will be detected automatically.")] 44 | public Transform rootBone = null; 45 | [Tooltip("This list contains all the bones for which motion will be recorded. If nothing is assigned, it will be automatically generated when the script starts. When manually setting up an avatar the Unity Editor, you can press the corresponding button at the bottom of this component to automatically populate the list and add or remove bones manually if necessary.")] 46 | public List bones; 47 | 48 | [Header("Informational")] 49 | [Tooltip("This field shows how many frames are currently captured. Clearing the capture will reset this to 0.")] 50 | public int frameNumber = 0; 51 | [Tooltip("This field will be set to the filename written to by the saveBVH() function.")] 52 | public string lastSavedFile = ""; 53 | 54 | private Vector3 basePosition; 55 | private Vector3 offsetScale; 56 | private bool lowPrecision = false; 57 | private SkelTree skel = null; 58 | private List boneOrder = null; 59 | private string hierarchy; 60 | private float lastFrame; 61 | private bool first = false; 62 | private List frames = null; 63 | private Dictionary boneMap; 64 | 65 | 66 | class SkelTree { 67 | public String name; 68 | public Transform transform; 69 | public List children; 70 | 71 | public SkelTree(Transform bone, Dictionary boneMap) { 72 | name = bone.gameObject.name; 73 | if (boneMap != null) { 74 | if (boneMap.ContainsKey(bone)) { 75 | name = boneMap[bone]; 76 | } else if (boneMap.ContainsValue(name)) { 77 | name = name + "_"; 78 | } 79 | } 80 | transform = bone; 81 | children = new List(); 82 | } 83 | } 84 | 85 | public static void populateBoneMap(out Dictionary boneMap, Animator targetAvatar) { 86 | if (!targetAvatar.avatar.isHuman) { 87 | throw new InvalidOperationException("Enforce humanoid bones and rename bones can only be used with humanoid avatars."); 88 | } 89 | 90 | Dictionary usedNames = new Dictionary(); 91 | RuntimeAnimatorController rac = targetAvatar.runtimeAnimatorController; 92 | targetAvatar.runtimeAnimatorController = null; 93 | boneMap = new Dictionary(); 94 | HumanBodyBones[] bones = (HumanBodyBones[])Enum.GetValues(typeof(HumanBodyBones)); 95 | foreach (HumanBodyBones bone in bones) { 96 | if (bone < 0 || bone >= HumanBodyBones.LastBone) { 97 | continue; 98 | } 99 | Transform bodyBone = targetAvatar.GetBoneTransform(bone); 100 | if (bodyBone != null && bone != HumanBodyBones.LastBone) { 101 | if (usedNames.ContainsKey(bone.ToString())) { 102 | throw new InvalidOperationException("Multiple bones were assigned to the same standard bone name."); 103 | } else { 104 | boneMap.Add(bodyBone, bone.ToString()); 105 | usedNames.Add(bone.ToString(), 1); 106 | } 107 | } 108 | } 109 | targetAvatar.runtimeAnimatorController = rac; 110 | } 111 | 112 | public static Transform getRootBone(Animator avatar) { 113 | return getRootBone(avatar, null); 114 | } 115 | 116 | public static Transform getRootBone (Animator avatar, List bones) { 117 | List meshes = new List(avatar.GetComponents()); 118 | meshes.AddRange(avatar.GetComponentsInChildren(true)); 119 | 120 | Transform root = null; 121 | if (bones == null) { 122 | foreach (SkinnedMeshRenderer smr in meshes) { 123 | if (root == null && smr.bones.Length > 0) { 124 | root = smr.bones[0]; 125 | } 126 | foreach (Transform bone in smr.bones) { 127 | if (root.IsChildOf(bone) && bone != root) { 128 | root = bone; 129 | } 130 | } 131 | } 132 | } else { 133 | foreach (Transform bone in bones) { 134 | if (root == null) { 135 | root = bone; 136 | } 137 | if (root.IsChildOf(bone) && bone != root) { 138 | root = bone; 139 | } 140 | } 141 | } 142 | 143 | return root; 144 | } 145 | 146 | private void getTargetAvatar() { 147 | if (targetAvatar == null) { 148 | targetAvatar = GetComponent(); 149 | } 150 | if (targetAvatar == null) { 151 | throw new InvalidOperationException("No target avatar set."); 152 | } 153 | 154 | } 155 | 156 | // This function tries to find all Transforms that are bones of the character 157 | public void getBones() { 158 | getTargetAvatar(); 159 | 160 | if (enforceHumanoidBones) { 161 | populateBoneMap(out boneMap, targetAvatar); 162 | } 163 | 164 | List meshes = new List(targetAvatar.GetComponents()); 165 | meshes.AddRange(targetAvatar.GetComponentsInChildren(true)); 166 | 167 | HashSet boneSet = new HashSet(); 168 | 169 | foreach (SkinnedMeshRenderer smr in meshes) { 170 | foreach (Transform bone in smr.bones) { 171 | if (rootBone == null || (bone.IsChildOf(rootBone) && bone != rootBone)) { 172 | if (enforceHumanoidBones) { 173 | if (boneMap.ContainsKey(bone)) { 174 | boneSet.Add(bone); 175 | } 176 | } else { 177 | boneSet.Add(bone); 178 | } 179 | } 180 | } 181 | } 182 | 183 | bones = boneSet.OrderBy(bone => bone.name).ToList(); 184 | } 185 | 186 | // This function removes empty entries from the bone list, in case the user deleted some that were misdetected 187 | public void cleanupBones() { 188 | List clean = new List(); 189 | for (int i = 0; i < bones.Count; i++) { 190 | if (bones[i] != null) { 191 | clean.Add(bones[i]); 192 | } 193 | } 194 | bones = clean; 195 | } 196 | 197 | // This returns a queue of all child Transforms of a Transform 198 | private static Queue getChildren(Transform parent) { 199 | Queue children = new Queue(); 200 | for (int i = 0; i < parent.childCount; i++) { 201 | children.Enqueue(parent.GetChild(i)); 202 | } 203 | return children; 204 | } 205 | 206 | // This checks if any bones from the boneSet are below the given bone. If the bone is part of the boneSet it is removed from the set. 207 | private static bool hasBone(HashSet boneSet, Transform bone) { 208 | bool result = false; 209 | foreach (Transform other in boneSet) { 210 | if (bone == other) { 211 | boneSet.Remove(bone); 212 | return true; 213 | } else { 214 | if (other.IsChildOf(bone)) { 215 | result = true; 216 | } 217 | } 218 | } 219 | return result; 220 | } 221 | 222 | // This builds a minimal tree covering all detected bones that will be used to generate the hierarchy section of the BVH file 223 | public void buildSkeleton() { 224 | getTargetAvatar(); 225 | 226 | cleanupBones(); 227 | if (bones.Count == 0) { 228 | throw new InvalidOperationException("Target avatar, root bone and the bones list have to be set before calling buildSkeleton(). You can initialize bones list by calling getBones()."); 229 | } 230 | 231 | rootBone = getRootBone(targetAvatar, bones); 232 | if (rootBone == null) { 233 | throw new InvalidOperationException("No root bone found."); 234 | } 235 | 236 | if (enforceHumanoidBones) { 237 | populateBoneMap(out boneMap, targetAvatar); 238 | } else { 239 | boneMap = null; 240 | } 241 | basePosition = targetAvatar.transform.position; 242 | 243 | HashSet boneSet = new HashSet(bones); 244 | skel = new SkelTree(rootBone, boneMap); 245 | 246 | Queue queue = new Queue(); 247 | queue.Enqueue(skel); 248 | 249 | while (queue.Any()) { 250 | SkelTree bone = queue.Dequeue(); 251 | Queue children = getChildren(bone.transform); 252 | foreach (Transform child in children) { 253 | if (hasBone(boneSet, child)) { 254 | SkelTree childBone = new SkelTree(child, boneMap); 255 | queue.Enqueue(childBone); 256 | bone.children.Add(childBone); 257 | } 258 | } 259 | } 260 | } 261 | 262 | // This adds tabs according to the level of indentation 263 | private static string tabs(int n) { 264 | string tabs = ""; 265 | for (int i = 0; i < n; i++) { 266 | tabs += "\t"; 267 | } 268 | return tabs; 269 | } 270 | 271 | // This formats local translation vectors 272 | private string getOffset(Vector3 offset) { 273 | offset = Vector3.Scale(offset, offsetScale); 274 | Vector3 offset2 = new Vector3(-offset.x, offset.y, offset.z); 275 | if (blender) { 276 | offset2 = new Vector3(-offset.x, -offset.z, offset.y); 277 | } 278 | if (lowPrecision) { 279 | return string.Format(CultureInfo.InvariantCulture, "{0: 0.00;-0.00}\t{1: 0.00;-0.00}\t{2: 0.00;-0.00}", offset2.x, offset2.y, offset2.z); 280 | } else { 281 | return string.Format(CultureInfo.InvariantCulture, "{0: 0.000000;-0.000000}\t{1: 0.000000;-0.000000}\t{2: 0.000000;-0.000000}", offset2.x, offset2.y, offset2.z); 282 | } 283 | } 284 | 285 | // From: http://bediyap.com/programming/convert-quaternion-to-euler-rotations/ 286 | Vector3 manualEuler(float a, float b, float c, float d, float e) { 287 | Vector3 euler = new Vector3(); 288 | euler.z = Mathf.Atan2(a, b) * Mathf.Rad2Deg; // Z 289 | euler.x = Mathf.Asin(Mathf.Clamp(c, -1f, 1f)) * Mathf.Rad2Deg; // Y 290 | euler.y = Mathf.Atan2(d, e) * Mathf.Rad2Deg; // X 291 | return euler; 292 | } 293 | 294 | // Unity to BVH 295 | Vector3 eulerZXY(Vector4 q) { 296 | return manualEuler(-2 * (q.x * q.y - q.w * q.z), 297 | q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z, 298 | 2 * (q.y * q.z + q.w * q.x), 299 | -2 * (q.x * q.z - q.w * q.y), 300 | q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z); // ZXY 301 | } 302 | 303 | 304 | private string getRotation(Quaternion rot) { 305 | Vector4 rot2 = new Vector4(rot.x, -rot.y, -rot.z, rot.w).normalized; 306 | if (blender) { 307 | rot2 = new Vector4(rot.x, rot.z, -rot.y, rot.w).normalized; 308 | } 309 | Vector3 angles = eulerZXY(rot2); 310 | // This does convert to XZY order, but it should be ZXY? 311 | 312 | if (lowPrecision) { 313 | return string.Format(CultureInfo.InvariantCulture, "{0: 0.00;-0.00}\t{1: 0.00;-0.00}\t{2: 0.00;-0.00}", wrapAngle(angles.z), wrapAngle(angles.x), wrapAngle(angles.y)); 314 | } else { 315 | return string.Format(CultureInfo.InvariantCulture, "{0: 0.000000;-0.000000}\t{1: 0.000000;-0.000000}\t{2: 0.000000;-0.000000}", wrapAngle(angles.z), wrapAngle(angles.x), wrapAngle(angles.y)); 316 | } 317 | } 318 | 319 | // Angels should be -180 to 180 320 | private float wrapAngle(float a) { 321 | if (a > 180f) { 322 | return a - 360f; 323 | } 324 | if (a < -180f) { 325 | return 360f + a; 326 | } 327 | return a; 328 | } 329 | 330 | // This function recursively generates JOINT entries for the hierarchy section of the BVH file 331 | private string genJoint(int level, SkelTree bone) { 332 | Quaternion rot = bone.transform.localRotation; 333 | bone.transform.localRotation = Quaternion.identity; 334 | 335 | Vector3 offset = bone.transform.position - bone.transform.parent.position; 336 | string result = tabs(level) + "JOINT " + bone.name + "\n" + tabs(level) + "{\n" + tabs(level) + "\tOFFSET\t" + getOffset(offset) + "\n" + tabs(level) + "\tCHANNELS 3 Zrotation Xrotation Yrotation\n"; 337 | boneOrder.Add(bone); 338 | 339 | if (bone.children.Any()) { 340 | foreach (SkelTree child in bone.children) { 341 | result += genJoint(level + 1, child); 342 | } 343 | } else { 344 | // I don't really know what to put here. UniVRM's importer ignores this node type anyway. Blender doesn't and uses it for the bone tails. 345 | result += tabs(level + 1) + "End Site\n" + tabs(level + 1) + "{\n" + tabs(level + 1) + "\tOFFSET\t" + getOffset(bone.transform.position - bone.transform.parent.position) + "\n" + tabs(level + 1) + "}\n"; 346 | } 347 | 348 | result += tabs(level) + "}\n"; 349 | bone.transform.localRotation = rot; 350 | 351 | return result; 352 | } 353 | 354 | // This function generates the hierarchy section of the BVH file 355 | public void genHierarchy() { 356 | getTargetAvatar(); 357 | 358 | if (skel == null) { 359 | throw new InvalidOperationException("Skeleton not initialized. You can initialize the skeleton by calling buildSkeleton()."); 360 | } 361 | 362 | offsetScale = new Vector3(1f/targetAvatar.transform.localScale.x, 1f/targetAvatar.transform.localScale.y, 1f/targetAvatar.transform.localScale.z); 363 | 364 | Quaternion rot = skel.transform.rotation; 365 | skel.transform.rotation = Quaternion.identity; 366 | boneOrder = new List() { skel }; 367 | hierarchy = "HIERARCHY\nROOT " + skel.name + "\n{\n\tOFFSET\t0.00\t0.00\t0.00\n\tCHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n"; 368 | 369 | if (skel.children.Any()) { 370 | foreach (SkelTree child in skel.children) { 371 | hierarchy += genJoint(1, child); 372 | } 373 | } else { 374 | // I don't really know what to put here. UniVRM's importer ignores this node type anyway. Blender doesn't and uses it for the bone tails. 375 | hierarchy += "\tEnd Site\n\t{\n\t\tOFFSET\t1.0\t0.0\t0.0\n\t}\n"; 376 | } 377 | 378 | hierarchy += "}\n"; 379 | skel.transform.rotation = rot; 380 | 381 | frames = new List(); 382 | lastFrame = Time.time; 383 | first = true; 384 | } 385 | 386 | // This function stores the current frame's bone positions as a string 387 | public void captureFrame() { 388 | if (frames == null || hierarchy == "") { 389 | throw new InvalidOperationException("Hierarchy not initialized. You can initialize the hierarchy by calling genHierarchy()."); 390 | } 391 | 392 | StringBuilder sb = new StringBuilder(); 393 | sb.Append(getOffset(skel.transform.position - basePosition)); 394 | foreach (SkelTree bone in boneOrder) { 395 | sb.Append("\t"); 396 | if (bone == skel) { 397 | sb.Append(getRotation(bone.transform.rotation)); 398 | } else { 399 | sb.Append(getRotation(bone.transform.localRotation)); 400 | } 401 | } 402 | sb.Append("\n"); 403 | frames.Add(sb.ToString()); 404 | frameNumber++; 405 | } 406 | 407 | // Just what it says 408 | public void clearCapture() { 409 | frames.Clear(); 410 | frameNumber = 0; 411 | } 412 | 413 | // This file attaches frame data to the hierarchy section 414 | public string genBVH() { 415 | if (frames == null || hierarchy == "") { 416 | throw new InvalidOperationException("Hierarchy not initialized. You can initialize the hierarchy by calling genHierarchy()."); 417 | } 418 | 419 | StringBuilder bvh = new StringBuilder(); 420 | bvh.Append(hierarchy); 421 | bvh.Append("MOTION\nFrames: " + frames.Count + "\nFrame Time: " + string.Format(CultureInfo.InvariantCulture, "{0}", 1f / frameRate) + "\n"); 422 | 423 | foreach (string frame in frames) { 424 | bvh.Append(frame); 425 | } 426 | 427 | return bvh.ToString(); 428 | } 429 | 430 | public string uniquePath(string path) { 431 | string dir = Path.GetDirectoryName(path); 432 | string fileName = Path.GetFileNameWithoutExtension(path); 433 | string fileExt = Path.GetExtension(path); 434 | 435 | int i = 1; 436 | while (File.Exists(path)) { 437 | path = Path.Combine(dir, fileName + " (" + i++ + ")" + fileExt); 438 | } 439 | return path; 440 | } 441 | 442 | // This saves the full BVH file to the filename set in the component 443 | public void saveBVH() { 444 | if (frames == null || hierarchy == "") { 445 | throw new InvalidOperationException("Hierarchy not initialized. You can initialize the hierarchy by calling genHierarchy()."); 446 | } 447 | 448 | string outputFile = filename; 449 | if (outputFile == "") { 450 | // If no filename is set, make one up 451 | outputFile = "motion-" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".bvh"; 452 | } else { 453 | if (!outputFile.EndsWith(".bvh", true, CultureInfo.InvariantCulture)) { 454 | if (outputFile.EndsWith(".")) { 455 | outputFile = outputFile + "bvh"; 456 | } else { 457 | outputFile = outputFile + ".bvh"; 458 | } 459 | } 460 | } 461 | if (directory == "" && !(filename.Contains("/") || filename.Contains("\\"))) { 462 | directory = Application.persistentDataPath; 463 | } 464 | if (!overwrite) { 465 | outputFile = uniquePath(Path.Combine(directory, outputFile)); 466 | } 467 | File.WriteAllText(outputFile, genBVH()); 468 | lastSavedFile = outputFile; 469 | } 470 | 471 | void Start () { 472 | if (scripted) { 473 | return; 474 | } 475 | 476 | if (bones.Count == 0) { 477 | getBones(); 478 | } 479 | buildSkeleton(); 480 | genHierarchy(); 481 | } 482 | 483 | void LateUpdate () { 484 | if (frames == null || hierarchy == "" || !capturing) { 485 | lastFrame = Time.time; 486 | first = true; 487 | return; 488 | } 489 | if (first || lastFrame + 1f / frameRate <= Time.time) { 490 | if (catchUp) { 491 | lastFrame = lastFrame + 1f / frameRate; 492 | } else { 493 | lastFrame = Time.time; 494 | } 495 | captureFrame(); 496 | first = false; 497 | } 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /Assets/pose_unity/PosTxtReader.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using System.IO; 5 | using System; 6 | 7 | // pos.txtのデータ 8 | // https://github.com/miu200521358/3d-pose-baseline-vmd/blob/master/doc/Output.md 9 | // 0 :Hip 10 | // 1 :RHip 11 | // 2 :RKnee 12 | // 3 :RFoot 13 | // 4 :LHip 14 | // 5 :LKnee 15 | // 6 :LFoot 16 | // 7 :Spine 17 | // 8 :Thorax 18 | // 9 :Neck/Nose 19 | // 10:Head 20 | // 11:LShoulder 21 | // 12:LElbow 22 | // 13:LWrist 23 | // 14:RShoulder 24 | // 15:RElbow 25 | // 16:RWrist 26 | 27 | public class PosTxtReader : MonoBehaviour 28 | { 29 | public String posFilename; // pos.txtのファイル名 30 | 31 | // ---------------------------------------------- 32 | [Header("Option")] 33 | public int startFrame; // 開始フレーム 34 | public String endFrame; // 終了フレーム 35 | public int nowFrame_readonly; // 現在のフレーム (Read only) 36 | public float upPosition = 0.1f; // 足の沈みの補正値(単位:m)。プラス値で体全体が上へ移動する 37 | public Boolean showDebugCube; // デバッグ用Cubeの表示フラグ 38 | 39 | // ---------------------------------------------- 40 | [Header("Save Motion")] 41 | [Tooltip("When this flag is set, save motion at the end frame of Play.")] 42 | public Boolean saveMotion; // Playの最終フレーム時にモーションを保存する 43 | 44 | [Tooltip("This is the filename to which the BVH file will be saved. If no filename is given, a new one will be generated based on a timestamp. If the file already exists, a number will be appended.")] 45 | public String saveBVHFilename; // 保存ファイル名 46 | 47 | [Tooltip("When this flag is set, existing files will be overwritten and no number will be appended at the end to avoid this.")] 48 | public bool overwrite = false; // Falseの場合は、上書きせずにファイル名の末尾に数字を付加する。 49 | 50 | [Tooltip("When this option is enabled, only humanoid bones will be targeted for detecting bones. This means that things like hair bones will not be added to the list of bones when detecting bones.")] 51 | public bool enforceHumanoidBones = true; // 髪などの骨格以外のボーンは出力しない 52 | 53 | // ---------------------------------------------- 54 | float scaleRatio = 0.001f; // pos.txtとUnityモデルのスケール比率 55 | // pos.txtの単位はmmでUnityはmのため、0.001に近い値を指定。モデルの大きさによって調整する 56 | float headAngle = 15f; // 顔の向きの調整 顔を15度上げる 57 | // ---------------------------------------------- 58 | 59 | 60 | float playTime; // 再生時間 61 | int frame; // 再生フレーム 62 | Transform[] boneT; // モデルのボーンのTransform 63 | Transform[] cubeT; // デバック表示用のCubeのTransform 64 | Vector3 rootPosition; // 初期のAvatarの位置 65 | Quaternion rootRotation; // 初期のAvatarのの回転 66 | Quaternion[] initRot; // 初期の回転値 67 | Quaternion[] initInv; // 初期のボーンの方向から計算されるクオータニオンのInverse 68 | float hipHeight; // hipのposition.y 69 | List pos; // pos.txtのデータを保持するコンテナ 70 | BVHRecorder recorder; // BVH保存用コンポーネント 71 | int[] bones = new int[10] { 1, 2, 4, 5, 7, 8, 11, 12, 14, 15 }; // 親ボーン 72 | int[] childBones = new int[10] { 2, 3, 5, 6, 8, 10, 12, 13, 15, 16 }; // bonesに対応する子ボーン 73 | int boneNum = 17; 74 | Animator anim; 75 | int sFrame; 76 | int eFrame; 77 | bool bvhSaved = false; 78 | 79 | // pos.txtのデータを読み込み、リストで返す 80 | List ReadPosData(string filename) { 81 | List data = new List(); 82 | 83 | List lines = new List(); 84 | StreamReader sr = new StreamReader(filename); 85 | while (!sr.EndOfStream) { 86 | lines.Add(sr.ReadLine()); 87 | } 88 | sr.Close(); 89 | 90 | try { 91 | foreach (string line in lines) { 92 | string line2 = line.Replace(",", ""); 93 | string[] str = line2.Split(new string[] { " " }, System.StringSplitOptions.RemoveEmptyEntries); // スペースで分割し、空の文字列は削除 94 | 95 | Vector3[] vs = new Vector3[boneNum]; 96 | for (int i = 0; i < str.Length; i += 4) { 97 | vs[(int)(i/4)] = new Vector3(-float.Parse(str[i + 1]), float.Parse(str[i + 3]), -float.Parse(str[i + 2])); 98 | } 99 | data.Add(vs); 100 | } 101 | } 102 | catch (Exception e) { 103 | Debug.Log("Error! Pos File is broken(" + filename + ")."); 104 | return null; 105 | } 106 | return data; 107 | } 108 | 109 | // BoneTransformの取得。回転の初期値を取得 110 | void GetInitInfo() 111 | { 112 | boneT = new Transform[boneNum]; 113 | initRot = new Quaternion[boneNum]; 114 | initInv = new Quaternion[boneNum]; 115 | 116 | boneT[0] = anim.GetBoneTransform(HumanBodyBones.Hips); 117 | boneT[1] = anim.GetBoneTransform(HumanBodyBones.RightUpperLeg); 118 | boneT[2] = anim.GetBoneTransform(HumanBodyBones.RightLowerLeg); 119 | boneT[3] = anim.GetBoneTransform(HumanBodyBones.RightFoot); 120 | boneT[4] = anim.GetBoneTransform(HumanBodyBones.LeftUpperLeg); 121 | boneT[5] = anim.GetBoneTransform(HumanBodyBones.LeftLowerLeg); 122 | boneT[6] = anim.GetBoneTransform(HumanBodyBones.LeftFoot); 123 | boneT[7] = anim.GetBoneTransform(HumanBodyBones.Spine); 124 | boneT[8] = anim.GetBoneTransform(HumanBodyBones.Neck); 125 | boneT[10] = anim.GetBoneTransform(HumanBodyBones.Head); 126 | boneT[11] = anim.GetBoneTransform(HumanBodyBones.LeftUpperArm); 127 | boneT[12] = anim.GetBoneTransform(HumanBodyBones.LeftLowerArm); 128 | boneT[13] = anim.GetBoneTransform(HumanBodyBones.LeftHand); 129 | boneT[14] = anim.GetBoneTransform(HumanBodyBones.RightUpperArm); 130 | boneT[15] = anim.GetBoneTransform(HumanBodyBones.RightLowerArm); 131 | boneT[16] = anim.GetBoneTransform(HumanBodyBones.RightHand); 132 | 133 | if (boneT[0] == null) { 134 | Debug.Log("Error! Failed to get Bone Transform. Confirm wherther animation type of your model is Humanoid"); 135 | return; 136 | } 137 | 138 | // Spine,LHip,RHipで三角形を作ってそれを前方向とする。 139 | Vector3 initForward = TriangleNormal(boneT[7].position, boneT[4].position, boneT[1].position); 140 | initInv[0] = Quaternion.Inverse(Quaternion.LookRotation(initForward)); 141 | 142 | // initPosition = boneT[0].position; 143 | rootPosition = this.transform.position; 144 | rootRotation = this.transform.rotation; 145 | initRot[0] = boneT[0].rotation; 146 | hipHeight = boneT[0].position.y - this.transform.position.y; 147 | for (int i = 0; i < bones.Length; i++) { 148 | int b = bones[i]; 149 | int cb = childBones[i]; 150 | 151 | // 対象モデルの回転の初期値 152 | initRot[b] = boneT[b].rotation; 153 | // 初期のボーンの方向から計算されるクオータニオン 154 | initInv[b] = Quaternion.Inverse(Quaternion.LookRotation(boneT[b].position - boneT[cb].position,initForward)); 155 | } 156 | } 157 | 158 | // 指定の3点でできる三角形に直交する長さ1のベクトルを返す 159 | Vector3 TriangleNormal(Vector3 a, Vector3 b, Vector3 c) 160 | { 161 | Vector3 d1 = a - b; 162 | Vector3 d2 = a - c; 163 | 164 | Vector3 dd = Vector3.Cross(d1, d2); 165 | dd.Normalize(); 166 | 167 | return dd; 168 | } 169 | 170 | // デバック用cubeを生成する。生成済みの場合は位置を更新する 171 | void UpdateCube(int frame) 172 | { 173 | if (cubeT == null) { 174 | // 初期化して、cubeを生成する 175 | cubeT = new Transform[boneNum]; 176 | 177 | for (int i = 0; i < boneNum; i++) { 178 | Transform t = GameObject.CreatePrimitive(PrimitiveType.Cube).transform; 179 | t.transform.parent = this.transform; 180 | t.localPosition = pos[frame][i] * scaleRatio; 181 | t.name = i.ToString(); 182 | t.localScale = new Vector3(0.05f, 0.05f, 0.05f); 183 | cubeT[i] = t; 184 | 185 | Destroy(t.GetComponent()); 186 | } 187 | } 188 | else { 189 | // モデルと重ならないように少しずらして表示 190 | Vector3 offset = new Vector3(1.2f, 0, 0); 191 | 192 | // 初期化済みの場合は、cubeの位置を更新する 193 | for (int i = 0; i < boneNum; i++) { 194 | cubeT[i].localPosition = pos[frame][i] * scaleRatio + new Vector3(0, upPosition, 0) + offset; 195 | } 196 | } 197 | } 198 | 199 | void Start() 200 | { 201 | 202 | anim = GetComponent(); 203 | playTime = 0; 204 | if (posFilename == "") { 205 | Debug.Log("Error! Pos filename is empty."); 206 | return; 207 | } 208 | if (System.IO.File.Exists(posFilename) == false) { 209 | Debug.Log("Error! Pos file not found(" + posFilename + ")."); 210 | return; 211 | } 212 | pos = ReadPosData(posFilename); 213 | GetInitInfo(); 214 | if (pos != null) { 215 | // inspectorで指定した開始フレーム、終了フレーム番号をセット 216 | if (startFrame >= 0 && startFrame < pos.Count) { 217 | sFrame = startFrame; 218 | } else { 219 | sFrame = 0; 220 | } 221 | int ef; 222 | if (int.TryParse(endFrame, out ef)) { 223 | if (ef >= sFrame && ef < pos.Count) { 224 | eFrame = ef; 225 | } else { 226 | eFrame = pos.Count - 1; 227 | } 228 | } else { 229 | eFrame = pos.Count - 1; 230 | } 231 | frame = sFrame; 232 | } 233 | 234 | if (saveMotion) { 235 | recorder = gameObject.AddComponent(); 236 | recorder.scripted = true; 237 | recorder.targetAvatar = anim; 238 | recorder.blender = false; 239 | recorder.enforceHumanoidBones = enforceHumanoidBones; 240 | recorder.getBones(); 241 | recorder.buildSkeleton(); 242 | recorder.genHierarchy(); 243 | recorder.frameRate = 30.0f; 244 | } 245 | } 246 | 247 | void Update() 248 | { 249 | if (pos == null || boneT[0] == null) { 250 | return; 251 | } 252 | playTime += Time.deltaTime; 253 | 254 | if (saveMotion && recorder != null) { 255 | // ファイル出力の場合は1フレームずつ進める 256 | frame += 1; 257 | } else { 258 | frame = sFrame + (int)(playTime * 30.0f); // pos.txtは30fpsを想定 259 | } 260 | if (frame > eFrame) { 261 | if (saveMotion && recorder != null) { 262 | if (!bvhSaved) { 263 | bvhSaved = true; 264 | if (saveBVHFilename != "") { 265 | string fullpath = Path.GetFullPath(saveBVHFilename); 266 | // recorder.directory = Path.GetDirectoryName(fullpath); 267 | // recorder.filename = Path.GetFileName(fullpath); 268 | recorder.directory = ""; 269 | recorder.filename = fullpath; 270 | recorder.overwrite = overwrite; 271 | recorder.saveBVH(); 272 | Debug.Log("Saved Motion(BVH) to " + recorder.lastSavedFile); 273 | } else { 274 | Debug.Log("Error! Save BVH Filename is empty."); 275 | } 276 | } 277 | } 278 | return; 279 | } 280 | nowFrame_readonly = frame; // Inspector表示用 281 | 282 | if (showDebugCube) { 283 | UpdateCube(frame); // デバッグ用Cubeを表示する 284 | } 285 | 286 | Vector3[] nowPos = pos[frame]; 287 | 288 | // センターの移動と回転 289 | Vector3 posForward = TriangleNormal(nowPos[7], nowPos[4], nowPos[1]); 290 | 291 | this.transform.position = rootRotation * nowPos[0] * scaleRatio + rootPosition + new Vector3(0, upPosition - hipHeight , 0); 292 | boneT[0].rotation = rootRotation * Quaternion.LookRotation(posForward) * initInv[0] * initRot[0]; 293 | 294 | // 各ボーンの回転 295 | for (int i = 0; i < bones.Length; i++) { 296 | int b = bones[i]; 297 | int cb = childBones[i]; 298 | boneT[b].rotation = rootRotation * Quaternion.LookRotation(nowPos[b] - nowPos[cb], posForward) * initInv[b] * initRot[b]; 299 | } 300 | 301 | // 顔の向きを上げる調整。両肩を結ぶ線を軸として回転 302 | boneT[8].rotation = Quaternion.AngleAxis(headAngle, boneT[11].position - boneT[14].position) * boneT[8].rotation; 303 | 304 | if (saveMotion && recorder != null) { 305 | recorder.captureFrame(); 306 | } 307 | } 308 | } -------------------------------------------------------------------------------- /Assets/pose_unity/sample_description.txt: -------------------------------------------------------------------------------- 1 | [pos_sample1.txt] 2 | 3 | Traced from: 4 | Slow action. -Japanese businessman game style action 279- 5 | https://www.youtube.com/watch?v=zmPkkSItAUc 6 | 7 | 8 | [pos_sample2-1.txt, pos_sample2-2.txt] 9 | 10 | Traced from: 11 | Yakko and Mariyan Buster 12 | https://www.nicovideo.jp/watch/sm27620009 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 kenkra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/LICENSE_BVHRecorder.txt: -------------------------------------------------------------------------------- 1 | BVH Tools for Unity/VRM 2 | Copyright (c) 2019 Emiliana (twitter.com/Emiliana_vt) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity-3d-pose-baseline 2 | 3 | [miu200521358/3d-pose-baseline-vmd](https://github.com/miu200521358/3d-pose-baseline-vmd)で自動トレースした3Dポーズ情報を使用して、Unityでキャラクターを動かすためのプログラムです。 4 | 5 | 内容は[Qiita記事](https://qiita.com/kenkra/items/7b5634ff7f8c6bf0257a)を参照ください。 6 | 7 | ## 使用方法 8 | ### 1. 3Dポーズデータの作成 9 | 以下の@miu200521358様の記事の参考にして動画から3Dポーズデータ(pos.txt)を作成してください。 10 | 11 | クラウド(colab)でMMD自動トレース 12 | https://qiita.com/miu200521358/items/fb0a7bcf2764d7797e26 13 | 14 | ### 2. キャラクターを動かす 15 | 上記で作成されるpos.txt、PosTxtReader.cs、BVHRecorder.csを適当なフォルダに配置し、PosTxtReader.csをキャラクターにアタッチ後、Pos Filenameにpos.txtのパスを指定し、再生してください。 16 | 17 | ![md_main](https://user-images.githubusercontent.com/23007499/80491926-51e4ab00-899e-11ea-9ce1-527903d67839.png) 18 | 19 | ### appx. モーションをBVH形式で保存する 20 | Save Motionをチェックし、保存先をSave BVH Filenameに指定し(例:D:\pose\motion1.bvh)、再生してください。再生が終わるとファイルが出力されます。bvh形式のファイルはBlenderなどのアプリケーションで読み込むことができます。 21 | --------------------------------------------------------------------------------