├── .gitignore ├── EasyMotionRecorder └── Assets │ ├── EasyMotionRecorder.meta │ └── EasyMotionRecorder │ ├── Prefabs.meta │ ├── Prefabs │ ├── EasyMotionRecorder.prefab │ └── EasyMotionRecorder.prefab.meta │ ├── Scripts.meta │ └── Scripts │ ├── CharacterFacialData.cs │ ├── CharacterFacialData.cs.meta │ ├── FaceAnimationRecorder.cs │ ├── FaceAnimationRecorder.cs.meta │ ├── ForRuntime.meta │ ├── ForRuntime │ ├── MotionDataPlayerCSV.cs │ ├── MotionDataPlayerCSV.cs.meta │ ├── MotionDataRecorderCSV.cs │ └── MotionDataRecorderCSV.cs.meta │ ├── HumanoidPoses.cs │ ├── HumanoidPoses.cs.meta │ ├── MotionDataPlayer.cs │ ├── MotionDataPlayer.cs.meta │ ├── MotionDataRecorder.cs │ └── MotionDataRecorder.cs.meta ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/unity,macos,windows 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Unity ### 33 | [Ll]ibrary/ 34 | [Tt]emp/ 35 | [Oo]bj/ 36 | [Bb]uild/ 37 | [Bb]uilds/ 38 | Assets/AssetStoreTools* 39 | 40 | # Visual Studio cache directory 41 | .vs/ 42 | 43 | # Autogenerated VS/MD/Consulo solution and project files 44 | ExportedObj/ 45 | .consulo/ 46 | *.csproj 47 | *.unityproj 48 | *.sln 49 | *.suo 50 | *.tmp 51 | *.user 52 | *.userprefs 53 | *.pidb 54 | *.booproj 55 | *.svd 56 | *.pdb 57 | *.opendb 58 | 59 | # Unity3D generated meta files 60 | *.pidb.meta 61 | *.pdb.meta 62 | 63 | # Unity3D Generated File On Crash Reports 64 | sysinfo.txt 65 | 66 | # Builds 67 | *.apk 68 | *.unitypackage 69 | 70 | ### Windows ### 71 | # Windows thumbnail cache files 72 | Thumbs.db 73 | ehthumbs.db 74 | ehthumbs_vista.db 75 | 76 | # Dump file 77 | *.stackdump 78 | 79 | # Folder config file 80 | [Dd]esktop.ini 81 | 82 | # Recycle Bin used on file shares 83 | $RECYCLE.BIN/ 84 | 85 | # Windows Installer files 86 | *.cab 87 | *.msi 88 | *.msix 89 | *.msm 90 | *.msp 91 | 92 | # Windows shortcuts 93 | *.lnk 94 | 95 | 96 | # End of https://www.gitignore.io/api/unity,macos,windows -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9b035c72dbaa4874d996a552d2768674 3 | folderAsset: yes 4 | timeCreated: 1523288373 5 | licenseType: Pro 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Prefabs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c16b8f8f7722f864f8b30a5e34a22af6 3 | folderAsset: yes 4 | timeCreated: 1523290582 5 | licenseType: Pro 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Prefabs/EasyMotionRecorder.prefab: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!1001 &100100000 4 | Prefab: 5 | m_ObjectHideFlags: 1 6 | serializedVersion: 2 7 | m_Modification: 8 | m_TransformParent: {fileID: 0} 9 | m_Modifications: [] 10 | m_RemovedComponents: [] 11 | m_ParentPrefab: {fileID: 0} 12 | m_RootGameObject: {fileID: 1076081908508874} 13 | m_IsPrefabParent: 1 14 | --- !u!1 &1076081908508874 15 | GameObject: 16 | m_ObjectHideFlags: 0 17 | m_PrefabParentObject: {fileID: 0} 18 | m_PrefabInternal: {fileID: 100100000} 19 | serializedVersion: 5 20 | m_Component: 21 | - component: {fileID: 4734933881020700} 22 | - component: {fileID: 114014463455714378} 23 | - component: {fileID: 114494349119178576} 24 | - component: {fileID: 114854222126709126} 25 | m_Layer: 0 26 | m_Name: EasyMotionRecorder 27 | m_TagString: Untagged 28 | m_Icon: {fileID: 0} 29 | m_NavMeshLayer: 0 30 | m_StaticEditorFlags: 0 31 | m_IsActive: 1 32 | --- !u!4 &4734933881020700 33 | Transform: 34 | m_ObjectHideFlags: 1 35 | m_PrefabParentObject: {fileID: 0} 36 | m_PrefabInternal: {fileID: 100100000} 37 | m_GameObject: {fileID: 1076081908508874} 38 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 39 | m_LocalPosition: {x: 0, y: 0, z: 0} 40 | m_LocalScale: {x: 1, y: 1, z: 1} 41 | m_Children: [] 42 | m_Father: {fileID: 0} 43 | m_RootOrder: 0 44 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 45 | --- !u!114 &114014463455714378 46 | MonoBehaviour: 47 | m_ObjectHideFlags: 1 48 | m_PrefabParentObject: {fileID: 0} 49 | m_PrefabInternal: {fileID: 100100000} 50 | m_GameObject: {fileID: 1076081908508874} 51 | m_Enabled: 1 52 | m_EditorHideFlags: 0 53 | m_Script: {fileID: 11500000, guid: 6a94616cf260a0e4386d211583fd6681, type: 3} 54 | m_Name: 55 | m_EditorClassIdentifier: 56 | _playStartKey: 115 57 | _playStopKey: 116 58 | RecordedMotionData: {fileID: 0} 59 | _animator: {fileID: 0} 60 | _startFrame: 0 61 | _playing: 0 62 | _frameIndex: 0 63 | _rootBoneSystem: 1 64 | _targetRootBone: 0 65 | --- !u!114 &114494349119178576 66 | MonoBehaviour: 67 | m_ObjectHideFlags: 1 68 | m_PrefabParentObject: {fileID: 0} 69 | m_PrefabInternal: {fileID: 100100000} 70 | m_GameObject: {fileID: 1076081908508874} 71 | m_Enabled: 1 72 | m_EditorHideFlags: 0 73 | m_Script: {fileID: 11500000, guid: 171f7816f5da7ba4eab3cf34b9955f13, type: 3} 74 | m_Name: 75 | m_EditorClassIdentifier: 76 | _recordStartKey: 114 77 | _recordStopKey: 120 78 | _animator: {fileID: 0} 79 | _recording: 0 80 | FrameIndex: 0 81 | _rootBoneSystem: 1 82 | _targetRootBone: 0 83 | --- !u!114 &114854222126709126 84 | MonoBehaviour: 85 | m_ObjectHideFlags: 1 86 | m_PrefabParentObject: {fileID: 0} 87 | m_PrefabInternal: {fileID: 100100000} 88 | m_GameObject: {fileID: 1076081908508874} 89 | m_Enabled: 1 90 | m_EditorHideFlags: 0 91 | m_Script: {fileID: 11500000, guid: 79e7c07a2aa3ba848ab52ca6ed2e213b, type: 3} 92 | m_Name: 93 | m_EditorClassIdentifier: 94 | _recordFaceBlendshapes: 0 95 | _exclusiveBlendshapeNames: [] 96 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Prefabs/EasyMotionRecorder.prefab.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 26de42e5745b2b547b1714ecb3625597 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 100100000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 345e046f8f9faaf4cb9145dfd9d56456 3 | folderAsset: yes 4 | timeCreated: 1523288379 5 | licenseType: Pro 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/CharacterFacialData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace Entum 7 | { 8 | public class CharacterFacialData : ScriptableObject 9 | { 10 | 11 | [System.SerializableAttribute] 12 | public class SerializeHumanoidFace 13 | { 14 | public class MeshAndBlendshape 15 | { 16 | public string path; 17 | public float[] blendShapes; 18 | } 19 | 20 | 21 | public int BlendShapeNum() 22 | { 23 | return Smeshes.Count == 0 ? 0 : Smeshes.Sum(t => t.blendShapes.Length); 24 | } 25 | 26 | //フレーム数 27 | public int FrameCount; 28 | 29 | //記録開始後の経過時間。処理落ち対策 30 | public float Time; 31 | 32 | public SerializeHumanoidFace(SerializeHumanoidFace serializeHumanoidFace) 33 | { 34 | for (int i = 0; i < serializeHumanoidFace.Smeshes.Count; i++) 35 | { 36 | Smeshes.Add(serializeHumanoidFace.Smeshes[i]); 37 | Array.Copy(serializeHumanoidFace.Smeshes[i].blendShapes,Smeshes[i].blendShapes, 38 | serializeHumanoidFace.Smeshes[i].blendShapes.Length); 39 | 40 | } 41 | FrameCount = serializeHumanoidFace.FrameCount; 42 | Time = serializeHumanoidFace.Time; 43 | } 44 | //単一フレームの中でも、口のメッシュや目のメッシュなどが個別にここに入る 45 | public List Smeshes= new List(); 46 | public SerializeHumanoidFace() 47 | { 48 | } 49 | } 50 | 51 | 52 | public List Facials = new List(); 53 | } 54 | } -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/CharacterFacialData.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9ee0d6b61543b364392a06bf7f7d0e32 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using UnityEditor; 7 | using UnityEngine; 8 | 9 | /** 10 | [EasyMotionRecorder] 11 | 12 | Copyright (c) 2018 Duo.inc 13 | 14 | This software is released under the MIT License. 15 | http://opensource.org/licenses/mit-license.php 16 | */ 17 | 18 | namespace Entum 19 | { 20 | /// 21 | /// Blendshapeの動きを記録するクラス 22 | /// リップシンクは後入れでTimeline上にAudioClipをつけて、みたいな可能性が高いので 23 | /// Exclusive(除外)するBlendshape名を登録できるようにしています。 24 | /// 25 | [RequireComponent(typeof(MotionDataRecorder))] 26 | public class FaceAnimationRecorder : MonoBehaviour 27 | { 28 | [Header("表情記録を同時に行う場合はtrueにします")] [SerializeField] 29 | private bool _recordFaceBlendshapes = false; 30 | 31 | [Header("リップシンクを記録したくない場合はここにモーフ名を入れていく 例:face_mouse_eなど")] [SerializeField] 32 | private List _exclusiveBlendshapeNames; 33 | 34 | [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] 35 | public float TargetFPS = 60.0f; 36 | 37 | private MotionDataRecorder _animRecorder; 38 | 39 | 40 | private SkinnedMeshRenderer[] _smeshs; 41 | 42 | private CharacterFacialData _facialData = null; 43 | 44 | private bool _recording = false; 45 | 46 | private int _frameCount = 0; 47 | 48 | 49 | CharacterFacialData.SerializeHumanoidFace _past = new CharacterFacialData.SerializeHumanoidFace(); 50 | 51 | private float _recordedTime = 0f; 52 | private float _startTime; 53 | 54 | // Use this for initialization 55 | private void OnEnable() 56 | { 57 | _animRecorder = GetComponent(); 58 | _animRecorder.OnRecordStart += RecordStart; 59 | _animRecorder.OnRecordEnd += RecordEnd; 60 | if (_animRecorder.CharacterAnimator != null) 61 | { 62 | _smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator); 63 | } 64 | } 65 | 66 | SkinnedMeshRenderer[] GetSkinnedMeshRenderers(Animator root) 67 | { 68 | var helper = root; 69 | var renderers = helper.GetComponentsInChildren(); 70 | List smeshList = new List(); 71 | for (int i = 0; i < renderers.Length; i++) 72 | { 73 | var rend = renderers[i]; 74 | var cnt = rend.sharedMesh.blendShapeCount; 75 | if (cnt > 0) 76 | { 77 | smeshList.Add(rend); 78 | } 79 | } 80 | 81 | return smeshList.ToArray(); 82 | } 83 | 84 | private void OnDisable() 85 | { 86 | if (_recording) 87 | { 88 | RecordEnd(); 89 | _recording = false; 90 | } 91 | 92 | if (_animRecorder == null) return; 93 | _animRecorder.OnRecordStart -= RecordStart; 94 | _animRecorder.OnRecordEnd -= RecordEnd; 95 | } 96 | 97 | /// 98 | /// 記録開始 99 | /// 100 | private void RecordStart() 101 | { 102 | if (_recordFaceBlendshapes == false) 103 | { 104 | return; 105 | } 106 | 107 | if (_recording) 108 | { 109 | return; 110 | } 111 | 112 | if (_smeshs.Length == 0) 113 | { 114 | Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しません"); 115 | return; 116 | } 117 | 118 | Debug.Log("FaceAnimationRecorder record start"); 119 | _recording = true; 120 | _recordedTime = 0f; 121 | _startTime = Time.time; 122 | _frameCount = 0; 123 | _facialData = ScriptableObject.CreateInstance(); 124 | } 125 | 126 | /// 127 | /// 記録終了 128 | /// 129 | private void RecordEnd() 130 | { 131 | if (_recordFaceBlendshapes == false) 132 | { 133 | return; 134 | } 135 | 136 | if (_smeshs.Length == 0) 137 | { 138 | Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しませんでした"); 139 | if (_recording == true) 140 | { 141 | Debug.LogAssertion("Unexpected execution!!!!"); 142 | } 143 | } 144 | else 145 | { 146 | //WriteAnimationFileToScriptableObject(); 147 | ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData); 148 | } 149 | 150 | Debug.Log("FaceAnimationRecorder record end"); 151 | 152 | _recording = false; 153 | } 154 | 155 | 156 | private void WriteAnimationFileToScriptableObject() 157 | { 158 | MotionDataRecorder.SafeCreateDirectory("Assets/Resources"); 159 | 160 | string path = AssetDatabase.GenerateUniqueAssetPath( 161 | "Assets/Resources/RecordMotion_ face" + _animRecorder.CharacterAnimator.name + 162 | DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") + 163 | ".asset"); 164 | 165 | if (_facialData == null) 166 | { 167 | Debug.LogError("記録されたFaceデータがnull"); 168 | } 169 | else 170 | { 171 | AssetDatabase.CreateAsset(_facialData, path); 172 | AssetDatabase.Refresh(); 173 | } 174 | _startTime = Time.time; 175 | _recordedTime = 0f; 176 | _frameCount = 0; 177 | } 178 | 179 | //フレーム内の差分が無いかをチェックするやつ。 180 | private bool IsSame(CharacterFacialData.SerializeHumanoidFace a, CharacterFacialData.SerializeHumanoidFace b) 181 | { 182 | if (a == null || b == null || a.Smeshes.Count == 0 || b.Smeshes.Count == 0) 183 | { 184 | return false; 185 | } 186 | 187 | if (a.BlendShapeNum() != b.BlendShapeNum()) 188 | { 189 | return false; 190 | } 191 | 192 | return !a.Smeshes.Where((t1, i) => 193 | t1.blendShapes.Where((t, j) => Mathf.Abs(t - b.Smeshes[i].blendShapes[j]) > 1).Any()).Any(); 194 | } 195 | 196 | private void LateUpdate() 197 | { 198 | if (Input.GetKeyDown(KeyCode.Y)) 199 | { 200 | ExportFacialAnimationClipTest(); 201 | } 202 | 203 | if (!_recording) 204 | { 205 | return; 206 | } 207 | 208 | _recordedTime = Time.time - _startTime; 209 | 210 | if (TargetFPS != 0.0f) 211 | { 212 | var nextTime = (1.0f * (_frameCount + 1)) / TargetFPS; 213 | if (nextTime > _recordedTime) 214 | { 215 | return; 216 | } 217 | if (_frameCount % TargetFPS == 0) 218 | { 219 | print("Face_FPS=" + 1 / (_recordedTime / _frameCount)); 220 | } 221 | } 222 | else 223 | { 224 | if (Time.frameCount % Application.targetFrameRate == 0) 225 | { 226 | print("Face_FPS=" + 1 / Time.deltaTime); 227 | } 228 | } 229 | 230 | 231 | var p = new CharacterFacialData.SerializeHumanoidFace(); 232 | for (int i = 0; i < _smeshs.Length; i++) 233 | { 234 | var mesh = new CharacterFacialData.SerializeHumanoidFace.MeshAndBlendshape(); 235 | mesh.path = _smeshs[i].name; 236 | mesh.blendShapes = new float[_smeshs[i].sharedMesh.blendShapeCount]; 237 | 238 | for (int j = 0; j < _smeshs[i].sharedMesh.blendShapeCount; j++) 239 | { 240 | var tname = _smeshs[i].sharedMesh.GetBlendShapeName(j); 241 | 242 | var useThis = true; 243 | 244 | foreach (var item in _exclusiveBlendshapeNames) 245 | { 246 | if (item.IndexOf(tname, StringComparison.Ordinal) >= 0) 247 | { 248 | useThis = false; 249 | } 250 | } 251 | 252 | 253 | if (useThis) 254 | { 255 | mesh.blendShapes[j] = _smeshs[i].GetBlendShapeWeight(j); 256 | } 257 | } 258 | 259 | p.Smeshes.Add(mesh); 260 | } 261 | 262 | if (!IsSame(p, _past)) 263 | { 264 | p.FrameCount = _frameCount; 265 | p.Time = _recordedTime; 266 | 267 | _facialData.Facials.Add(p); 268 | _past = new CharacterFacialData.SerializeHumanoidFace(p); 269 | } 270 | 271 | _frameCount++; 272 | } 273 | 274 | 275 | /// 276 | /// Animatorと記録したデータで書き込む 277 | /// 278 | /// 279 | /// 280 | void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) 281 | { 282 | var animclip = new AnimationClip(); 283 | 284 | var mesh = _smeshs; 285 | 286 | for (int faceTargetMeshIndex = 0; faceTargetMeshIndex < mesh.Length; faceTargetMeshIndex++) 287 | { 288 | var pathsb = new StringBuilder().Append(mesh[faceTargetMeshIndex].transform.name); 289 | var trans = mesh[faceTargetMeshIndex].transform; 290 | while (trans.parent != null && trans.parent != root.transform) 291 | { 292 | trans = trans.parent; 293 | pathsb.Insert(0, "/").Insert(0, trans.name); 294 | } 295 | 296 | //pathにはBlendshapeのベース名が入る 297 | //U_CHAR_1:SkinnedMeshRendererみたいなもの 298 | var path = pathsb.ToString(); 299 | 300 | //個別メッシュの個別Blendshapeごとに、AnimationCurveを生成している 301 | for (var blendShapeIndex = 0; 302 | blendShapeIndex < mesh[faceTargetMeshIndex].sharedMesh.blendShapeCount; 303 | blendShapeIndex++) 304 | { 305 | var curveBinding = new EditorCurveBinding(); 306 | curveBinding.type = typeof(SkinnedMeshRenderer); 307 | curveBinding.path = path; 308 | curveBinding.propertyName = "blendShape." + 309 | mesh[faceTargetMeshIndex].sharedMesh.GetBlendShapeName(blendShapeIndex); 310 | AnimationCurve curve = new AnimationCurve(); 311 | 312 | float pastBlendshapeWeight = -1; 313 | for (int k = 0; k < _facialData.Facials.Count; k++) 314 | { 315 | if (!(Mathf.Abs(pastBlendshapeWeight - _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex]) > 316 | 0.1f)) continue; 317 | curve.AddKey(new Keyframe(facial.Facials[k].Time, _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex], float.PositiveInfinity, 0f)); 318 | pastBlendshapeWeight = _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex]; 319 | } 320 | 321 | 322 | AnimationUtility.SetEditorCurve(animclip, curveBinding, curve); 323 | } 324 | } 325 | 326 | MotionDataRecorder.SafeCreateDirectory("Assets/Resources"); 327 | 328 | var outputPath = "Assets/Resources/FaceRecordMotion_" + _animRecorder.CharacterAnimator.name + "_" + 329 | DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") + "_Clip.anim"; 330 | 331 | Debug.Log("outputPath:" + outputPath); 332 | AssetDatabase.CreateAsset(animclip, 333 | AssetDatabase.GenerateUniqueAssetPath(outputPath)); 334 | AssetDatabase.SaveAssets(); 335 | AssetDatabase.Refresh(); 336 | } 337 | 338 | /// 339 | /// Animatorと記録したデータで書き込むテスト 340 | /// 341 | /// 342 | /// 343 | void ExportFacialAnimationClipTest() 344 | { 345 | var animclip = new AnimationClip(); 346 | 347 | var mesh = _smeshs; 348 | 349 | for (int i = 0; i < mesh.Length; i++) 350 | { 351 | var pathsb = new StringBuilder().Append(mesh[i].transform.name); 352 | var trans = mesh[i].transform; 353 | while (trans.parent != null && trans.parent != _animRecorder.CharacterAnimator.transform) 354 | { 355 | trans = trans.parent; 356 | pathsb.Insert(0, "/").Insert(0, trans.name); 357 | } 358 | 359 | var path = pathsb.ToString(); 360 | 361 | for (var j = 0; j < mesh[i].sharedMesh.blendShapeCount; j++) 362 | { 363 | var curveBinding = new EditorCurveBinding(); 364 | curveBinding.type = typeof(SkinnedMeshRenderer); 365 | curveBinding.path = path; 366 | curveBinding.propertyName = "blendShape." + mesh[i].sharedMesh.GetBlendShapeName(j); 367 | AnimationCurve curve = new AnimationCurve(); 368 | 369 | 370 | //全てのBlendshapeに対して0→100→0の遷移でキーを打つ 371 | curve.AddKey(0, 0); 372 | curve.AddKey(1, 100); 373 | curve.AddKey(2, 0); 374 | 375 | Debug.Log("path: " + curveBinding.path + "\r\nname: " + curveBinding.propertyName + " val:"); 376 | 377 | AnimationUtility.SetEditorCurve(animclip, curveBinding, curve); 378 | } 379 | } 380 | 381 | AssetDatabase.CreateAsset(animclip, 382 | AssetDatabase.GenerateUniqueAssetPath("Assets/" + _animRecorder.CharacterAnimator.name + 383 | "_facial_ClipTest.anim")); 384 | AssetDatabase.SaveAssets(); 385 | AssetDatabase.Refresh(); 386 | } 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 79e7c07a2aa3ba848ab52ca6ed2e213b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/ForRuntime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f12525cb1570a5d49875b582348ed4a4 3 | folderAsset: yes 4 | timeCreated: 1527228925 5 | licenseType: Free 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/ForRuntime/MotionDataPlayerCSV.cs: -------------------------------------------------------------------------------- 1 | /** 2 | [EasyMotionRecorder] 3 | 4 | Copyright (c) 2018 Duo.inc 5 | 6 | This software is released under the MIT License. 7 | http://opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | using UnityEngine; 11 | using System.IO; 12 | 13 | namespace Entum 14 | { 15 | /// 16 | /// CSVに吐かれたモーションデータを再生する 17 | /// 18 | public class MotionDataPlayerCSV : MotionDataPlayer 19 | { 20 | [SerializeField, Tooltip("スラッシュで終わる形で")] 21 | private string _recordedDirectory; 22 | 23 | [SerializeField, Tooltip("拡張子も")] 24 | private string _recordedFileName; 25 | 26 | // Use this for initialization 27 | private void Start() 28 | { 29 | if (string.IsNullOrEmpty(_recordedDirectory)) 30 | { 31 | _recordedDirectory = Application.streamingAssetsPath + "/"; 32 | } 33 | 34 | string motionCSVPath = _recordedDirectory + _recordedFileName; 35 | LoadCSVData(motionCSVPath); 36 | } 37 | 38 | //CSVから_recordedMotionDataを作る 39 | private void LoadCSVData(string motionDataPath) 40 | { 41 | //ファイルが存在しなければ終了 42 | if (!File.Exists(motionDataPath)) 43 | { 44 | return; 45 | } 46 | 47 | 48 | RecordedMotionData = ScriptableObject.CreateInstance(); 49 | 50 | FileStream fs = null; 51 | StreamReader sr = null; 52 | 53 | //ファイル読み込み 54 | try 55 | { 56 | fs = new FileStream(motionDataPath, FileMode.Open); 57 | sr = new StreamReader(fs); 58 | 59 | while (sr.Peek() > -1) 60 | { 61 | string line = sr.ReadLine(); 62 | var seriHumanPose = new HumanoidPoses.SerializeHumanoidPose(); 63 | if (line != "") 64 | { 65 | seriHumanPose.DeserializeCSV(line); 66 | RecordedMotionData.Poses.Add(seriHumanPose); 67 | } 68 | } 69 | sr.Close(); 70 | fs.Close(); 71 | sr = null; 72 | fs = null; 73 | } 74 | catch (System.Exception e) 75 | { 76 | Debug.LogError("ファイル読み込み失敗!" + e.Message + e.StackTrace); 77 | } 78 | 79 | if (sr != null) 80 | { 81 | sr.Close(); 82 | } 83 | 84 | if (fs != null) 85 | { 86 | fs.Close(); 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/ForRuntime/MotionDataPlayerCSV.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 97312877a935a97408e5c1e4895d95e0 3 | timeCreated: 1527233531 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/ForRuntime/MotionDataRecorderCSV.cs: -------------------------------------------------------------------------------- 1 | /** 2 | [EasyMotionRecorder] 3 | 4 | Copyright (c) 2018 Duo.inc 5 | 6 | This software is released under the MIT License. 7 | http://opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | using UnityEngine; 11 | using System; 12 | using System.IO; 13 | 14 | namespace Entum 15 | { 16 | /// 17 | /// モーションデータをCSVに記録するクラス 18 | /// ランタイムでも記録できる 19 | /// 20 | [DefaultExecutionOrder(31000)] 21 | public class MotionDataRecorderCSV : MotionDataRecorder 22 | { 23 | [SerializeField, Tooltip("スラッシュで終わる形で")] 24 | private string _outputDirectory; 25 | 26 | [SerializeField, Tooltip("拡張子も")] 27 | private string _outputFileName; 28 | 29 | protected override void WriteAnimationFile() 30 | { 31 | //ファイルオープン 32 | string directoryStr = _outputDirectory; 33 | if (directoryStr == "") 34 | { 35 | //自動設定ディレクトリ 36 | directoryStr = Application.streamingAssetsPath + "/"; 37 | 38 | if (!Directory.Exists(directoryStr)) 39 | { 40 | Directory.CreateDirectory(directoryStr); 41 | } 42 | } 43 | 44 | string fileNameStr = _outputFileName; 45 | if (fileNameStr == "") 46 | { 47 | //自動設定ファイル名 48 | fileNameStr = string.Format("motion_{0:yyyy_MM_dd_HH_mm_ss}.csv", DateTime.Now); 49 | } 50 | 51 | FileStream fs = new FileStream(directoryStr + fileNameStr, FileMode.Create); 52 | StreamWriter sw = new StreamWriter(fs); 53 | 54 | foreach (var pose in Poses.Poses) 55 | { 56 | string seriStr = pose.SerializeCSV(); 57 | sw.WriteLine(seriStr); 58 | } 59 | 60 | //ファイルクローズ 61 | try 62 | { 63 | sw.Close(); 64 | fs.Close(); 65 | sw = null; 66 | fs = null; 67 | } 68 | catch (Exception e) 69 | { 70 | Debug.LogError("ファイル書き出し失敗!" + e.Message + e.StackTrace); 71 | } 72 | 73 | if (sw != null) 74 | { 75 | sw.Close(); 76 | } 77 | 78 | if (fs != null) 79 | { 80 | fs.Close(); 81 | } 82 | 83 | #if UNITY_EDITOR 84 | UnityEditor.AssetDatabase.Refresh(); 85 | #endif 86 | 87 | RecordedTime = 0f; 88 | StartTime = Time.time; 89 | FrameIndex = 0; 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/ForRuntime/MotionDataRecorderCSV.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2f22fa5e3058f6342986e9f24598e16d 3 | timeCreated: 1527228942 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/HumanoidPoses.cs: -------------------------------------------------------------------------------- 1 | /** 2 | [EasyMotionRecorder] 3 | 4 | Copyright (c) 2018 Duo.inc 5 | 6 | This software is released under the MIT License. 7 | http://opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | using UnityEngine; 11 | using System; 12 | using System.Text; 13 | using System.Collections.Generic; 14 | #if UNITY_EDITOR 15 | using UnityEditor; 16 | #endif 17 | 18 | namespace Entum 19 | { 20 | [Serializable] 21 | public class MotionDataSettings 22 | { 23 | public enum Rootbonesystem 24 | { 25 | Hipbone, 26 | Objectroot 27 | } 28 | 29 | /// 30 | /// Humanoid用のMuscleマッピング 31 | /// 32 | public static Dictionary TraitPropMap = new Dictionary 33 | { 34 | {"Left Thumb 1 Stretched", "LeftHand.Thumb.1 Stretched"}, 35 | {"Left Thumb Spread", "LeftHand.Thumb.Spread"}, 36 | {"Left Thumb 2 Stretched", "LeftHand.Thumb.2 Stretched"}, 37 | {"Left Thumb 3 Stretched", "LeftHand.Thumb.3 Stretched"}, 38 | {"Left Index 1 Stretched", "LeftHand.Index.1 Stretched"}, 39 | {"Left Index Spread", "LeftHand.Index.Spread"}, 40 | {"Left Index 2 Stretched", "LeftHand.Index.2 Stretched"}, 41 | {"Left Index 3 Stretched", "LeftHand.Index.3 Stretched"}, 42 | {"Left Middle 1 Stretched", "LeftHand.Middle.1 Stretched"}, 43 | {"Left Middle Spread", "LeftHand.Middle.Spread"}, 44 | {"Left Middle 2 Stretched", "LeftHand.Middle.2 Stretched"}, 45 | {"Left Middle 3 Stretched", "LeftHand.Middle.3 Stretched"}, 46 | {"Left Ring 1 Stretched", "LeftHand.Ring.1 Stretched"}, 47 | {"Left Ring Spread", "LeftHand.Ring.Spread"}, 48 | {"Left Ring 2 Stretched", "LeftHand.Ring.2 Stretched"}, 49 | {"Left Ring 3 Stretched", "LeftHand.Ring.3 Stretched"}, 50 | {"Left Little 1 Stretched", "LeftHand.Little.1 Stretched"}, 51 | {"Left Little Spread", "LeftHand.Little.Spread"}, 52 | {"Left Little 2 Stretched", "LeftHand.Little.2 Stretched"}, 53 | {"Left Little 3 Stretched", "LeftHand.Little.3 Stretched"}, 54 | {"Right Thumb 1 Stretched", "RightHand.Thumb.1 Stretched"}, 55 | {"Right Thumb Spread", "RightHand.Thumb.Spread"}, 56 | {"Right Thumb 2 Stretched", "RightHand.Thumb.2 Stretched"}, 57 | {"Right Thumb 3 Stretched", "RightHand.Thumb.3 Stretched"}, 58 | {"Right Index 1 Stretched", "RightHand.Index.1 Stretched"}, 59 | {"Right Index Spread", "RightHand.Index.Spread"}, 60 | {"Right Index 2 Stretched", "RightHand.Index.2 Stretched"}, 61 | {"Right Index 3 Stretched", "RightHand.Index.3 Stretched"}, 62 | {"Right Middle 1 Stretched", "RightHand.Middle.1 Stretched"}, 63 | {"Right Middle Spread", "RightHand.Middle.Spread"}, 64 | {"Right Middle 2 Stretched", "RightHand.Middle.2 Stretched"}, 65 | {"Right Middle 3 Stretched", "RightHand.Middle.3 Stretched"}, 66 | {"Right Ring 1 Stretched", "RightHand.Ring.1 Stretched"}, 67 | {"Right Ring Spread", "RightHand.Ring.Spread"}, 68 | {"Right Ring 2 Stretched", "RightHand.Ring.2 Stretched"}, 69 | {"Right Ring 3 Stretched", "RightHand.Ring.3 Stretched"}, 70 | {"Right Little 1 Stretched", "RightHand.Little.1 Stretched"}, 71 | {"Right Little Spread", "RightHand.Little.Spread"}, 72 | {"Right Little 2 Stretched", "RightHand.Little.2 Stretched"}, 73 | {"Right Little 3 Stretched", "RightHand.Little.3 Stretched"}, 74 | }; 75 | } 76 | 77 | /// 78 | /// モーションデータの中身 79 | /// 80 | public class HumanoidPoses : ScriptableObject 81 | { 82 | #if UNITY_EDITOR 83 | //Genericなanimファイルとして出力する 84 | [ContextMenu("Export as Generic animation clips")] 85 | public void ExportGenericAnim() 86 | { 87 | var clip = new AnimationClip { frameRate = 30 }; 88 | AnimationUtility.SetAnimationClipSettings(clip, new AnimationClipSettings { loopTime = false }); 89 | 90 | var bones = Poses[0].HumanoidBones; 91 | for (int i = 0; i < bones.Count; i++) 92 | { 93 | var positionCurveX = new AnimationCurve(); 94 | var positionCurveY = new AnimationCurve(); 95 | var positionCurveZ = new AnimationCurve(); 96 | var rotationCurveX = new AnimationCurve(); 97 | var rotationCurveY = new AnimationCurve(); 98 | var rotationCurveZ = new AnimationCurve(); 99 | var rotationCurveW = new AnimationCurve(); 100 | 101 | foreach (var p in Poses) 102 | { 103 | positionCurveX.AddKey(p.Time, p.HumanoidBones[i].LocalPosition.x); 104 | positionCurveY.AddKey(p.Time, p.HumanoidBones[i].LocalPosition.y); 105 | positionCurveZ.AddKey(p.Time, p.HumanoidBones[i].LocalPosition.z); 106 | rotationCurveX.AddKey(p.Time, p.HumanoidBones[i].LocalRotation.x); 107 | rotationCurveY.AddKey(p.Time, p.HumanoidBones[i].LocalRotation.y); 108 | rotationCurveZ.AddKey(p.Time, p.HumanoidBones[i].LocalRotation.z); 109 | rotationCurveW.AddKey(p.Time, p.HumanoidBones[i].LocalRotation.w); 110 | } 111 | 112 | //pathは階層 113 | //http://mebiustos.hatenablog.com/entry/2015/09/16/230000 114 | AnimationUtility.SetEditorCurve(clip, 115 | new EditorCurveBinding 116 | { 117 | path = Poses[0].HumanoidBones[i].Name, 118 | type = typeof(Transform), 119 | propertyName = "m_LocalPosition.x" 120 | }, positionCurveX); 121 | AnimationUtility.SetEditorCurve(clip, 122 | new EditorCurveBinding 123 | { 124 | path = Poses[0].HumanoidBones[i].Name, 125 | type = typeof(Transform), 126 | propertyName = "m_LocalPosition.y" 127 | }, positionCurveY); 128 | AnimationUtility.SetEditorCurve(clip, 129 | new EditorCurveBinding 130 | { 131 | path = Poses[0].HumanoidBones[i].Name, 132 | type = typeof(Transform), 133 | propertyName = "m_LocalPosition.z" 134 | }, positionCurveZ); 135 | 136 | AnimationUtility.SetEditorCurve(clip, 137 | new EditorCurveBinding 138 | { 139 | path = Poses[0].HumanoidBones[i].Name, 140 | type = typeof(Transform), 141 | propertyName = "m_LocalRotation.x" 142 | }, rotationCurveX); 143 | AnimationUtility.SetEditorCurve(clip, 144 | new EditorCurveBinding 145 | { 146 | path = Poses[0].HumanoidBones[i].Name, 147 | type = typeof(Transform), 148 | propertyName = "m_LocalRotation.y" 149 | }, rotationCurveY); 150 | AnimationUtility.SetEditorCurve(clip, 151 | new EditorCurveBinding 152 | { 153 | path = Poses[0].HumanoidBones[i].Name, 154 | type = typeof(Transform), 155 | propertyName = "m_LocalRotation.z" 156 | }, rotationCurveZ); 157 | AnimationUtility.SetEditorCurve(clip, 158 | new EditorCurveBinding 159 | { 160 | path = Poses[0].HumanoidBones[i].Name, 161 | type = typeof(Transform), 162 | propertyName = "m_LocalRotation.w" 163 | }, rotationCurveW); 164 | } 165 | 166 | clip.EnsureQuaternionContinuity(); 167 | 168 | var path = string.Format("Assets/Resources/RecordMotion_{0:yyyy_MM_dd_HH_mm_ss}_Generic.anim", DateTime.Now); 169 | var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); 170 | 171 | AssetDatabase.CreateAsset(clip, uniqueAssetPath); 172 | AssetDatabase.SaveAssets(); 173 | } 174 | 175 | //Humanoidなanimファイルとして出力する。 176 | [ContextMenu("Export as Humanoid animation clips")] 177 | public void ExportHumanoidAnim() 178 | { 179 | var clip = new AnimationClip { frameRate = 30 }; 180 | AnimationUtility.SetAnimationClipSettings(clip, new AnimationClipSettings { loopTime = false }); 181 | 182 | 183 | // body position 184 | { 185 | var curveX = new AnimationCurve(); 186 | var curveY = new AnimationCurve(); 187 | var curveZ = new AnimationCurve(); 188 | foreach (var item in Poses) 189 | { 190 | curveX.AddKey(item.Time, item.BodyPosition.x); 191 | curveY.AddKey(item.Time, item.BodyPosition.y); 192 | curveZ.AddKey(item.Time, item.BodyPosition.z); 193 | } 194 | 195 | const string muscleX = "RootT.x"; 196 | clip.SetCurve("", typeof(Animator), muscleX, curveX); 197 | const string muscleY = "RootT.y"; 198 | clip.SetCurve("", typeof(Animator), muscleY, curveY); 199 | const string muscleZ = "RootT.z"; 200 | clip.SetCurve("", typeof(Animator), muscleZ, curveZ); 201 | } 202 | // Leftfoot position 203 | { 204 | var curveX = new AnimationCurve(); 205 | var curveY = new AnimationCurve(); 206 | var curveZ = new AnimationCurve(); 207 | foreach (var item in Poses) 208 | { 209 | curveX.AddKey(item.Time, item.LeftfootIK_Pos.x); 210 | curveY.AddKey(item.Time, item.LeftfootIK_Pos.y); 211 | curveZ.AddKey(item.Time, item.LeftfootIK_Pos.z); 212 | } 213 | 214 | const string muscleX = "LeftFootT.x"; 215 | clip.SetCurve("", typeof(Animator), muscleX, curveX); 216 | const string muscleY = "LeftFootT.y"; 217 | clip.SetCurve("", typeof(Animator), muscleY, curveY); 218 | const string muscleZ = "LeftFootT.z"; 219 | clip.SetCurve("", typeof(Animator), muscleZ, curveZ); 220 | } 221 | // Rightfoot position 222 | { 223 | var curveX = new AnimationCurve(); 224 | var curveY = new AnimationCurve(); 225 | var curveZ = new AnimationCurve(); 226 | foreach (var item in Poses) 227 | { 228 | curveX.AddKey(item.Time, item.RightfootIK_Pos.x); 229 | curveY.AddKey(item.Time, item.RightfootIK_Pos.y); 230 | curveZ.AddKey(item.Time, item.RightfootIK_Pos.z); 231 | } 232 | 233 | const string muscleX = "RightFootT.x"; 234 | clip.SetCurve("", typeof(Animator), muscleX, curveX); 235 | const string muscleY = "RightFootT.y"; 236 | clip.SetCurve("", typeof(Animator), muscleY, curveY); 237 | const string muscleZ = "RightFootT.z"; 238 | clip.SetCurve("", typeof(Animator), muscleZ, curveZ); 239 | } 240 | // body rotation 241 | { 242 | var curveX = new AnimationCurve(); 243 | var curveY = new AnimationCurve(); 244 | var curveZ = new AnimationCurve(); 245 | var curveW = new AnimationCurve(); 246 | foreach (var item in Poses) 247 | { 248 | curveX.AddKey(item.Time, item.BodyRotation.x); 249 | curveY.AddKey(item.Time, item.BodyRotation.y); 250 | curveZ.AddKey(item.Time, item.BodyRotation.z); 251 | curveW.AddKey(item.Time, item.BodyRotation.w); 252 | } 253 | 254 | const string muscleX = "RootQ.x"; 255 | clip.SetCurve("", typeof(Animator), muscleX, curveX); 256 | const string muscleY = "RootQ.y"; 257 | clip.SetCurve("", typeof(Animator), muscleY, curveY); 258 | const string muscleZ = "RootQ.z"; 259 | clip.SetCurve("", typeof(Animator), muscleZ, curveZ); 260 | const string muscleW = "RootQ.w"; 261 | clip.SetCurve("", typeof(Animator), muscleW, curveW); 262 | } 263 | // Leftfoot rotation 264 | { 265 | var curveX = new AnimationCurve(); 266 | var curveY = new AnimationCurve(); 267 | var curveZ = new AnimationCurve(); 268 | var curveW = new AnimationCurve(); 269 | foreach (var item in Poses) 270 | { 271 | curveX.AddKey(item.Time, item.LeftfootIK_Rot.x); 272 | curveY.AddKey(item.Time, item.LeftfootIK_Rot.y); 273 | curveZ.AddKey(item.Time, item.LeftfootIK_Rot.z); 274 | curveW.AddKey(item.Time, item.LeftfootIK_Rot.w); 275 | } 276 | 277 | const string muscleX = "LeftFootQ.x"; 278 | clip.SetCurve("", typeof(Animator), muscleX, curveX); 279 | const string muscleY = "LeftFootQ.y"; 280 | clip.SetCurve("", typeof(Animator), muscleY, curveY); 281 | const string muscleZ = "LeftFootQ.z"; 282 | clip.SetCurve("", typeof(Animator), muscleZ, curveZ); 283 | const string muscleW = "LeftFootQ.w"; 284 | clip.SetCurve("", typeof(Animator), muscleW, curveW); 285 | } 286 | // Rightfoot rotation 287 | { 288 | var curveX = new AnimationCurve(); 289 | var curveY = new AnimationCurve(); 290 | var curveZ = new AnimationCurve(); 291 | var curveW = new AnimationCurve(); 292 | foreach (var item in Poses) 293 | { 294 | curveX.AddKey(item.Time, item.RightfootIK_Rot.x); 295 | curveY.AddKey(item.Time, item.RightfootIK_Rot.y); 296 | curveZ.AddKey(item.Time, item.RightfootIK_Rot.z); 297 | curveW.AddKey(item.Time, item.RightfootIK_Rot.w); 298 | } 299 | 300 | const string muscleX = "RightFootQ.x"; 301 | clip.SetCurve("", typeof(Animator), muscleX, curveX); 302 | const string muscleY = "RightFootQ.y"; 303 | clip.SetCurve("", typeof(Animator), muscleY, curveY); 304 | const string muscleZ = "RightFootQ.z"; 305 | clip.SetCurve("", typeof(Animator), muscleZ, curveZ); 306 | const string muscleW = "RightFootQ.w"; 307 | clip.SetCurve("", typeof(Animator), muscleW, curveW); 308 | } 309 | 310 | // muscles 311 | for (int i = 0; i < HumanTrait.MuscleCount; i++) 312 | { 313 | var curve = new AnimationCurve(); 314 | foreach (var item in Poses) 315 | { 316 | curve.AddKey(item.Time, item.Muscles[i]); 317 | } 318 | 319 | var muscle = HumanTrait.MuscleName[i]; 320 | if (MotionDataSettings.TraitPropMap.ContainsKey(muscle)) 321 | { 322 | muscle = MotionDataSettings.TraitPropMap[muscle]; 323 | } 324 | 325 | clip.SetCurve("", typeof(Animator), muscle, curve); 326 | } 327 | 328 | clip.EnsureQuaternionContinuity(); 329 | 330 | var path = string.Format("Assets/Resources/RecordMotion_{0:yyyy_MM_dd_HH_mm_ss}_Humanoid.anim", DateTime.Now); 331 | var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); 332 | 333 | AssetDatabase.CreateAsset(clip, uniqueAssetPath); 334 | AssetDatabase.SaveAssets(); 335 | } 336 | #endif 337 | 338 | [Serializable] 339 | public class SerializeHumanoidPose 340 | { 341 | public Vector3 BodyRootPosition; 342 | public Quaternion BodyRootRotation; 343 | 344 | public Vector3 BodyPosition; 345 | public Quaternion BodyRotation; 346 | public Vector3 LeftfootIK_Pos; 347 | public Quaternion LeftfootIK_Rot; 348 | public Vector3 RightfootIK_Pos; 349 | public Quaternion RightfootIK_Rot; 350 | 351 | public float[] Muscles; 352 | 353 | //フレーム数 354 | public int FrameCount; 355 | 356 | //記録開始後の経過時間。処理落ち対策 357 | public float Time; 358 | 359 | [Serializable] 360 | public class HumanoidBone 361 | { 362 | public string Name; 363 | public Vector3 LocalPosition; 364 | public Quaternion LocalRotation; 365 | 366 | private static Dictionary _pathCache = new Dictionary(); 367 | 368 | private static string BuildRelativePath(Transform root, Transform target) 369 | { 370 | var path = ""; 371 | _pathCache.TryGetValue(target, out path); 372 | if(path != null) return path; 373 | 374 | var current = target; 375 | while (true) 376 | { 377 | if (current == null) throw new Exception(target.name + "は" + root.name + "の子ではありません"); 378 | if (current == root) break; 379 | 380 | path = (path == "") ? current.name : current.name + "/" + path; 381 | 382 | current = current.parent; 383 | } 384 | 385 | _pathCache.Add(target, path); 386 | 387 | return path; 388 | } 389 | 390 | public void Set(Transform root, Transform t) 391 | { 392 | Name = BuildRelativePath(root, t); 393 | 394 | LocalPosition = t.localPosition; 395 | LocalRotation = t.localRotation; 396 | } 397 | } 398 | 399 | public List HumanoidBones = new List(); 400 | 401 | //CSVシリアライズ 402 | public string SerializeCSV() 403 | { 404 | StringBuilder sb = new StringBuilder(); 405 | SerializeVector3(sb, BodyRootPosition); 406 | SerializeQuaternion(sb, BodyRootRotation); 407 | SerializeVector3(sb, BodyPosition); 408 | SerializeQuaternion(sb, BodyRotation); 409 | foreach (var muscle in Muscles) 410 | { 411 | sb.Append(muscle); 412 | sb.Append(","); 413 | } 414 | sb.Append(FrameCount); 415 | sb.Append(","); 416 | sb.Append(Time); 417 | sb.Append(","); 418 | foreach (var humanoidBone in HumanoidBones) 419 | { 420 | sb.Append(humanoidBone.Name); 421 | sb.Append(","); 422 | SerializeVector3(sb, humanoidBone.LocalPosition); 423 | SerializeQuaternion(sb, humanoidBone.LocalRotation); 424 | } 425 | sb.Length = sb.Length - 1; //最後のカンマ削除 426 | return sb.ToString(); 427 | } 428 | 429 | private static void SerializeVector3(StringBuilder sb, Vector3 vec) 430 | { 431 | sb.Append(vec.x); 432 | sb.Append(","); 433 | sb.Append(vec.y); 434 | sb.Append(","); 435 | sb.Append(vec.z); 436 | sb.Append(","); 437 | } 438 | 439 | private static void SerializeQuaternion(StringBuilder sb, Quaternion q) 440 | { 441 | sb.Append(q.x); 442 | sb.Append(","); 443 | sb.Append(q.y); 444 | sb.Append(","); 445 | sb.Append(q.z); 446 | sb.Append(","); 447 | sb.Append(q.w); 448 | sb.Append(","); 449 | } 450 | 451 | //CSVデシリアライズ 452 | public void DeserializeCSV(string str) 453 | { 454 | string[] dataString = str.Split(','); 455 | BodyRootPosition = DeserializeVector3(dataString, 0); 456 | BodyRootRotation = DeserializeQuaternion(dataString, 3); 457 | BodyPosition = DeserializeVector3(dataString, 7); 458 | BodyRotation = DeserializeQuaternion(dataString, 10); 459 | Muscles = new float[HumanTrait.MuscleCount]; 460 | for (int i = 0; i < HumanTrait.MuscleCount; i++) 461 | { 462 | Muscles[i] = float.Parse(dataString[i + 14]); 463 | } 464 | FrameCount = int.Parse(dataString[14 + HumanTrait.MuscleCount]); 465 | Time = float.Parse(dataString[15 + HumanTrait.MuscleCount]); 466 | var boneValues = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[]; 467 | for (int i = 0; i < boneValues.Length; i++) 468 | { 469 | int startIndex = 16 + HumanTrait.MuscleCount + (i * 8); 470 | if (dataString.Length <= startIndex) 471 | { 472 | break; 473 | } 474 | 475 | HumanoidBone bone = new HumanoidBone(); 476 | bone.Name = dataString[startIndex]; 477 | bone.LocalPosition = DeserializeVector3(dataString, startIndex + 1); 478 | bone.LocalRotation = DeserializeQuaternion(dataString, startIndex + 4); 479 | } 480 | } 481 | 482 | private static Vector3 DeserializeVector3(IList str, int startIndex) 483 | { 484 | return new Vector3(float.Parse(str[startIndex]), float.Parse(str[startIndex + 1]), float.Parse(str[startIndex + 2])); 485 | } 486 | 487 | private static Quaternion DeserializeQuaternion(IList str, int startIndex) 488 | { 489 | return new Quaternion(float.Parse(str[startIndex]), float.Parse(str[startIndex + 1]), float.Parse(str[startIndex + 2]), float.Parse(str[startIndex + 3])); 490 | } 491 | 492 | } 493 | 494 | public List Poses = new List(); 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/HumanoidPoses.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 02076ca2c2f9f354ab4a1d3642665ff6 3 | timeCreated: 1523288396 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/MotionDataPlayer.cs: -------------------------------------------------------------------------------- 1 | /** 2 | [EasyMotionRecorder] 3 | 4 | Copyright (c) 2018 Duo.inc 5 | 6 | This software is released under the MIT License. 7 | http://opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | using UnityEngine; 11 | using System; 12 | 13 | namespace Entum 14 | { 15 | /// 16 | /// モーションデータ再生クラス 17 | /// SpringBone, DynamicBone, BulletPhysicsImplなどの揺れ物アセットのScript Execution Orderを20000など 18 | /// 大きな値にしてください。 19 | /// DefaultExecutionOrder(11000) はVRIK系より処理順を遅くする、という意図です 20 | /// 21 | [DefaultExecutionOrder(11000)] 22 | public class MotionDataPlayer : MonoBehaviour 23 | { 24 | [SerializeField] 25 | private KeyCode _playStartKey = KeyCode.S; 26 | [SerializeField] 27 | private KeyCode _playStopKey = KeyCode.T; 28 | 29 | [SerializeField] 30 | protected HumanoidPoses RecordedMotionData; 31 | [SerializeField] 32 | private Animator _animator; 33 | 34 | [SerializeField, Tooltip("再生開始フレームを指定します。0だとファイル先頭から開始です")] 35 | private int _startFrame; 36 | [SerializeField] 37 | private bool _playing; 38 | [SerializeField] 39 | private int _frameIndex; 40 | 41 | [SerializeField, Tooltip("普段はOBJECTROOTで問題ないです。特殊な機材の場合は変更してください")] 42 | private MotionDataSettings.Rootbonesystem _rootBoneSystem = MotionDataSettings.Rootbonesystem.Objectroot; 43 | [SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")] 44 | private HumanBodyBones _targetRootBone = HumanBodyBones.Hips; 45 | 46 | private HumanPoseHandler _poseHandler; 47 | private Action _onPlayFinish; 48 | private float _playingTime; 49 | 50 | private void Awake() 51 | { 52 | if (_animator == null) 53 | { 54 | Debug.LogError("MotionDataPlayerにanimatorがセットされていません。MotionDataPlayerを削除します。"); 55 | Destroy(this); 56 | return; 57 | } 58 | 59 | 60 | _poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); 61 | _onPlayFinish += StopMotion; 62 | } 63 | 64 | // Update is called once per frame 65 | private void Update() 66 | { 67 | if (Input.GetKeyDown(_playStartKey)) 68 | { 69 | PlayMotion(); 70 | } 71 | 72 | if (Input.GetKeyDown(_playStopKey)) 73 | { 74 | StopMotion(); 75 | } 76 | } 77 | 78 | private void LateUpdate() 79 | { 80 | if (!_playing) 81 | { 82 | return; 83 | } 84 | 85 | 86 | _playingTime += Time.deltaTime; 87 | SetHumanPose(); 88 | } 89 | 90 | /// 91 | /// モーションデータ再生開始 92 | /// 93 | private void PlayMotion() 94 | { 95 | if (_playing) 96 | { 97 | return; 98 | } 99 | 100 | if (RecordedMotionData == null) 101 | { 102 | Debug.LogError("録画済みモーションデータが指定されていません。再生を行いません。"); 103 | return; 104 | } 105 | 106 | 107 | _playingTime = _startFrame * (Time.deltaTime / 1f); 108 | _frameIndex = _startFrame; 109 | _playing = true; 110 | } 111 | 112 | /// 113 | /// モーションデータ再生終了。フレーム数が最後になっても自動で呼ばれる 114 | /// 115 | private void StopMotion() 116 | { 117 | if (!_playing) 118 | { 119 | return; 120 | } 121 | 122 | 123 | _playingTime = 0f; 124 | _frameIndex = _startFrame; 125 | _playing = false; 126 | } 127 | 128 | private void SetHumanPose() 129 | { 130 | var pose = new HumanPose(); 131 | pose.muscles = RecordedMotionData.Poses[_frameIndex].Muscles; 132 | _poseHandler.SetHumanPose(ref pose); 133 | pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition; 134 | pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation; 135 | 136 | switch (_rootBoneSystem) 137 | { 138 | case MotionDataSettings.Rootbonesystem.Objectroot: 139 | //_animator.transform.localPosition = RecordedMotionData.Poses[_frameIndex].BodyRootPosition; 140 | //_animator.transform.localRotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation; 141 | break; 142 | 143 | case MotionDataSettings.Rootbonesystem.Hipbone: 144 | pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition; 145 | pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation; 146 | 147 | _animator.GetBoneTransform(_targetRootBone).position = RecordedMotionData.Poses[_frameIndex].BodyRootPosition; 148 | _animator.GetBoneTransform(_targetRootBone).rotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation; 149 | break; 150 | 151 | default: 152 | throw new ArgumentOutOfRangeException(); 153 | } 154 | 155 | //処理落ちしたモーションデータの再生速度調整 156 | if (_playingTime > RecordedMotionData.Poses[_frameIndex].Time) 157 | { 158 | _frameIndex++; 159 | } 160 | 161 | if (_frameIndex == RecordedMotionData.Poses.Count - 1) 162 | { 163 | if (_onPlayFinish != null) 164 | { 165 | _onPlayFinish(); 166 | } 167 | } 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/MotionDataPlayer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6a94616cf260a0e4386d211583fd6681 3 | timeCreated: 1523289503 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/MotionDataRecorder.cs: -------------------------------------------------------------------------------- 1 | /** 2 | [EasyMotionRecorder] 3 | 4 | Copyright (c) 2018 Duo.inc 5 | 6 | This software is released under the MIT License. 7 | http://opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | using UnityEngine; 11 | using System; 12 | using System.IO; 13 | using System.Reflection; 14 | #if UNITY_EDITOR 15 | using UnityEditor; 16 | #endif 17 | 18 | namespace Entum 19 | { 20 | /// 21 | /// モーションデータ記録クラス 22 | /// スクリプト実行順はVRIKの処理が終わった後の姿勢を取得したいので 23 | /// 最大値=32000を指定しています 24 | /// 25 | [DefaultExecutionOrder(32000)] 26 | public class MotionDataRecorder : MonoBehaviour 27 | { 28 | [SerializeField] 29 | private KeyCode _recordStartKey = KeyCode.R; 30 | [SerializeField] 31 | private KeyCode _recordStopKey = KeyCode.X; 32 | 33 | [SerializeField] 34 | private Animator _animator; 35 | 36 | [SerializeField] 37 | private bool _recording; 38 | [SerializeField] 39 | protected int FrameIndex; 40 | 41 | [SerializeField, Tooltip("普段はOBJECTROOTで問題ないです。特殊な機材の場合は変更してください")] 42 | private MotionDataSettings.Rootbonesystem _rootBoneSystem = MotionDataSettings.Rootbonesystem.Objectroot; 43 | [SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")] 44 | private HumanBodyBones _targetRootBone = HumanBodyBones.Hips; 45 | [SerializeField] 46 | private HumanBodyBones IK_LeftFootBone = HumanBodyBones.LeftFoot; 47 | [SerializeField] 48 | private HumanBodyBones IK_RightFootBone = HumanBodyBones.RightFoot; 49 | 50 | protected HumanoidPoses Poses; 51 | protected float RecordedTime; 52 | protected float StartTime; 53 | 54 | private HumanPose _currentPose; 55 | private HumanPoseHandler _poseHandler; 56 | public Action OnRecordStart; 57 | public Action OnRecordEnd; 58 | 59 | [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] 60 | public float TargetFPS = 60.0f; 61 | 62 | 63 | // Use this for initialization 64 | private void Awake() 65 | { 66 | if (_animator == null) 67 | { 68 | Debug.LogError("MotionDataRecorderにanimatorがセットされていません。MotionDataRecorderを削除します。"); 69 | Destroy(this); 70 | return; 71 | } 72 | 73 | _poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); 74 | } 75 | 76 | private void Update() 77 | { 78 | if (Input.GetKeyDown(_recordStartKey)) 79 | { 80 | RecordStart(); 81 | } 82 | 83 | if (Input.GetKeyDown(_recordStopKey)) 84 | { 85 | RecordEnd(); 86 | } 87 | } 88 | 89 | // Update is called once per frame 90 | private void LateUpdate() 91 | { 92 | if (!_recording) 93 | { 94 | return; 95 | } 96 | 97 | 98 | RecordedTime = Time.time - StartTime; 99 | 100 | if (TargetFPS != 0.0f) 101 | { 102 | var nextTime = (1.0f * (FrameIndex + 1)) / TargetFPS; 103 | if (nextTime > RecordedTime) 104 | { 105 | return; 106 | } 107 | if (FrameIndex % TargetFPS == 0) 108 | { 109 | print("Motion_FPS=" + 1 / (RecordedTime / FrameIndex)); 110 | } 111 | } 112 | else 113 | { 114 | if (Time.frameCount % Application.targetFrameRate == 0) 115 | { 116 | print("Motion_FPS=" + 1 / Time.deltaTime); 117 | } 118 | } 119 | 120 | 121 | //現在のフレームのHumanoidの姿勢を取得 122 | _poseHandler.GetHumanPose(ref _currentPose); 123 | //posesに取得した姿勢を書き込む 124 | var serializedPose = new HumanoidPoses.SerializeHumanoidPose(); 125 | 126 | switch (_rootBoneSystem) 127 | { 128 | case MotionDataSettings.Rootbonesystem.Objectroot: 129 | serializedPose.BodyRootPosition = _animator.transform.localPosition; 130 | serializedPose.BodyRootRotation = _animator.transform.localRotation; 131 | break; 132 | 133 | case MotionDataSettings.Rootbonesystem.Hipbone: 134 | serializedPose.BodyRootPosition = _animator.GetBoneTransform(_targetRootBone).position; 135 | serializedPose.BodyRootRotation = _animator.GetBoneTransform(_targetRootBone).rotation; 136 | Debug.LogWarning(_animator.GetBoneTransform(_targetRootBone).position); 137 | break; 138 | 139 | default: 140 | throw new ArgumentOutOfRangeException(); 141 | } 142 | var bodyTQ = new TQ(_currentPose.bodyPosition, _currentPose.bodyRotation); 143 | var LeftFootTQ = new TQ(_animator.GetBoneTransform(IK_LeftFootBone).position, _animator.GetBoneTransform(IK_LeftFootBone).rotation); 144 | var RightFootTQ = new TQ(_animator.GetBoneTransform(IK_RightFootBone).position, _animator.GetBoneTransform(IK_RightFootBone).rotation); 145 | LeftFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.LeftFoot, bodyTQ, LeftFootTQ); 146 | RightFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.RightFoot, bodyTQ, RightFootTQ); 147 | 148 | serializedPose.BodyPosition = bodyTQ.t; 149 | serializedPose.BodyRotation = bodyTQ.q; 150 | serializedPose.LeftfootIK_Pos = LeftFootTQ.t; 151 | serializedPose.LeftfootIK_Rot = LeftFootTQ.q; 152 | serializedPose.RightfootIK_Pos = RightFootTQ.t; 153 | serializedPose.RightfootIK_Rot = RightFootTQ.q; 154 | 155 | 156 | 157 | serializedPose.FrameCount = FrameIndex; 158 | serializedPose.Muscles = new float[_currentPose.muscles.Length]; 159 | serializedPose.Time = RecordedTime; 160 | for (int i = 0; i < serializedPose.Muscles.Length; i++) 161 | { 162 | serializedPose.Muscles[i] = _currentPose.muscles[i]; 163 | } 164 | 165 | SetHumanBoneTransformToHumanoidPoses(_animator, ref serializedPose); 166 | 167 | Poses.Poses.Add(serializedPose); 168 | FrameIndex++; 169 | } 170 | 171 | /// 172 | /// 録画開始 173 | /// 174 | private void RecordStart() 175 | { 176 | if (_recording) 177 | { 178 | return; 179 | } 180 | 181 | 182 | Poses = ScriptableObject.CreateInstance(); 183 | 184 | if (OnRecordStart != null) 185 | { 186 | OnRecordStart(); 187 | } 188 | 189 | OnRecordEnd += WriteAnimationFile; 190 | _recording = true; 191 | RecordedTime = 0f; 192 | StartTime = Time.time; 193 | FrameIndex = 0; 194 | } 195 | 196 | /// 197 | /// 録画終了 198 | /// 199 | private void RecordEnd() 200 | { 201 | if (!_recording) 202 | { 203 | return; 204 | } 205 | 206 | 207 | if (OnRecordEnd != null) 208 | { 209 | OnRecordEnd(); 210 | } 211 | 212 | OnRecordEnd -= WriteAnimationFile; 213 | _recording = false; 214 | } 215 | 216 | private static void SetHumanBoneTransformToHumanoidPoses(Animator animator, ref HumanoidPoses.SerializeHumanoidPose pose) 217 | { 218 | HumanBodyBones[] values = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[]; 219 | foreach (HumanBodyBones b in values) 220 | { 221 | if (b < 0 || b >= HumanBodyBones.LastBone) 222 | { 223 | continue; 224 | } 225 | 226 | Transform t = animator.GetBoneTransform(b); 227 | if (t != null) 228 | { 229 | var bone = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone(); 230 | bone.Set(animator.transform, t); 231 | pose.HumanoidBones.Add(bone); 232 | } 233 | } 234 | } 235 | 236 | protected virtual void WriteAnimationFile() 237 | { 238 | #if UNITY_EDITOR 239 | SafeCreateDirectory("Assets/Resources"); 240 | 241 | var path = string.Format("Assets/Resources/RecordMotion_{0}{1:yyyy_MM_dd_HH_mm_ss}.asset", _animator.name, DateTime.Now); 242 | var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); 243 | 244 | AssetDatabase.CreateAsset(Poses, uniqueAssetPath); 245 | AssetDatabase.Refresh(); 246 | StartTime = Time.time; 247 | RecordedTime = 0f; 248 | FrameIndex = 0; 249 | #endif 250 | } 251 | 252 | /// 253 | /// 指定したパスにディレクトリが存在しない場合 254 | /// すべてのディレクトリとサブディレクトリを作成します 255 | /// 256 | public static DirectoryInfo SafeCreateDirectory(string path) 257 | { 258 | return Directory.Exists(path) ? null : Directory.CreateDirectory(path); 259 | } 260 | public Animator CharacterAnimator 261 | { 262 | get { return _animator; } 263 | } 264 | 265 | public class TQ 266 | { 267 | public TQ(Vector3 translation, Quaternion rotation) 268 | { 269 | t = translation; 270 | q = rotation; 271 | } 272 | public Vector3 t; 273 | public Quaternion q; 274 | // Scale should always be 1,1,1 275 | } 276 | public class AvatarUtility 277 | { 278 | static public TQ GetIKGoalTQ(Avatar avatar, float humanScale, AvatarIKGoal avatarIKGoal, TQ animatorBodyPositionRotation, TQ skeletonTQ) 279 | { 280 | int humanId = (int)HumanIDFromAvatarIKGoal(avatarIKGoal); 281 | if (humanId == (int)HumanBodyBones.LastBone) 282 | throw new InvalidOperationException("Invalid human id."); 283 | MethodInfo methodGetAxisLength = typeof(Avatar).GetMethod("GetAxisLength", BindingFlags.Instance | BindingFlags.NonPublic); 284 | if (methodGetAxisLength == null) 285 | throw new InvalidOperationException("Cannot find GetAxisLength method."); 286 | MethodInfo methodGetPostRotation = typeof(Avatar).GetMethod("GetPostRotation", BindingFlags.Instance | BindingFlags.NonPublic); 287 | if (methodGetPostRotation == null) 288 | throw new InvalidOperationException("Cannot find GetPostRotation method."); 289 | Quaternion postRotation = (Quaternion)methodGetPostRotation.Invoke(avatar, new object[] { humanId }); 290 | var goalTQ = new TQ(skeletonTQ.t, skeletonTQ.q * postRotation); 291 | if (avatarIKGoal == AvatarIKGoal.LeftFoot || avatarIKGoal == AvatarIKGoal.RightFoot) 292 | { 293 | // Here you could use animator.leftFeetBottomHeight or animator.rightFeetBottomHeight rather than GetAxisLenght 294 | // Both are equivalent but GetAxisLength is the generic way and work for all human bone 295 | float axislength = (float)methodGetAxisLength.Invoke(avatar, new object[] { humanId }); 296 | Vector3 footBottom = new Vector3(axislength, 0, 0); 297 | goalTQ.t += (goalTQ.q * footBottom); 298 | } 299 | // IK goal are in avatar body local space 300 | Quaternion invRootQ = Quaternion.Inverse(animatorBodyPositionRotation.q); 301 | goalTQ.t = invRootQ * (goalTQ.t - animatorBodyPositionRotation.t); 302 | goalTQ.q = invRootQ * goalTQ.q; 303 | goalTQ.t /= humanScale; 304 | 305 | return goalTQ; 306 | } 307 | static public HumanBodyBones HumanIDFromAvatarIKGoal(AvatarIKGoal avatarIKGoal) 308 | { 309 | HumanBodyBones humanId = HumanBodyBones.LastBone; 310 | switch (avatarIKGoal) 311 | { 312 | case AvatarIKGoal.LeftFoot: humanId = HumanBodyBones.LeftFoot; break; 313 | case AvatarIKGoal.RightFoot: humanId = HumanBodyBones.RightFoot; break; 314 | case AvatarIKGoal.LeftHand: humanId = HumanBodyBones.LeftHand; break; 315 | case AvatarIKGoal.RightHand: humanId = HumanBodyBones.RightHand; break; 316 | } 317 | return humanId; 318 | } 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/MotionDataRecorder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 171f7816f5da7ba4eab3cf34b9955f13 3 | timeCreated: 1523288492 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Duo.inc 2020 @neon-izm 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![release](https://img.shields.io/github/release/neon-izm/EasyMotionRecorder.svg?style=flat-square)](https://github.com/neon-izm/EasyMotionRecorder/releases) 3 | 4 | # EasyMotionRecorder 5 | Unityエディタ上でVRIKなどのモーションキャプチャをしたHumanoidキャラクターのモーションを記録, 再生をするスクリプトです。 6 | 7 | 例えばVTuberの人が1テイク目でキャラの動きだけを撮り、2テイク目以降で1テイク目のモーションを再生しながら音声と表情を付ける、という使い方を想定しています。 8 | 9 | ## 使い方 10 | [releases](https://github.com/neon-izm/EasyMotionRecorder/releases)からEasyMotionRecorder.unitypackageをダウンロードしてプロジェクトにインポートしてください 11 | 12 | ### Setup手順 13 | 0. モーションキャプチャ対象のキャラクターをHumanoidにしておく。OculusTouchやViveコントローラ、あるいはAxisNeuronやKinectの動きがエディタ上で反映されているシーンをセットアップする。 14 | 15 | 1. シーン上に`/Assets/EasyMotionRecorder/Prefabs/EasyMotionRecorder.prefab`を配置する。 16 | 17 | 2. 1.でシーン上に配置したEasyMotionRecorderにアタッチされているMotionDataPlayerコンポーネントおよびMotionDataRecoderコンポーネントのAnimatorにモーションキャプチャ対象のキャラクターをアタッチする。 18 | 19 | ### モーション記録 20 | 1. Unityエディタ上で実行して、Rキーを押したタイミングからモーションキャプチャデータを記録、Xキーでファイル書き出しをして記録を終了します。 21 | 22 | 2. /Assets/Resources/の中にRecordMotion_2018~~~ ファイルが生成されていればモーション記録が成功しています。 23 | 24 | ### モーション再生 25 | 1. エディタ実行前にモーション録画で生成したファイル(RecordMotion_2018~~~ )をシーン上に存在するEasyMotionRecorderゲームオブジェクトのMotionDataPlayerコンポーネントのRecordedMotionDataプロパティにアタッチします。 26 | 27 | 2. Unityエディタ上で実行して、Sキーでモーションデータの再生開始が行えます。モーションデータの最後に到達するか、Tキーでモーションデータ再生が終了します。 28 | 29 | ## おすすめの使い方(.animファイル書き出し) 30 | モーション記録して生成されたRecordMotion_2018~~~ ファイルを選択、インスペクタ上で右クリックして「Export as Humanoid animation clips」を選択するとAnimationClipに変換されます。 31 | 32 | 変換後のAnimationClipはHumanoid準拠のモーションとしてMecanimAnimatorやUnityTimeline上で扱うことが出来ます。 33 | 34 | もし床にキャラクターが沈んでしまう場合はHumanoidのAnimationClipにあるRoot Transform Position(Y)をBased Upon:Originalに変更してください。 35 | ![export_gif](https://github.com/duo-inc/EasyMotionRecorder/blob/readme_images/Images/emrec_export_humanoid.gif) 36 | 37 | ## FAQ 38 | 39 | ### 使っているとUnityが重い 40 | 申し訳ありません、録画中の処理負荷は多少増えます。 41 | 42 | ### 使うショートカットキーを変えたい 43 | インスペクタ上のEasyMotionRecorder内、MotionDataRecoderとMotionDataPlayerでキーを選べるようになっています。 44 | 45 | ### 長時間記録しているとUnityが落ちた 46 | 素朴な実装を行っているため記録中は常にメモリを食い続けます。 47 | 48 | もし長時間の記録が必要であればメモリの増設をお勧めします、10分程度でしたら問題ありません。 49 | 50 | ### モーションを再生しているときにスカートや髪が揺れない 51 | スクリプトの実行順を変更してください。 52 | 53 | SpringBone, DynamicBone, BulletPhysicsImplなど、揺れ物アセットの[Script Execution Order](https://docs.unity3d.com/jp/530/Manual/class-ScriptExecution.html)を20000以上に設定してください。 54 | 55 | ### 再生開始フレームを指定したい 56 | MotionDataPlayer内のstartFrameで再生開始フレーム指定が可能です。 57 | 58 | ### セットアップの説明が分かりづらい 59 | 申し訳ありません、[セットアップ手順の動画](https://twitter.com/entum_info/status/986823609329926146)がありますのでご参照ください。 60 | 61 | ビルドしたバイナリ上でモーションを記録したい場合は[MotionDataPlayerCSV.csなど](https://github.com/neon-izm/EasyMotionRecorder/tree/master/EasyMotionRecorder/Assets/EasyMotionRecorder/Scripts/ForRuntime)をご参照ください。 62 | 63 | ## Known Issues 64 | ### VRIK使用時にキャラクターの立ち位置がズレることがある 65 | VRIKの処理順による問題です、再生開始時の位置を変更することで暫定対処可能です。 66 | 67 | ## 動作環境 68 | Unity 2017.4.28f1 64bit (Windows)のエディタ上で動作確認をしています。 69 | 70 | Unity2017.4~Unity2019.3 での動作実績があります。 71 | 72 | 公式にはVRIKをサポートしますが、Kinectや各種モーションキャプチャーシステムでの動作も(無保証ですが)動作実績があります。 73 | 74 | ## ライセンス 75 | This software is released under the [MIT License, see LICENSE.txt](https://github.com/neon-izm/EasyMotionRecorder/blob/master/LICENSE.txt). 76 | 77 | (このソフトウェアは、[MITライセンス](https://github.com/neon-izm/EasyMotionRecorder/blob/master/LICENSE.txt)のもとで公開されています。) 78 | --------------------------------------------------------------------------------