├── MocapData.R ├── README.md ├── RecordAnimation.cs └── mocap.jpg /MocapData.R: -------------------------------------------------------------------------------- 1 | # this script is designed to parse through each Unity HumanPose.muscle individually 2 | # written by Marius Rubo 3 | 4 | rm(list=ls()) 5 | animationNumber = 5 6 | projectPath <- 'C:/Users/Public/Documents/Unity Projects/ERC WPC 2017-04-03' 7 | filePath <- paste(projectPath, '/Assets/Animations/Animation', animationNumber, '.csv', sep="") 8 | 9 | mocapData <- read.table(filePath,skip=1,dec=",", sep=";",na.strings="NA") # leave out first row, which stores data on position and rotation at program start 10 | names(mocapData) <- c("bodyPosition_x", "bodyPosition_y", "bodyPosition_z", "bodyRotation_x", "bodyRotation_y", "bodyRotation_z", 11 | "bodyRotation_w", "bodyRotationEuler_x", "bodyRotationEuler_y","bodyRotationEuler_z", 12 | "Spine_Front_Back","Spine_Left_Right","Spine_Twist_Left_Right","Chest_Front_Back","Chest_Left_Right","Chest_Twist_Left_Right","Neck_Nod_Down_Up", 13 | "Neck_Tilt_Left_Right","Neck_Turn_Left_Right","Head_Nod_Down_Up","Head_Tilt_Left_Right","Head_Turn_Left_Right","Left_Eye_Down_Up","Left_Eye_In_Out", 14 | "Right_Eye_Down_Up","Right_Eye_In_Out","Jaw_Close","Jaw_Left_Right","Left_Upper_Leg_Front_Back","Left_Upper_Leg_In_Out","Left_Upper_Leg_Twist_In_Out", 15 | "Left_Lower_Leg_Stretch","Left_Lower_Leg_Twist_In_Out","Left_Foot_Up_Down","Left_Foot_Twist_In_Out","Left_Toes_Up_Down","Right_Upper_Leg_Front_Back", 16 | "Right_Upper_Leg_In_Out","Right_Upper_Leg_Twist_In_Out","Right_Lower_Leg_Stretch","Right_Lower_Leg_Twist_In_Out","Right_Foot_Up_Down", 17 | "Right_Foot_Twist_In_Out","Right_Toes_Up_Down","Left_Shoulder_Down_Up","Left_Shoulder_Front_Back","Left_Arm_Down_Up","Left_Arm_Front_Back", 18 | "Left_Arm_Twist_In_Out","Left_Forearm_Stretch","Left_Forearm_Twist_In_Out","Left_Hand_Down_Up","Left_Hand_In_Out","Right_Shoulder_Down_Up", 19 | "Right_Shoulder_Front_Back","Right_Arm_Down_Up","Right_Arm_Front_Back","Right_Arm_Twist_In_Out","Right_Forearm_Stretch","Right_Forearm_Twist_In_Out", 20 | "Right_Hand_Down_Up","Right_Hand_In_Out","Left_Thumb_1_Stretched","Left_Thumb_Spread","Left_Thumb_2_Stretched","Left_Thumb_3_Stretched", 21 | "Left_Index_1_Stretched","Left_Index_Spread","Left_Index_2_Stretched","Left_Index_3_Stretched","Left_Middle_1_Stretched","Left_Middle_Spread", 22 | "Left_Middle_2_Stretched","Left_Middle_3_Stretched","Left_Ring_1_Stretched","Left_Ring_Spread","Left_Ring_2_Stretched","Left_Ring_3_Stretched", 23 | "Left_Little_1_Stretched","Left_Little_Spread","Left_Little_2_Stretched","Left_Little_3_Stretched","Right_Thumb_1_Stretched","Right_Thumb_Spread", 24 | "Right_Thumb_2_Stretched","Right_Thumb_3_Stretched","Right_Index_1_Stretched","Right_Index_Spread","Right_Index_2_Stretched","Right_Index_3_Stretched", 25 | "Right_Middle_1_Stretched","Right_Middle_Spread","Right_Middle_2_Stretched","Right_Middle_3_Stretched","Right_Ring_1_Stretched","Right_Ring_Spread", 26 | "Right_Ring_2_Stretched","Right_Ring_3_Stretched","Right_Little_1_Stretched","Right_Little_Spread","Right_Little_2_Stretched", 27 | "Right_Little_3_Stretched") 28 | 29 | # Make all variables numeric 30 | for (var in 1:length(mocapData)){ 31 | mocapData[,var] <- as.numeric(as.character(mocapData[,var])) 32 | } 33 | 34 | # some interesting variables 35 | plot(mocapData$bodyPosition_z) 36 | plot(mocapData$bodyRotationEuler_y) 37 | plot(mocapData$Right_Forearm_Stretch[0:200]) 38 | plot(mocapData$Spine_Left_Right[0:200]) 39 | plot(mocapData$Left_Foot_Up_Down[0:200]) 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | These scripts allow you to record mocap data to, and play from .csv files. This approach facilitates parsing of body movement data in statistical software such as MATLAB or R. 3 | 4 | ![alt tag](https://github.com/mariusrubo/Unity-Humanoid-Mocap-CSV/blob/master/mocap.jpg) 5 | 6 | # Installation 7 | * Attach the script "RecordAnimation.cs" to the character that is being tracked. 8 | * Press play, choose an animation number, press 'Start Rec' and "Stop Rec" to track the character's movement and "Save Anim" to store the data on your harddrive. A folder "Assets/Animations" will be created, and the data will be stored in there as a .csv file. 9 | * Press 'Play Anim' to scroll through the recorded animation using a slider. 10 | * You can load the recorded data into R using "MocapData.R". 11 | 12 | # Limitations 13 | * The script only captures movements that are completed inside the Update()-loop. This should work well for most motion capturing devices. If you capture movements processed in LateUpdate(), you will need to modify the scripts. For instance, if you use the Final IK package, the best option would be to use the solver's OnPostUpdate delegate. 14 | * While storing mocap data as .csv file facilitates statistical analyses, recorded animations can not easily be integrated and blended in a Unity controller component, as you can with .anim files. 15 | 16 | # License 17 | These scripts run under the GPLv3 license. 18 | -------------------------------------------------------------------------------- /RecordAnimation.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections; 3 | using System.IO; // needed for reading and writing .csv 4 | using System.Text; // for csv 5 | 6 | [RequireComponent(typeof(Animator))] 7 | public class RecordAnimation : MonoBehaviour 8 | { 9 | //public // skeletonRoot does not need to be inserted - it is simply the main transform 10 | Transform skeletonRoot; 11 | HumanPose currentPose = new HumanPose(); // keeps track of currentPose while animated 12 | HumanPose poseToSet; // reassembles Pose from .csv data 13 | Animator animator; 14 | HumanPoseHandler poseHandler; // to record entire animation 15 | 16 | int nframes = 108000; // max number of frames until unity stops recording automatically: currently set to 30min (30*60*60 = 108000) 17 | float[] currentPoseFloats; // sort current pose values as one array simply containing floats 18 | float[,] animationFloats; // = new float[nframes, 102]; // stack all currentPoseFloats in one array 19 | int AnimationNumber; 20 | int counterRec = 0; // count number of frames 21 | 22 | Vector3 PositionAtStart; 23 | Vector3 PoseAtStart; 24 | Quaternion RotationAtStart; 25 | 26 | // for setting a pose 27 | Vector3 posePosition; 28 | Vector3 poseRotationEuler; 29 | float[] poseMuscles; 30 | bool recordPoses = false; 31 | bool Reapply; // the recorded animation 32 | int counterPlay; // like counter, but counts animation playback frames 33 | float[] musclesPlayback; // zwischenspeichert values for muscles, cause they cannot be inserted directly 34 | 35 | void Start() 36 | { 37 | animationFloats = new float[nframes, 102]; 38 | currentPoseFloats = new float[102]; 39 | 40 | animator = GetComponent(); 41 | skeletonRoot = transform; // I take main transform as root (not "Hips" as I first guessed). Not sure if this works for all agents or only Autodesk. 42 | poseHandler = new HumanPoseHandler(animator.avatar, skeletonRoot); 43 | 44 | poseMuscles = new float[92]; 45 | musclesPlayback = new float[92]; 46 | } 47 | 48 | // Update is called once per frame 49 | // even running this in LateUpdate does not capture IK 50 | void LateUpdate() 51 | { 52 | if (recordPoses) { RecordPoses(); } 53 | if (Reapply) { ReapplyAnimation(); } 54 | } 55 | 56 | void OnGUI() 57 | { 58 | // change animation number using buttons (text field looked less elegant and I could not set it up via code) 59 | GUILayout.BeginArea(new Rect(240, 10, 100, 200)); 60 | if (GUILayout.Button("+")) { AnimationNumber++; } 61 | if (GUILayout.Button("Animation "+AnimationNumber.ToString())) { } 62 | if (GUILayout.Button("- ")) { AnimationNumber--; } 63 | GUILayout.EndArea(); 64 | 65 | GUILayout.BeginArea(new Rect(350, 10, 100, 200)); 66 | if (recordPoses == false) { if (GUILayout.Button("Start Rec")) { recordPoses = true; counterRec = 0; } } 67 | if (recordPoses == true) { if (GUILayout.Button("Stop Rec")) { recordPoses = false; } } 68 | if (GUILayout.Button("Play Anim")) { Reapply = true; counterPlay = 0; } 69 | if (GUILayout.Button("Save Anim")) { SaveAnimation(); } 70 | if (GUILayout.Button("Load Anim")) { LoadAnimation(); } 71 | //if (GUILayout.Button("Note Muscles")) { NoteMuscles(); } 72 | GUILayout.EndArea(); 73 | if (Reapply) { counterPlay = (int)GUI.HorizontalSlider(new Rect(30, 120, 400, 30), counterPlay, 1.0F, counterRec-1); } // start at 1 -> line 0 just sets position 74 | } 75 | 76 | // to get a simple file containing the names of all muscles 77 | void NoteMuscles() 78 | { 79 | string[] musclename = HumanTrait.MuscleName; // show names of muscles 80 | string path = Directory.GetCurrentDirectory() + "/Assets/Animations/MuscleNames.csv"; 81 | TextWriter sw = new StreamWriter(path); 82 | string Text = ""; 83 | for (int i = 0; i < HumanTrait.MuscleCount; i++) {Text = Text + musclename[i].ToString() + ";";} 84 | sw.WriteLine(Text); 85 | sw.Close(); 86 | } 87 | 88 | // Save entire animation as .csv (note: Streamwriter is much faster than "File.WriteAllText(path, Text);") 89 | public void SaveAnimation() 90 | { 91 | string path = Directory.GetCurrentDirectory(); 92 | path = path + "/Assets/Animations"; 93 | if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } // create "Animations" folder if it does not exist 94 | path = path + "/Animation" + AnimationNumber + ".csv"; 95 | TextWriter sw = new StreamWriter(path); 96 | string Line; 97 | 98 | for (int frame = 0; frame < counterRec; frame++) // run through all frames 99 | { 100 | Line = ""; 101 | for (int i = 0; i < currentPoseFloats.Length; i++) // and all values composing one Pose 102 | { 103 | Line = Line + animationFloats[frame, i].ToString() + ";"; 104 | } 105 | sw.WriteLine(Line); 106 | } 107 | sw.Close(); 108 | } 109 | 110 | public void LoadAnimation() // refill animationFloats with values from .csv-Files 111 | { 112 | string path = Directory.GetCurrentDirectory(); 113 | path = path + "/Assets/Animations"; 114 | path = path + "/Animation" + AnimationNumber + ".csv"; 115 | Debug.Log("Loading " + path.ToString()); 116 | 117 | if (File.Exists(path)) 118 | { 119 | animationFloats = new float[nframes, 102]; 120 | StreamReader sr = new StreamReader(path); 121 | int frame = 0; 122 | while (!sr.EndOfStream) 123 | { 124 | string[] Line = sr.ReadLine().Split(';'); 125 | for (int column = 0; column < Line.Length - 1; column++) 126 | { 127 | animationFloats[frame, column] = float.Parse(Line[column]); 128 | } 129 | frame++; 130 | counterRec = frame; // remember length of data, to correctly display slider 131 | } 132 | } 133 | else { Debug.Log("File not found"); } 134 | } 135 | 136 | public void RecordPoses() 137 | { 138 | poseHandler.GetHumanPose(ref currentPose); 139 | 140 | if (counterRec == 0) // first note down position and rotation at start 141 | { 142 | PoseAtStart = currentPose.bodyPosition; // somehow need to note down pose at start, reference all other poses to this 143 | PositionAtStart = transform.position; 144 | RotationAtStart = transform.rotation; 145 | 146 | // note down all values defining a pose 147 | currentPoseFloats[0] = PositionAtStart.x; 148 | currentPoseFloats[1] = PositionAtStart.y; 149 | currentPoseFloats[2] = PositionAtStart.z; 150 | 151 | currentPoseFloats[3] = RotationAtStart.x; 152 | currentPoseFloats[4] = RotationAtStart.y; 153 | currentPoseFloats[5] = RotationAtStart.z; 154 | currentPoseFloats[6] = RotationAtStart.w; 155 | 156 | currentPoseFloats[7] = RotationAtStart.eulerAngles.x; // also save rotation as Vector3 157 | currentPoseFloats[8] = RotationAtStart.eulerAngles.y; 158 | currentPoseFloats[9] = RotationAtStart.eulerAngles.z; 159 | 160 | for (int i = 0; i < 102; i++) { animationFloats[counterRec, i] = currentPoseFloats[i]; } 161 | counterRec++; 162 | } 163 | 164 | if (counterRec > 0 && counterRec < nframes) // then note down poses 165 | { 166 | // note down all values defining a pose 167 | currentPoseFloats[0] = currentPose.bodyPosition.x - PoseAtStart.x; 168 | currentPoseFloats[1] = currentPose.bodyPosition.y; // - PositionAtStart.y; // no need to correct y component. I don't know why. 169 | currentPoseFloats[2] = currentPose.bodyPosition.z - PoseAtStart.z; 170 | 171 | currentPoseFloats[3] = currentPose.bodyRotation.x; 172 | currentPoseFloats[4] = currentPose.bodyRotation.y; 173 | currentPoseFloats[5] = currentPose.bodyRotation.z; 174 | currentPoseFloats[6] = currentPose.bodyRotation.w; 175 | 176 | currentPoseFloats[7] = currentPose.bodyRotation.eulerAngles.x; // also save rotation as Vector3 177 | currentPoseFloats[8] = currentPose.bodyRotation.eulerAngles.y; 178 | currentPoseFloats[9] = currentPose.bodyRotation.eulerAngles.z; 179 | 180 | // note down all muscles 181 | for (int i = 0; i < 92; i++) { currentPoseFloats[i + 10] = currentPose.muscles[i]; } 182 | 183 | for (int i = 0; i < 102; i++) { animationFloats[counterRec, i] = currentPoseFloats[i]; } 184 | counterRec++; 185 | } 186 | 187 | } 188 | 189 | // loop through array and apply poses one after another. 190 | public void ReapplyAnimation() 191 | { 192 | if (counterPlay == 0) // first set position and rotation 193 | { 194 | animator.enabled = false; // first set off animator: otherwise, it will rotate towards goal again 195 | Vector3 PositionAtStart = new Vector3(animationFloats[0, 0], animationFloats[0, 1], animationFloats[0, 2]); 196 | transform.position = PositionAtStart; 197 | Quaternion RotationAtStart = new Quaternion(animationFloats[0, 3], animationFloats[0, 4], animationFloats[0, 5], animationFloats[0, 6]); 198 | //transform.rotation = RotationAtStart; 199 | transform.rotation = Quaternion.Euler(0f, 0f, 0f); // actually no need to reference to initial rotation 200 | 201 | counterPlay++; 202 | } 203 | 204 | if (counterPlay < nframes) 205 | { 206 | poseToSet = new HumanPose(); 207 | float[] currentPose = new float[102]; 208 | for (int i = 0; i < 102; i++) { currentPose[i] = animationFloats[counterPlay, i]; } // get only single pose from animation data 209 | 210 | poseToSet.bodyPosition = new Vector3(currentPose[0], currentPose[1], currentPose[2]); // retrieve position from stored data 211 | 212 | poseToSet.bodyRotation = new Quaternion(currentPose[3], currentPose[4], currentPose[5], currentPose[6]); // retrieve position 213 | 214 | //for (int i = 0; i < 92; i++) { poseToSet.muscles[i] = currentPose[i + 10]; } // does not work 215 | for (int i = 0; i < 92; i++) { musclesPlayback[i] = currentPose[i + 10]; } // retrieve muscles 216 | poseToSet.muscles = musclesPlayback; 217 | poseHandler.SetHumanPose(ref poseToSet); 218 | 219 | //counterPlay++; // enable this to let animation run at original speed 220 | } 221 | } 222 | } 223 | 224 | 225 | -------------------------------------------------------------------------------- /mocap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariusrubo/Unity-Humanoid-Mocap-CSV/259790aa5237245f8f2ffe1645a1782b79abb341/mocap.jpg --------------------------------------------------------------------------------