├── README.md ├── source_rigs ├── mega.json ├── mb.json ├── max.json ├── vicon.json ├── accad.json ├── eyes.json └── cmu.json ├── target_rigs ├── genesis.json ├── rigify.json ├── rigify2.json ├── mhx.json ├── genesis3.json ├── genesis8.json └── mh-official.json ├── LICENSE ├── buttons27.py ├── buttons28.py ├── io_json.py ├── action.py ├── source.py ├── simplify.py ├── target.py ├── floor.py ├── armature.py ├── t_pose.py ├── edit.py ├── loop.py ├── props.py ├── utils.py └── retarget.py /README.md: -------------------------------------------------------------------------------- 1 | # makewalk 2 | Mocap retargeting for Blender 3 | -------------------------------------------------------------------------------- /source_rigs/mega.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Mega", 3 | "url" : "", 4 | 5 | "armature" : { 6 | "hips" : "hips", 7 | "upperback" : "spine", 8 | "chest" : "chest", 9 | "neck" : "neck", 10 | "neck1" : "head", 11 | 12 | "lcollar" : "shoulder.L", 13 | "leftuparm" : "upper_arm.L", 14 | "leftlowarm" : "forearm.L", 15 | "lefthand" : "hand.L", 16 | 17 | "rcollar" : "shoulder.R", 18 | "rightuparm" : "upper_arm.R", 19 | "rightlowarm" : "forearm.R", 20 | "righthand" : "hand.R", 21 | 22 | "leftupleg" : "thigh.L", 23 | "leftlowleg" : "shin.L", 24 | "leftfoot" : "foot.L", 25 | "lefttoes" : "toe.L", 26 | 27 | "rightupleg" : "thigh.R", 28 | "rightlowleg" : "shin.R", 29 | "rightfoot" : "foot.R", 30 | "righttoes" : "toe.R" 31 | }, 32 | 33 | "t-pose" : [] 34 | } 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /source_rigs/mb.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "3DS Max", 3 | "url" : "", 4 | 5 | "armature" : { 6 | "hips" : "hips", 7 | "lowerback" : "spine", 8 | "spine" : "spine-1", 9 | "spine1" : "chest", 10 | "neck" : "neck", 11 | "neck1" : "head", 12 | 13 | "leftshoulder" : "shoulder.L", 14 | "leftarm" : "upper_arm.L", 15 | "leftforearm" : "forearm.L", 16 | "lefthand" : "hand.L", 17 | 18 | "rightshoulder" : "shoulder.R", 19 | "rightarm" : "upper_arm.R", 20 | "rightforearm" : "forearm.R", 21 | "righthand" : "hand.R", 22 | 23 | "leftupleg" : "thigh.L", 24 | "leftleg" : "shin.L", 25 | "leftfoot" : "foot.L", 26 | "lefttoebase" : "toe.L", 27 | 28 | "rightupleg" : "thigh.R", 29 | "rightleg" : "shin.R", 30 | "rightfoot" : "foot.R", 31 | "righttoebase" : "toe.R" 32 | }, 33 | 34 | "t-pose" : "cmu.json" 35 | } 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /target_rigs/genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Genesis", 3 | "url" : "www.daz3d.com", 4 | 5 | "bones" : { 6 | "hip" : "hips", 7 | "abdomen" : "spine", 8 | "abdomen2" : "spine-1", 9 | "chest" : "chest", 10 | "neck" : "neck", 11 | "head" : "head", 12 | 13 | "lCollar" : "shoulder.L", 14 | "lShldr" : "upper_arm.L", 15 | "lForeArm" : "forearm.L", 16 | "lHand" : "hand.L", 17 | 18 | "rCollar" : "shoulder.R", 19 | "rShldr" : "upper_arm.R", 20 | "rForeArm" : "forearm.R", 21 | "rHand" : "hand.R", 22 | 23 | "lThigh" : "thigh.L", 24 | "lShin" : "shin.L", 25 | "lFoot" : "foot.L", 26 | "lToe" : "toe.L", 27 | 28 | "rThigh" : "thigh.R", 29 | "rShin" : "shin.R", 30 | "rFoot" : "foot.R", 31 | "rToe" : "toe.R" 32 | }, 33 | 34 | "ik-bones" : [] 35 | } 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /target_rigs/rigify.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Rigify", 3 | "url" : "", 4 | 5 | "bones" : { 6 | "torso" : "hips", 7 | "spine" : "spine", 8 | "chest" : "chest", 9 | "neck" : "neck", 10 | "head" : "head", 11 | 12 | "shoulder.L" : "shoulder.L", 13 | "upper_arm.fk.L" : "upper_arm.L", 14 | "forearm.fk.L" : "forearm.L", 15 | "hand.fk.L" : "hand.L", 16 | 17 | "shoulder.R" : "shoulder.R", 18 | "upper_arm.fk.R" : "upper_arm.R", 19 | "forearm.fk.R" : "forearm.R", 20 | "hand.fk.R" : "hand.R", 21 | 22 | "thigh.fk.L" : "thigh.L", 23 | "shin.fk.L" : "shin.L", 24 | "foot.fk.L" : "foot.L", 25 | "toe.L" : "toe.L", 26 | 27 | "thigh.fk.R" : "thigh.R", 28 | "shin.fk.R" : "shin.R", 29 | "foot.fk.R" : "foot.R", 30 | "toe.R" : "toe.R" 31 | }, 32 | 33 | "ik-bones" : [] 34 | } 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /target_rigs/rigify2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Rigify2", 3 | "url" : "", 4 | 5 | "bones" : { 6 | "torso" : "hips", 7 | "spine" : "spine", 8 | "chest" : "chest", 9 | "neck" : "neck", 10 | "head" : "head", 11 | 12 | "shoulder.L" : "shoulder.L", 13 | "upper_arm_fk.L" : "upper_arm.L", 14 | "forearm_fk.L" : "forearm.L", 15 | "hand_fk.L" : "hand.L", 16 | 17 | "shoulder.R" : "shoulder.R", 18 | "upper_arm_fk.R" : "upper_arm.R", 19 | "forearm_fk.R" : "forearm.R", 20 | "hand_fk.R" : "hand.R", 21 | 22 | "thigh_fk.L" : "thigh.L", 23 | "shin_fk.L" : "shin.L", 24 | "foot_fk.L" : "foot.L", 25 | "toe.L" : "toe.L", 26 | 27 | "thigh_fk.R" : "thigh.R", 28 | "shin_fk.R" : "shin.R", 29 | "foot_fk.R" : "foot.R", 30 | "toe.R" : "toe.R" 31 | }, 32 | 33 | "ik-bones" : [] 34 | } 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /target_rigs/mhx.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "MHX", 3 | "url" : "www.makehuman.org", 4 | 5 | "bones" : { 6 | "hips" : "hips", 7 | "spine" : "spine", 8 | "spine-1" : "spine-1", 9 | "chest" : "chest", 10 | "neck" : "neck", 11 | "head" : "head", 12 | 13 | "clavicle.L" : "shoulder.L", 14 | "upper_arm.L" : "upper_arm.L", 15 | "forearm.L" : "forearm.L", 16 | "hand.L" : "hand.L", 17 | 18 | "clavicle.R" : "shoulder.R", 19 | "upper_arm.R" : "upper_arm.R", 20 | "forearm.R" : "forearm.R", 21 | "hand.R" : "hand.R", 22 | 23 | "thigh.L" : "thigh.L", 24 | "shin.L" : "shin.L", 25 | "foot.L" : "foot.L", 26 | "toe.L" : "toe.L", 27 | 28 | "thigh.R" : "thigh.R", 29 | "shin.R" : "shin.R", 30 | "foot.R" : "foot.R", 31 | "toe.R" : "toe.R" 32 | }, 33 | 34 | "ik-bones" : [] 35 | } 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /source_rigs/max.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "3DS Max", 3 | "url" : "", 4 | 5 | "armature" : { 6 | "hips" : "hips", 7 | 8 | "lefthip" : "thigh.L", 9 | "leftknee" : "shin.L", 10 | "leftankle" : "foot.L", 11 | "lefttoe" : "toe.L", 12 | 13 | "righthip" : "thigh.R", 14 | "rightknee" : "shin.R", 15 | "rightankle" : "foot.R", 16 | "righttoe" : "toe.R", 17 | 18 | "lowerback" : "spine", 19 | "spine-1" : "spine-1", 20 | "chest1" : "chest", 21 | "chest2" : "chest-1", 22 | "lowerneck" : "LowerNeck", 23 | "neck" : "neck", 24 | "head" : "head", 25 | 26 | "leftcollar" : "shoulder.L", 27 | "leftshoulder" : "upper_arm.L", 28 | "leftelbow" : "forearm.L", 29 | "leftwrist" : "hand.L", 30 | 31 | "rightcollar" : "shoulder.R", 32 | "rightshoulder" : "upper_arm.R", 33 | "rightelbow" : "forearm.R", 34 | "rightwrist" : "hand.R" 35 | }, 36 | 37 | "t-pose" : [] 38 | } 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /source_rigs/vicon.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Vicon Blade", 3 | "url" : "", 4 | 5 | "armature" : { 6 | "root" : "hips", 7 | "lowerback" : "spine", 8 | "thorax" : "chest", 9 | "neck" : "neck", 10 | "head" : "head", 11 | 12 | "l_collar" : "shoulder.L", 13 | "l_humerus" : "upper_arm.L", 14 | "l_elbow" : "forearm.L", 15 | "l_wrist" : "hand.L", 16 | "l_thumb" : "thumb.02.L", 17 | "l_wrist_end" : "thumb.03.L", 18 | 19 | "r_collar" : "shoulder.R", 20 | "r_humerus" : "upper_arm.R", 21 | "r_elbow" : "forearm.R", 22 | "r_wrist" : "hand.R", 23 | "r_thumb" : "thumb.02.R", 24 | "r_wrist_end" : "thumb.03.R", 25 | 26 | "l_femur" : "thigh.L", 27 | "l_tibia" : "shin.L", 28 | "l_foot" : "foot.L", 29 | "l_toe" : "toe.L", 30 | 31 | "r_femur" : "thigh.R", 32 | "r_tibia" : "shin.R", 33 | "r_foot" : "foot.R", 34 | "r_toe" : "toe.R" 35 | }, 36 | 37 | "t-pose" : [] 38 | } 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /source_rigs/accad.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "ACCAD", 3 | "url" : "www.accad.Accad.edu/research/mocap/mocap_data.htm", 4 | 5 | "armature" : { 6 | "hips" : "hips", 7 | "tospine" : "spine", 8 | "spine" : "spine-1", 9 | "spine1" : "chest", 10 | "neck" : "neck", 11 | "head" : "head", 12 | 13 | "leftshoulder" : "shoulder.L", 14 | "leftarm" : "upper_arm.L", 15 | "leftforearm" : "forearm.L", 16 | "lefthand" : "hand.L", 17 | 18 | "rightshoulder" : "shoulder.R", 19 | "rightarm" : "upper_arm.R", 20 | "rightforearm" : "forearm.R", 21 | "righthand" : "hand.R", 22 | 23 | "leftupleg" : "thigh.L", 24 | "leftleg" : "shin.L", 25 | "leftfoot" : "foot.L", 26 | "lefttoebase" : "toe.L", 27 | 28 | "rightupleg" : "thigh.R", 29 | "rightleg" : "shin.R", 30 | "rightfoot" : "foot.R", 31 | "righttoebase" : "toe.R" 32 | }, 33 | 34 | "t-pose" : [] 35 | } 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /source_rigs/eyes.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Eyes Japan", 3 | "url" : "", 4 | 5 | "armature" : { 6 | "hips" : "hips", 7 | "lefthip" : "thigh.L", 8 | "leftknee" : "shin.L", 9 | "leftankle" : "foot.L", 10 | "righthip" : "thigh.R", 11 | "rightknee" : "shin.R", 12 | "rightankle" : "foot.R", 13 | "spine-1" : "spine", 14 | "chest" : "spine-1", 15 | "chest2" : "spine-1", 16 | "cs_bvh" : "chest", 17 | "leftcollar" : "shoulder.L", 18 | "leftshoulder" : "upper_arm.L", 19 | "leftelbow" : "forearm.L", 20 | "leftwrist" : "hand.L", 21 | "rightcollar" : "shoulder.R", 22 | "rightshoulder" : "upper_arm.R", 23 | "rightelbow" : "forearm.R", 24 | "rightwrist" : "hand.R", 25 | "neck" : "neck", 26 | "head" : "head" 27 | }, 28 | 29 | "t-pose" : [ 30 | [ 31 | "LeftShoulder", 32 | [0.70711, 0, 0, -0.70711] 33 | ], 34 | [ 35 | "RightShoulder", 36 | [0.70711, 0, 0, 0.70711] 37 | ] 38 | ] 39 | } 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, Thomas Larsson 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /source_rigs/cmu.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "CMU", 3 | "url" : "www.accad.Accad.edu/research/mocap/mocap_data.htm", 4 | 5 | "armature" : { 6 | "hips" : "hips", 7 | "tospine" : "spine", 8 | "spine" : "spine-1", 9 | "spine1" : "chest", 10 | "neck" : "neck", 11 | "head" : "head", 12 | 13 | "leftshoulder" : "shoulder.L", 14 | "leftarm" : "upper_arm.L", 15 | "leftforearm" : "forearm.L", 16 | "lefthand" : "hand.L", 17 | 18 | "rightshoulder" : "shoulder.R", 19 | "rightarm" : "upper_arm.R", 20 | "rightforearm" : "forearm.R", 21 | "righthand" : "hand.R", 22 | 23 | "leftupleg" : "thigh.L", 24 | "leftleg" : "shin.L", 25 | "leftfoot" : "foot.L", 26 | "lefttoebase" : "toe.L", 27 | 28 | "rightupleg" : "thigh.R", 29 | "rightleg" : "shin.R", 30 | "rightfoot" : "foot.R", 31 | "righttoebase" : "toe.R" 32 | }, 33 | 34 | "t-pose" : [ 35 | [ 36 | "LeftUpLeg", 37 | [0.98481, -0.059391, 0, 0.16318] 38 | ], 39 | [ 40 | "LeftFoot", 41 | [0.98481, -1.9207e-06, -0.17365, 0] 42 | ], 43 | [ 44 | "LeftToeBase", 45 | [0.34239, -0.049454, -0.9381, 0.017106] 46 | ], 47 | [ 48 | "RightUpLeg", 49 | [0.98481, -0.059391, 0, -0.16318] 50 | ], 51 | [ 52 | "RightFoot", 53 | [0.98323, -0.0021406, 0.18234, -0.0026579] 54 | ], 55 | [ 56 | "RightToeBase", 57 | [0.34239, -0.049454, 0.9381, -0.017105] 58 | ] 59 | ] 60 | } 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /target_rigs/genesis3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Genesis3", 3 | "url" : "www.daz3d.com", 4 | 5 | "bones" : { 6 | "hip" : "hips", 7 | "abdomenLower" : "spine", 8 | "abdomenUpper" : "spine-1", 9 | "chestLower" : "chest", 10 | "neckLower" : "neck", 11 | "head" : "head", 12 | 13 | "lCollar" : "shoulder.L", 14 | "lShldrBend" : "upper_arm.L", 15 | "lForearmBend" : "forearm.L", 16 | "lHand" : "hand.L", 17 | 18 | "rCollar" : "shoulder.R", 19 | "rShldrBend" : "upper_arm.R", 20 | "rForearmBend" : "forearm.R", 21 | "rHand" : "hand.R", 22 | 23 | "lThighBend" : "thigh.L", 24 | "lShin" : "shin.L", 25 | "lFoot" : "foot.L", 26 | "lToe" : "toe.L", 27 | 28 | "rThighBend" : "thigh.R", 29 | "rShin" : "shin.R", 30 | "rFoot" : "foot.R", 31 | "rToe" : "toe.R", 32 | 33 | "lThumb1" : "lThumb1", 34 | "lThumb2" : "lThumb2", 35 | "lThumb3" : "lThumb3", 36 | 37 | "lIndex1" : "lIndex1", 38 | "lIndex2" : "lIndex2", 39 | "lIndex3" : "lIndex3", 40 | 41 | "lMid1" : "lMid1", 42 | "lMid2" : "lMid2", 43 | "lMid3" : "lMid3", 44 | 45 | "lRing1" : "lRing1", 46 | "lRing2" : "lRing2", 47 | "lRing3" : "lRing3", 48 | 49 | "lPinky1" : "lPinky1", 50 | "lPinky2" : "lPinky2", 51 | "lPinky3" : "lPinky3", 52 | 53 | "rThumb1" : "rThumb1", 54 | "rThumb2" : "rThumb2", 55 | "rThumb3" : "rThumb3", 56 | 57 | "rIndex1" : "rIndex1", 58 | "rIndex2" : "rIndex2", 59 | "rIndex3" : "rIndex3", 60 | 61 | "rMid1" : "rMid1", 62 | "rMid2" : "rMid2", 63 | "rMid3" : "rMid3", 64 | 65 | "rRing1" : "rRing1", 66 | "rRing2" : "rRing2", 67 | "rRing3" : "rRing3", 68 | 69 | "rPinky1" : "rPinky1", 70 | "rPinky2" : "rPinky2", 71 | "rPinky3" : "rPinky3" 72 | }, 73 | 74 | "ik-bones" : [] 75 | } 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /target_rigs/genesis8.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Genesis8", 3 | "url" : "www.daz3d.com", 4 | 5 | "bones" : { 6 | "hip" : "hips", 7 | "abdomenLower" : "spine", 8 | "abdomenUpper" : "spine-1", 9 | "chestLower" : "chest", 10 | "neckLower" : "neck", 11 | "head" : "head", 12 | 13 | "lCollar" : "shoulder.L", 14 | "lShldrBend" : "upper_arm.L", 15 | "lForearmBend" : "forearm.L", 16 | "lHand" : "hand.L", 17 | 18 | "rCollar" : "shoulder.R", 19 | "rShldrBend" : "upper_arm.R", 20 | "rForearmBend" : "forearm.R", 21 | "rHand" : "hand.R", 22 | 23 | "lThighBend" : "thigh.L", 24 | "lShin" : "shin.L", 25 | "lFoot" : "foot.L", 26 | "lToe" : "toe.L", 27 | 28 | "rThighBend" : "thigh.R", 29 | "rShin" : "shin.R", 30 | "rFoot" : "foot.R", 31 | "rToe" : "toe.R", 32 | 33 | "lThumb1" : "lThumb1", 34 | "lThumb2" : "lThumb2", 35 | "lThumb3" : "lThumb3", 36 | 37 | "lIndex1" : "lIndex1", 38 | "lIndex2" : "lIndex2", 39 | "lIndex3" : "lIndex3", 40 | 41 | "lMid1" : "lMid1", 42 | "lMid2" : "lMid2", 43 | "lMid3" : "lMid3", 44 | 45 | "lRing1" : "lRing1", 46 | "lRing2" : "lRing2", 47 | "lRing3" : "lRing3", 48 | 49 | "lPinky1" : "lPinky1", 50 | "lPinky2" : "lPinky2", 51 | "lPinky3" : "lPinky3", 52 | 53 | "rThumb1" : "rThumb1", 54 | "rThumb2" : "rThumb2", 55 | "rThumb3" : "rThumb3", 56 | 57 | "rIndex1" : "rIndex1", 58 | "rIndex2" : "rIndex2", 59 | "rIndex3" : "rIndex3", 60 | 61 | "rMid1" : "rMid1", 62 | "rMid2" : "rMid2", 63 | "rMid3" : "rMid3", 64 | 65 | "rRing1" : "rRing1", 66 | "rRing2" : "rRing2", 67 | "rRing3" : "rRing3", 68 | 69 | "rPinky1" : "rPinky1", 70 | "rPinky2" : "rPinky2", 71 | "rPinky3" : "rPinky3" 72 | }, 73 | 74 | "ik-bones" : [] 75 | } 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /target_rigs/mh-official.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "MH Official", 3 | "url" : "www.makehuman.org", 4 | 5 | "bones" : { 6 | "root" : "hips", 7 | "spine4" : "spine", 8 | "spine3" : "spine-1", 9 | "spine2" : "chest", 10 | "neck" : "neck", 11 | "head" : "head", 12 | 13 | "clavicle.L" : "shoulder.L", 14 | "shoulder02.L" : "upper_arm.L", 15 | "lowerarm01.L" : "forearm.L", 16 | "wrist.L" : "hand.L", 17 | 18 | "clavicle.R" : "shoulder.R", 19 | "shoulder02.R" : "upper_arm.R", 20 | "lowerarm01.R" : "forearm.R", 21 | "wrist.R" : "hand.R", 22 | 23 | "upperleg01.L" : "thigh.L", 24 | "lowerleg01.L" : "shin.L", 25 | "foot.L" : "foot.L", 26 | "toe1-1.L" : "toe.L", 27 | "toe2-1.L" : "toe.L", 28 | "toe3-1.L" : "toe.L", 29 | "toe4-1.L" : "toe.L", 30 | "toe5-1.L" : "toe.L", 31 | 32 | "upperleg01.R" : "thigh.R", 33 | "lowerleg01.R" : "shin.R", 34 | "foot.R" : "foot.R", 35 | "toe1-1.R" : "toe.R", 36 | "toe2-1.R" : "toe.R", 37 | "toe3-1.R" : "toe.R", 38 | "toe4-1.R" : "toe.R", 39 | "toe5-1.R" : "toe.R" 40 | }, 41 | 42 | "ik-bones" : [], 43 | 44 | "t-pose": [ 45 | [ 46 | "thigh.R", 47 | [0.99778, 0.039374, 0.0021162, -0.053626] 48 | ], 49 | [ 50 | "thigh.L", 51 | [0.99778, 0.039374, -0.0021162, 0.053626] 52 | ], 53 | [ 54 | "shoulder.L", 55 | [0.99905, 0.041293, -0.012932, 0.0055036] 56 | ], 57 | [ 58 | "upper_arm.L", 59 | [0.92388, 0.01805, 0.0012752, 0.38226] 60 | ], 61 | [ 62 | "forearm.L", 63 | [0.93969, -0.34144, 0.019455, 0.0038203] 64 | ], 65 | [ 66 | "shoulder.R", 67 | [0.99905, 0.041293, 0.012932, -0.0055035] 68 | ], 69 | [ 70 | "upper_arm.R", 71 | [0.92388, 0.021653, 0.0069787, -0.38201] 72 | ], 73 | [ 74 | "forearm.R", 75 | [0.93969, -0.34132, -0.020894, -0.0065809] 76 | ] 77 | ] 78 | } 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /buttons27.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | from bpy.props import * 33 | from bpy_extras.io_utils import ImportHelper, ExportHelper 34 | 35 | 36 | class SaveTargetExport(ExportHelper): 37 | filename_ext = ".trg" 38 | filter_glob = StringProperty(default="*.trg", options={'HIDDEN'}) 39 | filepath = StringProperty(name="File Path", description="Filepath to target file", maxlen=1024, default="") 40 | 41 | 42 | class LoadBVH(ImportHelper): 43 | filename_ext = ".bvh" 44 | filter_glob = StringProperty(default="*.bvh", options={'HIDDEN'}) 45 | filepath = StringProperty(name="File Path", description="Filepath used for importing the BVH file", maxlen=1024, default="") 46 | 47 | 48 | class LoadJson(ImportHelper): 49 | filename_ext = ".json" 50 | filter_glob = StringProperty(default="*.json", options={'HIDDEN'}) 51 | filepath = StringProperty(name="File Path", description="Filepath to tpose file", maxlen=1024, default="") 52 | 53 | 54 | class ProblemsString: 55 | problems = "" 56 | 57 | 58 | class TypeString: 59 | type = StringProperty() 60 | 61 | 62 | class AnswerString: 63 | answer = StringProperty() 64 | 65 | 66 | class PropString: 67 | prop = StringProperty() 68 | 69 | 70 | class LocRotDel: 71 | loc = BoolProperty("Loc", default=False) 72 | rot = BoolProperty("Rot", default=False) 73 | delete = BoolProperty("Del", default=False) 74 | 75 | 76 | class LeftLast: 77 | left = BoolProperty("Loc", default=False) 78 | last = BoolProperty("Rot", default=False) 79 | 80 | -------------------------------------------------------------------------------- /buttons28.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | from bpy.props import * 33 | from bpy_extras.io_utils import ImportHelper, ExportHelper 34 | 35 | 36 | class SaveTargetExport(ExportHelper): 37 | filename_ext = ".trg" 38 | filter_glob : StringProperty(default="*.trg", options={'HIDDEN'}) 39 | filepath : StringProperty(name="File Path", description="Filepath to target file", maxlen=1024, default="") 40 | 41 | 42 | class LoadBVH(ImportHelper): 43 | filename_ext = ".bvh" 44 | filter_glob : StringProperty(default="*.bvh", options={'HIDDEN'}) 45 | filepath : StringProperty(name="File Path", description="Filepath used for importing the BVH file", maxlen=1024, default="") 46 | 47 | 48 | class LoadJson(ImportHelper): 49 | filename_ext = ".json" 50 | filter_glob : StringProperty(default="*.json", options={'HIDDEN'}) 51 | filepath : StringProperty(name="File Path", description="Filepath to tpose file", maxlen=1024, default="") 52 | 53 | 54 | class ProblemsString: 55 | problems = "" 56 | 57 | 58 | class TypeString: 59 | type : StringProperty() 60 | 61 | 62 | class AnswerString: 63 | answer : StringProperty() 64 | 65 | 66 | class PropString: 67 | prop : StringProperty() 68 | 69 | 70 | class LocRotDel: 71 | loc : BoolProperty("Loc", default=False) 72 | rot : BoolProperty("Rot", default=False) 73 | delete : BoolProperty("Del", default=False) 74 | 75 | 76 | class LeftLast: 77 | left : BoolProperty("Loc", default=False) 78 | last : BoolProperty("Rot", default=False) 79 | 80 | -------------------------------------------------------------------------------- /io_json.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import json 32 | import gzip 33 | 34 | def loadJson(filepath): 35 | try: 36 | with gzip.open(filepath, 'rb') as fp: 37 | bytes = fp.read() 38 | except IOError: 39 | bytes = None 40 | 41 | if bytes: 42 | string = bytes.decode("utf-8") 43 | struct = json.loads(string) 44 | else: 45 | with open(filepath, "r") as fp: 46 | struct = json.load(fp) 47 | 48 | return struct 49 | 50 | 51 | def saveJson(struct, filepath, binary=False): 52 | if binary: 53 | bytes = json.dumps(struct) 54 | with gzip.open(realpath, 'wb') as fp: 55 | fp.write(bytes) 56 | else: 57 | string = encodeJsonData(struct, "") 58 | with open(filepath, "w", encoding="utf-8") as fp: 59 | fp.write(string) 60 | fp.write("\n") 61 | 62 | 63 | def encodeJsonData(data, pad=""): 64 | if data == None: 65 | return "none" 66 | elif isinstance(data, bool): 67 | if data == True: 68 | return "true" 69 | else: 70 | return "false" 71 | elif isinstance(data, float): 72 | if abs(data) < 1e-6: 73 | return "0" 74 | else: 75 | return "%.5g" % data 76 | elif isinstance(data, int): 77 | return str(data) 78 | elif isinstance(data, str): 79 | return "\"%s\"" % data 80 | elif isinstance(data, (list, tuple)): 81 | if data == []: 82 | return "[]" 83 | elif leafList(data): 84 | string = "[" 85 | for elt in data: 86 | string += encodeJsonData(elt) + ", " 87 | return string[:-2] + "]" 88 | else: 89 | string = "[" 90 | for elt in data: 91 | string += "\n " + pad + encodeJsonData(elt, pad+" ") + "," 92 | return string[:-1] + "\n%s]" % pad 93 | elif isinstance(data, dict): 94 | if data == {}: 95 | return "{}" 96 | string = "{" 97 | for key,value in data.items(): 98 | string += "\n %s\"%s\" : " % (pad, key) + encodeJsonData(value, pad+" ") + "," 99 | return string[:-1] + "\n%s}" % pad 100 | 101 | 102 | def leafList(data): 103 | for elt in data: 104 | if isinstance(elt, (list,tuple,dict)): 105 | return False 106 | return True 107 | -------------------------------------------------------------------------------- /action.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | import bpy 30 | from bpy.props import EnumProperty, StringProperty 31 | 32 | from . import utils 33 | from .utils import * 34 | if bpy.app.version < (2,80,0): 35 | from .buttons27 import PropString 36 | else: 37 | from .buttons28 import PropString 38 | 39 | # 40 | # Global variables 41 | # 42 | 43 | _actions = [] 44 | 45 | # 46 | # Select or delete action 47 | # Delete button really deletes action. Handle with care. 48 | # 49 | # listAllActions(context): 50 | # findActionNumber(name): 51 | # class MCP_OT_UpdateActionList(bpy.types.Operator): 52 | # 53 | 54 | def listAllActions(context): 55 | global _actions 56 | 57 | scn = context.scene 58 | try: 59 | doFilter = scn.McpFilterActions 60 | filter = context.object.name 61 | if len(filter) > 4: 62 | filter = filter[0:4] 63 | flen = 4 64 | else: 65 | flen = len(filter) 66 | except: 67 | doFilter = False 68 | 69 | _actions = [] 70 | for act in bpy.data.actions: 71 | name = act.name 72 | if (not doFilter) or (name[0:flen] == filter): 73 | _actions.append((name, name, name)) 74 | bpy.types.Scene.McpActions = EnumProperty( 75 | items = _actions, 76 | name = "Actions") 77 | bpy.types.Scene.McpFirstAction = EnumProperty( 78 | items = _actions, 79 | name = "First action") 80 | bpy.types.Scene.McpSecondAction = EnumProperty( 81 | items = _actions, 82 | name = "Second action") 83 | print("Actions declared") 84 | 85 | 86 | def findActionNumber(name): 87 | global _actions 88 | for n,enum in enumerate(_actions): 89 | (name1, name2, name3) = enum 90 | if name == name1: 91 | return n 92 | raise MocapError("Unrecognized action %s" % name) 93 | 94 | 95 | class MCP_OT_UpdateActionList(bpy.types.Operator): 96 | bl_idname = "mcp.update_action_list" 97 | bl_label = "Update Action List" 98 | bl_description = "Update the action list" 99 | bl_options = {'UNDO'} 100 | 101 | @classmethod 102 | def poll(cls, context): 103 | return context.object 104 | 105 | def execute(self, context): 106 | listAllActions(context) 107 | return{'FINISHED'} 108 | 109 | # 110 | # deleteAction(context): 111 | # class MCP_OT_Delete(bpy.types.Operator): 112 | # 113 | 114 | def deleteAction(context): 115 | global _actions 116 | listAllActions(context) 117 | scn = context.scene 118 | try: 119 | act = bpy.data.actions[scn.McpActions] 120 | except KeyError: 121 | act = None 122 | if not act: 123 | raise MocapError("Did not find action %s" % scn.McpActions) 124 | print('Delete action', act) 125 | act.use_fake_user = False 126 | if act.users == 0: 127 | print("Deleting", act) 128 | n = findActionNumber(act.name) 129 | _actions.pop(n) 130 | bpy.data.actions.remove(act) 131 | print('Action', act, 'deleted') 132 | listAllActions(context) 133 | #del act 134 | else: 135 | raise MocapError("Cannot delete. Action %s has %d users." % (act.name, act.users)) 136 | 137 | 138 | class MCP_OT_Delete(bpy.types.Operator): 139 | bl_idname = "mcp.delete" 140 | bl_label = "Delete Action" 141 | bl_description = "Delete the action selected in the action list" 142 | bl_options = {'UNDO'} 143 | 144 | def execute(self, context): 145 | try: 146 | deleteAction(context) 147 | except MocapError: 148 | bpy.ops.mcp.error('INVOKE_DEFAULT') 149 | return{'FINISHED'} 150 | 151 | def invoke(self, context, event): 152 | wm = context.window_manager 153 | return wm.invoke_props_dialog(self, width=200, height=20) 154 | 155 | def draw(self, context): 156 | self.layout.label(text="Really delete action?") 157 | 158 | # 159 | # deleteHash(): 160 | # class MCP_OT_DeleteHash(bpy.types.Operator): 161 | # 162 | 163 | def deleteHash(): 164 | for act in bpy.data.actions: 165 | if act.name[0] == '#': 166 | deleteAction(act) 167 | return 168 | 169 | 170 | class MCP_OT_DeleteHash(bpy.types.Operator): 171 | bl_idname = "mcp.delete_hash" 172 | bl_label = "Delete Temporary Actions" 173 | bl_description = ( 174 | "Delete all actions whose name start with '#'. " + 175 | "Such actions are created temporarily by MakeWalk. " + 176 | "They should be deleted automatically but may be left over." 177 | ) 178 | bl_options = {'UNDO'} 179 | 180 | def execute(self, context): 181 | try: 182 | deleteHash() 183 | except MocapError: 184 | bpy.ops.mcp.error('INVOKE_DEFAULT') 185 | return{'FINISHED'} 186 | 187 | # 188 | # setCurrentAction(context, prop): 189 | # class MCP_OT_SetCurrentAction(bpy.types.Operator): 190 | # 191 | 192 | def setCurrentAction(context, prop): 193 | listAllActions(context) 194 | name = getattr(context.scene, prop) 195 | act = getAction(name) 196 | context.object.animation_data.action = act 197 | print("Action set to %s" % act) 198 | return 199 | 200 | 201 | def getAction(name): 202 | try: 203 | return bpy.data.actions[name] 204 | except KeyError: 205 | pass 206 | raise MocapError("Did not find action %s" % name) 207 | 208 | 209 | class MCP_OT_SetCurrentAction(bpy.types.Operator, PropString): 210 | bl_idname = "mcp.set_current_action" 211 | bl_label = "Set Current Action" 212 | bl_description = "Set the action selected in the action list as the current action" 213 | bl_options = {'UNDO'} 214 | 215 | def execute(self, context): 216 | try: 217 | setCurrentAction(context, self.prop) 218 | except MocapError: 219 | bpy.ops.mcp.error('INVOKE_DEFAULT') 220 | return{'FINISHED'} 221 | 222 | #---------------------------------------------------------- 223 | # Initialize 224 | #---------------------------------------------------------- 225 | 226 | classes = [ 227 | MCP_OT_UpdateActionList, 228 | MCP_OT_Delete, 229 | MCP_OT_DeleteHash, 230 | MCP_OT_SetCurrentAction, 231 | ] 232 | 233 | def initialize(): 234 | for cls in classes: 235 | bpy.utils.register_class(cls) 236 | 237 | 238 | def uninitialize(): 239 | for cls in classes: 240 | bpy.utils.unregister_class(cls) 241 | -------------------------------------------------------------------------------- /source.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | import os 33 | from collections import OrderedDict 34 | from math import pi 35 | from mathutils import * 36 | from bpy.props import * 37 | 38 | from .armature import CArmature 39 | from .utils import * 40 | 41 | # 42 | # Global variables 43 | # 44 | 45 | #_sourceArmatures = { "Automatic" : None } 46 | _sourceArmatures = {} 47 | _srcArmature = None 48 | 49 | def getSourceArmature(name): 50 | global _sourceArmatures 51 | return _sourceArmatures[name] 52 | 53 | def getSourceBoneName(bname): 54 | global _srcArmature 55 | lname = canonicalName(bname) 56 | try: 57 | return _srcArmature.boneNames[lname] 58 | except KeyError: 59 | return None 60 | 61 | def getSourceTPoseFile(): 62 | global _srcArmature 63 | return _srcArmature.tposeFile 64 | 65 | def isSourceInited(scn): 66 | global _sourceArmatures 67 | return (_sourceArmatures != {}) 68 | 69 | def ensureSourceInited(scn): 70 | if not isSourceInited(scn): 71 | initSources(scn) 72 | 73 | # 74 | # guessSrcArmatureFromList(rig, scn): 75 | # 76 | 77 | def guessSrcArmatureFromList(rig, scn): 78 | ensureSourceInited(scn) 79 | bestMisses = 1000 80 | 81 | misses = {} 82 | for name in _sourceArmatures.keys(): 83 | if name == "Automatic": 84 | continue 85 | amt = _sourceArmatures[name] 86 | nMisses = 0 87 | for bone in rig.data.bones: 88 | try: 89 | amt.boneNames[canonicalName(bone.name)] 90 | except KeyError: 91 | nMisses += 1 92 | misses[name] = nMisses 93 | if nMisses < bestMisses: 94 | best = amt 95 | bestMisses = nMisses 96 | 97 | if bestMisses == 0: 98 | scn.McpSourceRig = best.name 99 | return best 100 | else: 101 | print("Number of misses:") 102 | for (name, n) in misses.items(): 103 | print(" %14s: %2d" % (name, n)) 104 | print("Best bone map for armature %s:" % best.name) 105 | amt = _sourceArmatures[best.name] 106 | for bone in rig.data.bones: 107 | try: 108 | bname = amt.boneNames[canonicalName(bone.name)] 109 | string = " " 110 | except KeyError: 111 | string = " *** " 112 | bname = "?" 113 | print("%s %14s => %s" % (string, bone.name, bname)) 114 | raise MocapError('Did not find matching armature. nMisses = %d' % bestMisses) 115 | 116 | # 117 | # findSrcArmature(context, rig): 118 | # 119 | 120 | def findSrcArmature(context, rig): 121 | global _srcArmature, _sourceArmatures 122 | from . import t_pose 123 | scn = context.scene 124 | 125 | setCategory("Identify Source Rig") 126 | ensureSourceInited(scn) 127 | if not scn.McpAutoSourceRig: 128 | _srcArmature = _sourceArmatures[scn.McpSourceRig] 129 | else: 130 | amt = _srcArmature = CArmature() 131 | putInRestPose(rig, True) 132 | amt.findArmature(rig) 133 | t_pose.autoTPose(rig, context) 134 | t_pose.defineTPose(rig) 135 | _sourceArmatures["Automatic"] = amt 136 | amt.display("Source") 137 | 138 | rig.McpArmature = _srcArmature.name 139 | print("Using matching armature %s." % rig.McpArmature) 140 | clearCategory() 141 | 142 | # 143 | # setArmature(rig, scn) 144 | # 145 | 146 | def setArmature(rig, scn): 147 | global _srcArmature, _sourceArmatures 148 | try: 149 | name = rig.McpArmature 150 | except: 151 | name = scn.McpSourceRig 152 | 153 | if name: 154 | rig.McpArmature = name 155 | scn.McpSourceRig = name 156 | else: 157 | raise MocapError("No source armature set") 158 | _srcArmature = _sourceArmatures[name] 159 | print("Set source armature to %s" % name) 160 | return 161 | 162 | # 163 | # findSourceKey(mhx, struct): 164 | # 165 | 166 | def findSourceKey(bname, struct): 167 | for bone in struct.keys(): 168 | if bname == struct[bone]: 169 | return bone 170 | return None 171 | 172 | 173 | ############################################################################### 174 | # 175 | # Source initialization 176 | # 177 | ############################################################################### 178 | 179 | 180 | def initSources(scn): 181 | global _sourceArmatures, _srcArmatureEnums 182 | 183 | _sourceArmatures = { "Automatic" : CArmature() } 184 | path = os.path.join(os.path.dirname(__file__), "source_rigs") 185 | for fname in os.listdir(path): 186 | file = os.path.join(path, fname) 187 | (name, ext) = os.path.splitext(fname) 188 | if ext == ".json" and os.path.isfile(file): 189 | armature = readSrcArmature(file, name) 190 | _sourceArmatures[armature.name] = armature 191 | _srcArmatureEnums = [("Automatic", "Automatic", "Automatic")] 192 | keys = list(_sourceArmatures.keys()) 193 | keys.sort() 194 | for key in keys: 195 | _srcArmatureEnums.append((key,key,key)) 196 | 197 | bpy.types.Scene.McpSourceRig = EnumProperty( 198 | items = _srcArmatureEnums, 199 | name = "Source rig", 200 | default = 'Automatic') 201 | scn.McpSourceRig = 'Automatic' 202 | print("Defined McpSourceRig") 203 | 204 | 205 | def readSrcArmature(filepath, name): 206 | import json 207 | print("Read source file", filepath) 208 | with open(filepath, "r") as fp: 209 | struct = json.load(fp) 210 | armature = CArmature() 211 | armature.name = struct["name"] 212 | tpose = struct["t-pose"] 213 | if isinstance(tpose, str): 214 | armature.tposeFile = tpose 215 | else: 216 | armature.tposeFile = filepath 217 | bones = armature.boneNames 218 | for key,value in struct["armature"].items(): 219 | bones[canonicalName(key)] = nameOrNone(value) 220 | return armature 221 | 222 | 223 | class MCP_OT_InitSources(bpy.types.Operator): 224 | bl_idname = "mcp.init_sources" 225 | bl_label = "Init Source Panel" 226 | bl_options = {'UNDO'} 227 | 228 | def execute(self, context): 229 | initSources(context.scene) 230 | return{'FINISHED'} 231 | 232 | #---------------------------------------------------------- 233 | # Initialize 234 | #---------------------------------------------------------- 235 | 236 | classes = [ 237 | MCP_OT_InitSources, 238 | ] 239 | 240 | def initialize(): 241 | for cls in classes: 242 | bpy.utils.register_class(cls) 243 | 244 | 245 | def uninitialize(): 246 | for cls in classes: 247 | bpy.utils.unregister_class(cls) 248 | -------------------------------------------------------------------------------- /simplify.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | from math import pi 33 | from . import utils 34 | from .utils import * 35 | 36 | # 37 | # simplifyFCurves(context, rig, useVisible, useMarkers): 38 | # 39 | 40 | def simplifyFCurves(context, rig, useVisible, useMarkers): 41 | scn = context.scene 42 | act = getAction(rig) 43 | if not act: 44 | return 45 | (fcurves, minTime, maxTime) = getActionFCurves(act, useVisible, useMarkers, scn) 46 | if not fcurves: 47 | return 48 | 49 | for fcu in fcurves: 50 | simplifyFCurve(fcu, rig.animation_data.action, scn.McpErrorLoc, scn.McpErrorRot, minTime, maxTime) 51 | setInterpolation(rig) 52 | print("Curves simplified") 53 | return 54 | 55 | # 56 | # getActionFCurves(act, useVisible, useMarkers, scn): 57 | # 58 | 59 | def getActionFCurves(act, useVisible, useMarkers, scn): 60 | if useVisible: 61 | fcurves = [] 62 | for fcu in act.fcurves: 63 | if not fcu.hide: 64 | fcurves.append(fcu) 65 | #print(fcu.data_path, fcu.array_index) 66 | else: 67 | fcurves = act.fcurves 68 | 69 | if useMarkers: 70 | (minTime, maxTime) = getMarkedTime(scn) 71 | if minTime == None: 72 | print("Need two selected markers") 73 | return ([], 0, 0) 74 | else: 75 | (minTime, maxTime) = ('All', 0) 76 | return (fcurves, minTime, maxTime) 77 | 78 | # 79 | # splitFCurvePoints(fcu, minTime, maxTime): 80 | # 81 | 82 | def splitFCurvePoints(fcu, minTime, maxTime): 83 | if minTime == 'All': 84 | points = fcu.keyframe_points 85 | before = [] 86 | after = [] 87 | else: 88 | points = [] 89 | before = [] 90 | after = [] 91 | for pt in fcu.keyframe_points: 92 | t = pt.co[0] 93 | if t < minTime: 94 | before.append(pt.co) 95 | elif t > maxTime: 96 | after.append(pt.co) 97 | else: 98 | points.append(pt) 99 | return (points, before, after) 100 | 101 | # 102 | # simplifyFCurve(fcu, act, maxErrLoc, maxErrRot, minTime, maxTime): 103 | # 104 | 105 | def simplifyFCurve(fcu, act, maxErrLoc, maxErrRot, minTime, maxTime): 106 | #print("WARNING: F-curve simplification turned off") 107 | #return 108 | words = fcu.data_path.split('.') 109 | if words[-1] == 'location': 110 | maxErr = maxErrLoc 111 | elif words[-1] == 'rotation_quaternion': 112 | maxErr = maxErrRot * 1.0/180 113 | elif words[-1] == 'rotation_euler': 114 | maxErr = maxErrRot * pi/180 115 | else: 116 | raise MocapError("Unknown FCurve type %s" % words[-1]) 117 | 118 | (points, before, after) = splitFCurvePoints(fcu, minTime, maxTime) 119 | 120 | nPoints = len(points) 121 | nBefore = len(before) 122 | nAfter = len(after) 123 | if nPoints <= 2: 124 | return 125 | keeps = [] 126 | new = [0, nPoints-1] 127 | while new: 128 | keeps += new 129 | keeps.sort() 130 | new = iterateFCurves(points, keeps, maxErr) 131 | newVerts = [] 132 | for n in keeps: 133 | newVerts.append(points[n].co.copy()) 134 | nNewPoints = len(newVerts) 135 | 136 | oldOffset = nBefore+nPoints 137 | newOffset = nBefore+nNewPoints 138 | for n in range(nAfter): 139 | fcu.keyframe_points[n+newOffset].co = fcu.keyframe_points[n+oldOffset].co.copy() 140 | n = nBefore+nPoints+nAfter 141 | n1 = nBefore+nNewPoints+nAfter 142 | while n > n1: 143 | n -= 1 144 | kp = fcu.keyframe_points[n] 145 | fcu.keyframe_points.remove(kp) 146 | for n in range(nNewPoints): 147 | fcu.keyframe_points[n+nBefore].co = newVerts[n] 148 | return 149 | 150 | # 151 | # iterateFCurves(points, keeps, maxErr): 152 | # 153 | 154 | def iterateFCurves(points, keeps, maxErr): 155 | new = [] 156 | for edge in range(len(keeps)-1): 157 | n0 = keeps[edge] 158 | n1 = keeps[edge+1] 159 | (x0, y0) = points[n0].co 160 | (x1, y1) = points[n1].co 161 | if x1 > x0: 162 | dxdn = (x1-x0)/(n1-n0) 163 | dydx = (y1-y0)/(x1-x0) 164 | err = 0 165 | for n in range(n0+1, n1): 166 | (x, y) = points[n].co 167 | xn = n0 + dxdn*(n-n0) 168 | yn = y0 + dydx*(xn-x0) 169 | if abs(y-yn) > err: 170 | err = abs(y-yn) 171 | worst = n 172 | if err > maxErr: 173 | new.append(worst) 174 | return new 175 | 176 | # 177 | # rescaleFCurves(context, rig, factor): 178 | # 179 | 180 | def rescaleFCurves(context, rig, factor): 181 | act = getAction(context.object) 182 | if not act: 183 | return 184 | for fcu in act.fcurves: 185 | rescaleFCurve(fcu, factor) 186 | print("Curves rescaled") 187 | return 188 | 189 | # 190 | # rescaleFCurve(fcu, factor): 191 | # 192 | 193 | def rescaleFCurve(fcu, factor): 194 | n = len(fcu.keyframe_points) 195 | if n < 2: 196 | return 197 | (t0,v0) = fcu.keyframe_points[0].co 198 | (tn,vn) = fcu.keyframe_points[n-1].co 199 | limitData = getFCurveLimits(fcu) 200 | (mode, upper, lower, diff) = limitData 201 | 202 | tm = t0 203 | vm = v0 204 | inserts = [] 205 | for pk in fcu.keyframe_points: 206 | (tk,vk) = pk.co 207 | tn = factor*(tk-t0) + t0 208 | if upper: 209 | if (vk > upper) and (vm < lower): 210 | inserts.append((tm, vm, tn, vk)) 211 | elif (vm > upper) and (vk < lower): 212 | inserts.append((tm, vm, tn,vk)) 213 | pk.co = (tn,vk) 214 | tm = tn 215 | vm = vk 216 | 217 | addFCurveInserts(fcu, inserts, limitData) 218 | return 219 | 220 | # 221 | # getFCurveLimits(fcu): 222 | # 223 | 224 | def getFCurveLimits(fcu): 225 | words = fcu.data_path.split('.') 226 | mode = words[-1] 227 | if mode == 'rotation_euler': 228 | upper = 0.8*pi 229 | lower = -0.8*pi 230 | diff = pi 231 | elif mode == 'rotation_quaternion': 232 | upper = 0.8 233 | lower = -0.8 234 | diff = 2 235 | else: 236 | upper = 0 237 | lower = 0 238 | diff = 0 239 | #print(words[1], mode, upper, lower) 240 | return (mode, upper, lower, diff) 241 | 242 | # 243 | # addFCurveInserts(fcu, inserts, limitData): 244 | # 245 | 246 | def addFCurveInserts(fcu, inserts, limitData): 247 | (mode, upper, lower, diff) = limitData 248 | for (tm,vm,tn,vn) in inserts: 249 | tp = int((tm+tn)/2 - 0.1) 250 | tq = tp + 1 251 | vp = (vm+vn)/2 252 | if vm > upper: 253 | vp += diff/2 254 | vq = vp - diff 255 | elif vm < lower: 256 | vp -= diff/2 257 | vq = vp + diff 258 | if tp > tm: 259 | fcu.keyframe_points.insert(frame=tp, value=vp) 260 | if tq < tn: 261 | fcu.keyframe_points.insert(frame=tq, value=vq) 262 | return 263 | 264 | 265 | ######################################################################## 266 | # 267 | # class MCP_OT_SimplifyFCurves(bpy.types.Operator): 268 | # 269 | 270 | class MCP_OT_SimplifyFCurves(bpy.types.Operator): 271 | bl_idname = "mcp.simplify_fcurves" 272 | bl_label = "Simplify FCurves" 273 | bl_options = {'UNDO'} 274 | 275 | def execute(self, context): 276 | try: 277 | scn = context.scene 278 | simplifyFCurves(context, context.object, scn.McpSimplifyVisible, scn.McpSimplifyMarkers) 279 | except MocapError: 280 | bpy.ops.mcp.error('INVOKE_DEFAULT') 281 | return{'FINISHED'} 282 | 283 | class MCP_OT_RescaleFCurves(bpy.types.Operator): 284 | bl_idname = "mcp.rescale_fcurves" 285 | bl_label = "Rescale FCurves" 286 | bl_options = {'UNDO'} 287 | 288 | def execute(self, context): 289 | try: 290 | scn = context.scene 291 | rescaleFCurves(context, context.object, scn.McpRescaleFactor) 292 | except MocapError: 293 | bpy.ops.mcp.error('INVOKE_DEFAULT') 294 | return{'FINISHED'} 295 | 296 | #---------------------------------------------------------- 297 | # Initialize 298 | #---------------------------------------------------------- 299 | 300 | classes = [ 301 | MCP_OT_SimplifyFCurves, 302 | MCP_OT_RescaleFCurves, 303 | ] 304 | 305 | def initialize(): 306 | for cls in classes: 307 | bpy.utils.register_class(cls) 308 | 309 | 310 | def uninitialize(): 311 | for cls in classes: 312 | bpy.utils.unregister_class(cls) 313 | -------------------------------------------------------------------------------- /target.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | from bpy.props import * 33 | import math 34 | import os 35 | 36 | from . import utils 37 | from . import t_pose 38 | from .utils import * 39 | from .armature import CArmature 40 | if bpy.app.version < (2,80,0): 41 | from .buttons27 import SaveTargetExport 42 | else: 43 | from .buttons28 import SaveTargetExport 44 | 45 | # 46 | # Global variables 47 | # 48 | 49 | _target = None 50 | _targetInfo = {} 51 | #_targetArmatures = { "Automatic" : ([],[],[]) } 52 | _targetArmatures = {} 53 | _trgArmature = None 54 | _trgArmatureEnums =[("Automatic", "Automatic", "Automatic")] 55 | _ikBones = [] 56 | _bendTwist = {} 57 | 58 | def getTargetInfo(rigname): 59 | global _targetInfo 60 | return _targetInfo[rigname] 61 | 62 | def loadTargets(): 63 | global _targetInfo 64 | _targetInfo = {} 65 | 66 | def isTargetInited(scn): 67 | return ( _targetArmatures != {} ) 68 | 69 | def ensureTargetInited(scn): 70 | if not isTargetInited(scn): 71 | initTargets(scn) 72 | 73 | # 74 | # getTargetArmature(rig, context): 75 | # 76 | 77 | def getTargetArmature(rig, context): 78 | global _target, _targetArmatures, _targetInfo, _trgArmature, _ikBones, _bendTwist 79 | 80 | scn = context.scene 81 | setCategory("Identify Target Rig") 82 | ensureTargetInited(scn) 83 | putInRestPose(rig, True) 84 | bones = rig.data.bones.keys() 85 | 86 | if scn.McpAutoTargetRig: 87 | name = guessTargetArmatureFromList(rig, bones, scn) 88 | else: 89 | try: 90 | name = scn.McpTargetRig 91 | except: 92 | raise MocapError("Initialize Target Panel first") 93 | 94 | if name == "Automatic": 95 | setCategory("Automatic Target Rig") 96 | amt = _trgArmature = CArmature() 97 | amt.findArmature(rig, ignoreHiddenLayers=scn.McpIgnoreHiddenLayers) 98 | _targetArmatures["Automatic"] = amt 99 | scn.McpTargetRig = "Automatic" 100 | amt.display("Target") 101 | 102 | boneAssoc = [] 103 | for pb in rig.pose.bones: 104 | if pb.McpBone: 105 | boneAssoc.append( (pb.name, pb.McpBone) ) 106 | 107 | _ikBones = [] 108 | rig.McpTPoseFile = "" 109 | _targetInfo[name] = (boneAssoc, _ikBones, rig.McpTPoseFile, _bendTwist) 110 | clearCategory() 111 | return boneAssoc 112 | 113 | else: 114 | setCategory("Manual Target Rig") 115 | scn.McpTargetRig = name 116 | _target = name 117 | (boneAssoc, _ikBones, rig.McpTPoseFile, _bendTwist) = _targetInfo[name] 118 | if not testTargetRig(name, rig, boneAssoc): 119 | print("WARNING:\nTarget armature %s does not match armature %s.\nBones:" % (rig.name, name)) 120 | for pair in boneAssoc: 121 | print(" %s : %s" % pair) 122 | print("Target armature %s" % name) 123 | 124 | for pb in rig.pose.bones: 125 | pb.McpBone = pb.McpParent = "" 126 | for bname,mhx in boneAssoc: 127 | try: 128 | rig.pose.bones[bname].McpBone = mhx 129 | except KeyError: 130 | print(" ", bname) 131 | pass 132 | 133 | clearCategory() 134 | return boneAssoc 135 | 136 | 137 | 138 | def guessTargetArmatureFromList(rig, bones, scn): 139 | global _target, _targetArmatures, _targetInfo 140 | ensureTargetInited(scn) 141 | print("Guessing target") 142 | 143 | if isMhxRig(rig): 144 | return "MHX" 145 | elif isMhOfficialRig(rig): 146 | return "MH-Official" 147 | elif isRigify(rig): 148 | return "Rigify" 149 | elif isRigify2(rig): 150 | return "Rigify2" 151 | elif isMhx7Rig(rig): 152 | return "MH-alpha7" 153 | elif isGenesis(rig): 154 | return "Genesis" 155 | elif isGenesis3(rig): 156 | return "Genesis3" 157 | elif False: 158 | for name in _targetInfo.keys(): 159 | if name not in ["MHX", "MH-Official", "Rigify", "Rigify2", "MH-alpha7", "Genesis", "Genesis3"]: 160 | (boneAssoc, _ikBones, _tpose, _bendTwist) = _targetInfo[name] 161 | if testTargetRig(name, rig, boneAssoc): 162 | return name 163 | else: 164 | return "Automatic" 165 | 166 | 167 | def testTargetRig(name, rig, rigBones): 168 | from .armature import validBone 169 | print("Testing %s" % name) 170 | for (bname, mhxname) in rigBones: 171 | try: 172 | pb = rig.pose.bones[bname] 173 | except KeyError: 174 | pb = None 175 | if pb is None or not validBone(pb): 176 | print(" Did not find bone %s (%s)" % (bname, mhxname)) 177 | return False 178 | return True 179 | 180 | # 181 | # findTargetKeys(mhx, list): 182 | # 183 | 184 | def findTargetKeys(mhx, list): 185 | bones = [] 186 | for (bone, mhx1) in list: 187 | if mhx1 == mhx: 188 | bones.append(bone) 189 | return bones 190 | 191 | ############################################################################### 192 | # 193 | # Target armatures 194 | # 195 | ############################################################################### 196 | 197 | # (mhx bone, text) 198 | 199 | TargetBoneNames = [ 200 | ('hips', 'Root bone'), 201 | ('spine', 'Lower spine'), 202 | ('spine-1', 'Middle spine'), 203 | ('chest', 'Upper spine'), 204 | ('neck', 'Neck'), 205 | ('head', 'Head'), 206 | None, 207 | ('shoulder.L', 'L shoulder'), 208 | ('upper_arm.L', 'L upper arm'), 209 | ('forearm.L', 'L forearm'), 210 | ('hand.L', 'L hand'), 211 | None, 212 | ('shoulder.R', 'R shoulder'), 213 | ('upper_arm.R', 'R upper arm'), 214 | ('forearm.R', 'R forearm'), 215 | ('hand.R', 'R hand'), 216 | None, 217 | ('hip.L', 'L hip'), 218 | ('thigh.L', 'L thigh'), 219 | ('shin.L', 'L shin'), 220 | ('foot.L', 'L foot'), 221 | ('toe.L', 'L toes'), 222 | None, 223 | ('hip.R', 'R hip'), 224 | ('thigh.R', 'R thigh'), 225 | ('shin.R', 'R shin'), 226 | ('foot.R', 'R foot'), 227 | ('toe.R', 'R toes'), 228 | ] 229 | 230 | ############################################################################### 231 | # 232 | # Target initialization 233 | # 234 | ############################################################################### 235 | 236 | 237 | def initTargets(scn): 238 | global _targetArmatures, _targetInfo, _trgArmatureEnums 239 | _targetInfo = { "Automatic" : ([], [], "", {}) } 240 | _targetArmatures = { "Automatic" : CArmature() } 241 | path = os.path.join(os.path.dirname(__file__), "target_rigs") 242 | for fname in os.listdir(path): 243 | file = os.path.join(path, fname) 244 | (name, ext) = os.path.splitext(fname) 245 | if ext == ".json" and os.path.isfile(file): 246 | (name, stuff) = readTrgArmature(file, name) 247 | _targetInfo[name] = stuff 248 | 249 | _trgArmatureEnums =[("Automatic", "Automatic", "Automatic")] 250 | keys = list(_targetInfo.keys()) 251 | keys.sort() 252 | for key in keys: 253 | _trgArmatureEnums.append((key,key,key)) 254 | 255 | bpy.types.Scene.McpTargetRig = EnumProperty( 256 | items = _trgArmatureEnums, 257 | name = "Target rig", 258 | default = 'Automatic') 259 | print("Defined McpTargetRig") 260 | return 261 | 262 | 263 | def readTrgArmature(filepath, name): 264 | import json 265 | print("Read target file", filepath) 266 | with open(filepath, "r") as fp: 267 | struct = json.load(fp) 268 | name = struct["name"] 269 | bones = [(key, nameOrNone(value)) for key,value in struct["bones"].items()] 270 | ikbones = [] 271 | if "ikbones" in struct.keys(): 272 | ikbones = [(key, nameOrNone(value)) for key,value in struct["ikbones"].items()] 273 | if "t-pose" in struct.keys(): 274 | tpose = filepath 275 | else: 276 | tpose = "" 277 | bendtwist = [] 278 | if "bendtwist" in struct.keys(): 279 | bendtwist = [(key, nameOrNone(value)) for key,value in struct["bendtwist"].items()] 280 | return (name, (bones,ikbones,tpose,bendtwist)) 281 | 282 | 283 | class MCP_OT_InitTargets(bpy.types.Operator): 284 | bl_idname = "mcp.init_targets" 285 | bl_label = "Init Target Panel" 286 | bl_description = "(Re)load all .trg files in the target_rigs directory." 287 | bl_options = {'UNDO'} 288 | 289 | def execute(self, context): 290 | try: 291 | initTargets(context.scene) 292 | except MocapError: 293 | bpy.ops.mcp.error('INVOKE_DEFAULT') 294 | return{'FINISHED'} 295 | 296 | 297 | class MCP_OT_GetTargetRig(bpy.types.Operator): 298 | bl_idname = "mcp.get_target_rig" 299 | bl_label = "Identify Target Rig" 300 | bl_description = "Identify the target rig type of the active armature." 301 | bl_options = {'UNDO'} 302 | 303 | def execute(self, context): 304 | from .retarget import changeTargetData, restoreTargetData 305 | rig = context.object 306 | scn = context.scene 307 | data = changeTargetData(rig, scn) 308 | try: 309 | getTargetArmature(rig, context) 310 | except MocapError: 311 | bpy.ops.mcp.error('INVOKE_DEFAULT') 312 | finally: 313 | restoreTargetData(rig, data) 314 | return{'FINISHED'} 315 | 316 | 317 | def saveTargetFile(filepath, context): 318 | from .t_pose import saveTPose 319 | 320 | rig = context.object 321 | scn = context.scene 322 | fname,ext = os.path.splitext(filepath) 323 | filepath = fname + ".trg" 324 | fp = open(filepath, "w") 325 | name = os.path.basename(fname).capitalize().replace(" ","_") 326 | fp.write("Name:\t%s\n\nBones:\n" % name) 327 | for pb in rig.pose.bones: 328 | if pb.McpBone: 329 | fp.write("\t%s\t%s\n" % (pb.name, pb.McpBone)) 330 | fp.write("\nIkBones:\n\n") 331 | if scn.McpSaveTargetTPose: 332 | fp.write("T-pose:\t%s-tpose.json\n" % name) 333 | fp.close() 334 | print("Saved %s" % filepath) 335 | 336 | if scn.McpSaveTargetTPose: 337 | tposePath = fname + "-tpose.json" 338 | saveTPose(context, tposePath) 339 | 340 | 341 | class MCP_OT_SaveTargetFile(bpy.types.Operator, SaveTargetExport): 342 | bl_idname = "mcp.save_target_file" 343 | bl_label = "Save Target File" 344 | bl_description = "Save a .trg file for this character" 345 | bl_options = {'UNDO'} 346 | 347 | def execute(self, context): 348 | try: 349 | saveTargetFile(self.properties.filepath, context) 350 | except MocapError: 351 | bpy.ops.mcp.error('INVOKE_DEFAULT') 352 | return{'FINISHED'} 353 | 354 | def invoke(self, context, event): 355 | context.window_manager.fileselect_add(self) 356 | return {'RUNNING_MODAL'} 357 | 358 | #---------------------------------------------------------- 359 | # Initialize 360 | #---------------------------------------------------------- 361 | 362 | classes = [ 363 | MCP_OT_InitTargets, 364 | MCP_OT_GetTargetRig, 365 | MCP_OT_SaveTargetFile, 366 | ] 367 | 368 | def initialize(): 369 | for cls in classes: 370 | bpy.utils.register_class(cls) 371 | 372 | 373 | def uninitialize(): 374 | for cls in classes: 375 | bpy.utils.unregister_class(cls) 376 | -------------------------------------------------------------------------------- /floor.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | from bpy.props import BoolProperty 33 | from mathutils import Matrix, Vector 34 | from .utils import * 35 | from . import fkik 36 | 37 | #------------------------------------------------------------- 38 | # Plane 39 | #------------------------------------------------------------- 40 | 41 | def getRigAndPlane(context): 42 | rig = None 43 | plane = None 44 | for ob in getSceneObjects(context): 45 | if getSelected(ob): 46 | if ob.type == 'ARMATURE': 47 | if rig: 48 | raise MocapError("Two armatures selected: %s and %s" % (rig.name, ob.name)) 49 | else: 50 | rig = ob 51 | elif ob.type == 'MESH': 52 | if plane: 53 | raise MocapError("Two meshes selected: %s and %s" % (plane.name, ob.name)) 54 | else: 55 | plane = ob 56 | if rig is None: 57 | raise MocapError("No rig selected") 58 | return rig,plane 59 | 60 | 61 | def getPlaneInfo(plane): 62 | if plane is None: 63 | ez = Vector((0,0,1)) 64 | origin = Vector((0,0,0)) 65 | rot = Matrix() 66 | else: 67 | mat = plane.matrix_world.to_3x3().normalized() 68 | ez = mat.col[2] 69 | origin = plane.location 70 | rot = mat.to_4x4() 71 | return ez,origin,rot 72 | 73 | #------------------------------------------------------------- 74 | # Offset and projection 75 | #------------------------------------------------------------- 76 | 77 | def getProjection(vec, ez): 78 | return ez.dot(Vector(vec[:3])) 79 | 80 | 81 | def getOffset(point, ez, origin): 82 | vec = Vector(point[:3]) - origin 83 | offset = -ez.dot(vec) 84 | return offset 85 | 86 | 87 | def getHeadOffset(pb, ez, origin): 88 | head = pb.matrix.col[3] 89 | return getOffset(head, ez, origin) 90 | 91 | 92 | def getTailOffset(pb, ez, origin): 93 | head = pb.matrix.col[3] 94 | y = pb.matrix.col[1] 95 | tail = head + y*pb.length 96 | return getOffset(tail, ez, origin) 97 | 98 | 99 | def addOffset(pb, offset, ez): 100 | gmat = pb.matrix.copy() 101 | x,y,z = offset*ez 102 | gmat.col[3] += Vector((x,y,z,0)) 103 | pmat = fkik.getPoseMatrix(gmat, pb) 104 | fkik.insertLocation(pb, pmat) 105 | 106 | #------------------------------------------------------------- 107 | # Toe below ball 108 | #------------------------------------------------------------- 109 | 110 | def toesBelowBall(context): 111 | scn = context.scene 112 | rig,plane = getRigAndPlane(context) 113 | try: 114 | useIk = rig["MhaLegIk_L"] or rig["MhaLegIk_R"] 115 | except KeyError: 116 | useIk = False 117 | if useIk: 118 | raise MocapError("Toe Below Ball only for FK feet") 119 | 120 | layers = list(rig.data.layers) 121 | startProgress("Keep toes down") 122 | frames = getActiveFramesBetweenMarkers(rig, scn) 123 | print("Left toe") 124 | toeBelowBall(context, frames, rig, plane, ".L") 125 | print("Right toe") 126 | toeBelowBall(context, frames, rig, plane, ".R") 127 | endProgress("Toes kept down") 128 | rig.data.layers = layers 129 | 130 | 131 | def toeBelowBall(context, frames, rig, plane, suffix): 132 | from .retarget import getLocks 133 | 134 | scn = context.scene 135 | foot,toe,mBall,mToe,mHeel = getFkFeetBones(rig, suffix) 136 | ez,origin,rot = getPlaneInfo(plane) 137 | order,lock = getLocks(toe, context) 138 | factor = 1.0/toe.length 139 | nFrames = len(frames) 140 | if mBall: 141 | for n,frame in enumerate(frames): 142 | scn.frame_set(frame) 143 | showProgress(n, frame, nFrames) 144 | zToe = getProjection(mToe.matrix.col[3], ez) 145 | zBall = getProjection(mBall.matrix.col[3], ez) 146 | if zToe > zBall: 147 | pmat = offsetToeRotation(toe, ez, factor, order, lock, context) 148 | else: 149 | pmat = fkik.getPoseMatrix(toe.matrix, toe) 150 | pmat = keepToeRotationNegative(pmat, scn) 151 | fkik.insertRotation(toe, pmat) 152 | else: 153 | for n,frame in enumerate(frames): 154 | scn.frame_set(frame) 155 | showProgress(n, frame, nFrames) 156 | dzToe = getProjection(toe.matrix.col[1], ez) 157 | if dzToe > 0: 158 | pmat = offsetToeRotation(toe, ez, factor, order, lock, context) 159 | else: 160 | pmat = fkik.getPoseMatrix(toe.matrix, toe) 161 | pmat = keepToeRotationNegative(pmat, scn) 162 | fkik.insertRotation(toe, pmat) 163 | 164 | 165 | def offsetToeRotation(toe, ez, factor, order, lock, context): 166 | from .retarget import correctMatrixForLocks 167 | 168 | mat = toe.matrix.to_3x3() 169 | y = mat.col[1] 170 | y -= ez.dot(y)*ez 171 | y.normalize() 172 | x = mat.col[0] 173 | x -= x.dot(y)*y 174 | x.normalize() 175 | z = x.cross(y) 176 | mat.col[0] = x 177 | mat.col[1] = y 178 | mat.col[2] = z 179 | gmat = mat.to_4x4() 180 | gmat.col[3] = toe.matrix.col[3] 181 | pmat = fkik.getPoseMatrix(gmat, toe) 182 | return correctMatrixForLocks(pmat, order, lock, toe, context.scene.McpUseLimits) 183 | 184 | 185 | def keepToeRotationNegative(pmat, scn): 186 | euler = pmat.to_3x3().to_euler('YZX') 187 | if euler.x > 0: 188 | pmat0 = pmat 189 | euler.x = 0 190 | pmat = euler.to_matrix().to_4x4() 191 | pmat.col[3] = pmat0.col[3] 192 | return pmat 193 | 194 | 195 | class MCP_OT_OffsetToe(bpy.types.Operator): 196 | bl_idname = "mcp.offset_toe" 197 | bl_label = "Offset Toes" 198 | bl_description = "Keep toes below balls" 199 | bl_options = {'UNDO'} 200 | 201 | def execute(self, context): 202 | from .target import getTargetArmature 203 | getTargetArmature(context.object, context) 204 | try: 205 | toesBelowBall(context) 206 | except MocapError: 207 | bpy.ops.mcp.error('INVOKE_DEFAULT') 208 | return{'FINISHED'} 209 | 210 | 211 | #------------------------------------------------------------- 212 | # Floor 213 | #------------------------------------------------------------- 214 | 215 | def floorFoot(context): 216 | startProgress("Keep feet above floor") 217 | scn = context.scene 218 | rig,plane = getRigAndPlane(context) 219 | try: 220 | useIk = rig["MhaLegIk_L"] or rig["MhaLegIk_R"] 221 | except KeyError: 222 | useIk = False 223 | frames = getActiveFramesBetweenMarkers(rig, scn) 224 | if useIk: 225 | floorIkFoot(rig, plane, scn, frames) 226 | else: 227 | floorFkFoot(rig, plane, scn, frames) 228 | endProgress("Feet kept above floor") 229 | 230 | 231 | def getFkFeetBones(rig, suffix): 232 | foot = getTrgBone("foot" + suffix, rig) 233 | toe = getTrgBone("toe" + suffix, rig) 234 | try: 235 | mBall = rig.pose.bones["ball.marker" + suffix] 236 | mToe = rig.pose.bones["toe.marker" + suffix] 237 | mHeel = rig.pose.bones["heel.marker" + suffix] 238 | except KeyError: 239 | mBall = mToe = mHeel = None 240 | return foot,toe,mBall,mToe,mHeel 241 | 242 | 243 | def floorFkFoot(rig, plane, scn, frames): 244 | hips = getTrgBone("hips", rig) 245 | lFoot,lToe,lmBall,lmToe,lmHeel = getFkFeetBones(rig, ".L") 246 | rFoot,rToe,rmBall,rmToe,rmHeel = getFkFeetBones(rig, ".R") 247 | ez,origin,rot = getPlaneInfo(plane) 248 | 249 | nFrames = len(frames) 250 | for n,frame in enumerate(frames): 251 | scn.frame_set(frame) 252 | fkik.updateScene() 253 | offset = 0 254 | if scn.McpFloorLeft: 255 | offset = getFkOffset(rig, ez, origin, lFoot, lToe, lmBall, lmToe, lmHeel) 256 | if scn.McpFloorRight: 257 | rOffset = getFkOffset(rig, ez, origin, rFoot, rToe, rmBall, rmToe, rmHeel) 258 | if rOffset > offset: 259 | offset = rOffset 260 | showProgress(n, frame, nFrames) 261 | if offset > 0: 262 | addOffset(hips, offset, ez) 263 | 264 | 265 | def getFkOffset(rig, ez, origin, foot, toe, mBall, mToe, mHeel): 266 | if mBall: 267 | offset = toeOffset = getHeadOffset(mToe, ez, origin) 268 | ballOffset = getHeadOffset(mBall, ez, origin) 269 | if ballOffset > offset: 270 | offset = ballOffset 271 | heelOffset = getHeadOffset(mHeel, ez, origin) 272 | if heelOffset > offset: 273 | offset = heelOffset 274 | elif toe: 275 | offset = getTailOffset(toe, ez, origin) 276 | ballOffset = getHeadOffset(toe, ez, origin) 277 | if ballOffset > offset: 278 | offset = ballOffset 279 | ball = toe.matrix.col[3] 280 | y = toe.matrix.col[1] 281 | heel = ball - y*foot.length 282 | heelOffset = getOffset(heel, ez, origin) 283 | if heelOffset > offset: 284 | offset = heelOffset 285 | else: 286 | offset = 0 287 | 288 | return offset 289 | 290 | 291 | def floorIkFoot(rig, plane, scn, frames): 292 | root = rig.pose.bones["root"] 293 | lleg = rig.pose.bones["foot.ik.L"] 294 | rleg = rig.pose.bones["foot.ik.R"] 295 | ez,origin,rot = getPlaneInfo(plane) 296 | 297 | fillKeyFrames(lleg, rig, frames, 3, mode='location') 298 | fillKeyFrames(rleg, rig, frames, 3, mode='location') 299 | if scn.McpFloorHips: 300 | fillKeyFrames(root, rig, frames, 3, mode='location') 301 | 302 | nFrames = len(frames) 303 | for n,frame in enumerate(frames): 304 | scn.frame_set(frame) 305 | showProgress(n, frame, nFrames) 306 | 307 | if scn.McpFloorLeft: 308 | lOffset = getIkOffset(rig, ez, origin, lleg) 309 | if lOffset > 0: 310 | addOffset(lleg, lOffset, ez) 311 | else: 312 | lOffset = 0 313 | if scn.McpFloorRight: 314 | rOffset = getIkOffset(rig, ez, origin, rleg) 315 | if rOffset > 0: 316 | addOffset(rleg, rOffset, ez) 317 | else: 318 | rOffset = 0 319 | 320 | hOffset = min(lOffset,rOffset) 321 | if hOffset > 0 and scn.McpFloorHips: 322 | addOffset(root, hOffset, ez) 323 | 324 | 325 | def getIkOffset(rig, ez, origin, leg): 326 | offset = getHeadOffset(leg, ez, origin) 327 | tailOffset = getTailOffset(leg, ez, origin) 328 | if tailOffset > offset: 329 | offset = tailOffset 330 | return offset 331 | 332 | 333 | foot = rig.pose.bones["foot.rev" + suffix] 334 | toe = rig.pose.bones["toe.rev" + suffix] 335 | 336 | 337 | ballOffset = getTailOffset(toe, ez, origin) 338 | if ballOffset > offset: 339 | offset = ballOffset 340 | 341 | ball = foot.matrix.col[3] 342 | y = toe.matrix.col[1] 343 | heel = ball + y*foot.length 344 | heelOffset = getOffset(heel, ez, origin) 345 | if heelOffset > offset: 346 | offset = heelOffset 347 | 348 | return offset 349 | 350 | 351 | class MCP_OT_FloorFoot(bpy.types.Operator): 352 | bl_idname = "mcp.floor_foot" 353 | bl_label = "Keep Feet Above Floor" 354 | bl_description = "Keep Feet Above Plane" 355 | bl_options = {'UNDO'} 356 | 357 | def execute(self, context): 358 | from .target import getTargetArmature 359 | getTargetArmature(context.object, context) 360 | try: 361 | floorFoot(context) 362 | except MocapError: 363 | bpy.ops.mcp.error('INVOKE_DEFAULT') 364 | return{'FINISHED'} 365 | 366 | #---------------------------------------------------------- 367 | # Initialize 368 | #---------------------------------------------------------- 369 | 370 | classes = [ 371 | MCP_OT_OffsetToe, 372 | MCP_OT_FloorFoot, 373 | ] 374 | 375 | def initialize(): 376 | for cls in classes: 377 | bpy.utils.register_class(cls) 378 | 379 | 380 | def uninitialize(): 381 | for cls in classes: 382 | bpy.utils.unregister_class(cls) 383 | -------------------------------------------------------------------------------- /armature.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | import bpy 31 | import os 32 | from collections import OrderedDict 33 | from math import pi 34 | from mathutils import * 35 | from bpy.props import * 36 | 37 | from .utils import * 38 | 39 | 40 | class CArmature: 41 | 42 | def __init__(self): 43 | self.name = "Automatic" 44 | self.boneNames = OrderedDict() 45 | self.tposeFile = None 46 | self.rig = None 47 | 48 | 49 | def display(self, type): 50 | print("%s Armature: %s" % (type, self.name)) 51 | for bname,mhx in self.boneNames.items(): 52 | print(" %14s %14s" % (bname, mhx)) 53 | 54 | 55 | def findArmature(self, rig, ignoreHiddenLayers=True): 56 | if ignoreHiddenLayers: 57 | self.rig = rig 58 | else: 59 | self.rig = None 60 | 61 | roots = [] 62 | for pb in rig.pose.bones: 63 | if pb.parent is None: 64 | roots.append(pb) 65 | 66 | hipsChildren = roots 67 | nChildren = len(roots) 68 | first = True 69 | hips = None 70 | while first or nChildren != 3: 71 | if not first and nChildren == 2 and rig.MhReverseHip: 72 | break 73 | elif nChildren == 0: 74 | raise MocapError("Hip bone must have children: %s" % hips.name) 75 | elif nChildren == 1: 76 | hips = hipsChildren[0] 77 | hipsChildren = self.validChildren(hips) 78 | nChildren = len(hipsChildren) 79 | elif nChildren == 2: 80 | counts = self.getChildCount(hipsChildren) 81 | #for m,n,pb in counts: 82 | # print(m,n,pb.name) 83 | hips = counts[-1][2] 84 | hipsChildren = self.validChildren(hips) 85 | nChildren = len(hipsChildren) 86 | else: 87 | counts = self.getChildCount(hipsChildren) 88 | hipsChildren = [counts[-3][2], counts[-2][2], counts[-1][2]] 89 | nChildren = 3 90 | if hips is not None: 91 | print(" Try hips: %s, children: %d" % (hips.name, nChildren)) 92 | first = False 93 | 94 | if hips is None: 95 | raise MocapError("Found no candidate hip bone") 96 | 97 | print("Mapping bones automatically:") 98 | print(" hips:", hips.name) 99 | self.setBone("hips", hips) 100 | hiphead,hiptail,_ = getHeadTailDir(hips) 101 | 102 | if rig.MhReverseHip and len(hipsChildren) == 2: 103 | legroot = hipsChildren[1] 104 | spine = hipsChildren[0] 105 | _,terminal = self.chainEnd(legroot) 106 | head,tail,vec = getHeadTailDir(terminal) 107 | if tail[2] > hiptail[2]: 108 | legroot = hipsChildren[0] 109 | spine = hipsChildren[1] 110 | hipsChildren = [spine] + self.validChildren(legroot) 111 | 112 | if len(hipsChildren) < 3: 113 | string = "Hips %s has %d children:\n" % (hips.name, len(hipsChildren)) 114 | for child in hipsChildren: 115 | string += " %s\n" % child.name 116 | raise MocapError(string) 117 | 118 | spine = None 119 | spineTail = hiptail 120 | leftLeg = None 121 | leftLegTail = hiptail 122 | rightLeg = None 123 | rightLegTail = hiptail 124 | 125 | limbs = [] 126 | for pb in hipsChildren: 127 | _,terminal = self.chainEnd(pb) 128 | _,tail,_ = getHeadTailDir(terminal) 129 | limbs.append((tail[0], pb)) 130 | limbs.sort() 131 | _,rightLeg = limbs[0] 132 | _,spine = limbs[1] 133 | _,leftLeg = limbs[2] 134 | 135 | print(" spine:", spine.name) 136 | print(" right leg:", rightLeg.name) 137 | print(" left leg:", leftLeg.name) 138 | self.findSpine(spine) 139 | self.findLeg(leftLeg, ".L") 140 | self.findLeg(rightLeg, ".R") 141 | 142 | 143 | def errLimb(self, name, pb, tail): 144 | if pb is None: 145 | return (" No %s\n" % name) 146 | else: 147 | return (" %s = %s, tail = %s\n" % (name, pb.name, tuple(tail))) 148 | 149 | 150 | def setBone(self, bname, pb): 151 | pb.McpBone = bname 152 | self.boneNames[canonicalName(pb.name)] = bname 153 | 154 | 155 | def findTerminal(self, pb, bnames, prefnames=None): 156 | if prefnames is None: 157 | prefnames = bnames 158 | self.setBone(bnames[0], pb) 159 | bnames = bnames[1:] 160 | prefnames = prefnames[1:] 161 | children = self.validChildren(pb) 162 | if bnames: 163 | if len(children) >= 1: 164 | child = children[0] 165 | for pb in children[1:]: 166 | if prefnames[0] in pb.name.lower(): 167 | child = pb 168 | break 169 | return self.findTerminal(child, bnames, prefnames) 170 | else: 171 | return None 172 | else: 173 | while len(children) == 1: 174 | pb = children[0] 175 | children = self.validChildren(pb) 176 | return pb 177 | 178 | 179 | def findLeg(self, hip, suffix): 180 | bnames = ["hip"+suffix, "thigh"+suffix, "shin"+suffix, "foot"+suffix, "toe"+suffix] 181 | prefnames = ["X", "X", "X", "foot", "toe"] 182 | print(" hip%s:" % suffix, hip.name) 183 | try: 184 | thigh = self.validChildren(hip)[0] 185 | except IndexError: 186 | raise MocapError("Hip %s has no children" % hip.name) 187 | shins = self.validChildren(thigh, True) 188 | if len(shins) == 0: 189 | raise MocapError("Thigh %s has no children" % thigh.name) 190 | elif len(shins) > 1: 191 | shin = thigh 192 | thigh = hip 193 | print(" thigh%s:" % suffix, thigh.name) 194 | print(" shin%s:" % suffix, shin.name) 195 | bnames = bnames[1:] 196 | prefnames = prefnames[1:] 197 | else: 198 | shin = shins[0] 199 | foot = None 200 | if hip.length > shin.length: 201 | foot = shin 202 | shin = thigh 203 | thigh = hip 204 | bnames = bnames[1:] 205 | prefnames = prefnames[1:] 206 | else: 207 | feet = self.validChildren(shin) 208 | if feet: 209 | foot = feet[0] 210 | print(" thigh%s:" % suffix, thigh.name) 211 | print(" shin%s:" % suffix, shin.name) 212 | if foot: 213 | print(" foot%s:" % suffix, foot.name) 214 | 215 | self.findTerminal(hip, bnames, prefnames) 216 | 217 | 218 | def findArm(self, shoulder, suffix): 219 | bnames = ["shoulder"+suffix, "upper_arm"+suffix, "forearm"+suffix, "hand"+suffix] 220 | print(" shoulder%s:" % suffix, shoulder.name) 221 | try: 222 | upperarm = self.validChildren(shoulder)[0] 223 | except IndexError: 224 | raise MocapError("Shoulder %s has no children" % shoulder.name) 225 | print(" upper_arm%s:" % suffix, upperarm.name) 226 | try: 227 | forearm = self.validChildren(upperarm, True)[0] 228 | except IndexError: 229 | raise MocapError("Upper arm %s has no children" % upperarm.name) 230 | print(" forearm%s:" % suffix, forearm.name) 231 | hands = self.validChildren(forearm) 232 | if hands: 233 | hand = hands[0] 234 | print(" hand%s:" % suffix, hand.name) 235 | if upperarm.bone.length < hand.bone.length: 236 | bnames = ["shoulder"+suffix, ""] + bnames[1:] 237 | self.findTerminal(shoulder, bnames) 238 | 239 | 240 | def findHead(self, neck): 241 | bnames = ["neck", "head"] 242 | print(" neck:", neck.name) 243 | self.findTerminal(neck, bnames) 244 | 245 | 246 | def findSpine(self, spine1): 247 | n,spine2 = self.spineEnd(spine1) 248 | print(" spine:", spine1.name) 249 | print(" chest:", spine2.name) 250 | if n == 1: 251 | bnames = ["spine"] 252 | elif n == 2: 253 | bnames = ["spine", "chest"] 254 | elif n == 3: 255 | bnames = ["spine", "spine-1", "chest"] 256 | else: 257 | bnames = ["spine", "spine-1", "chest", "chest-1"] 258 | 259 | self.findTerminal(spine1, bnames) 260 | spine2Children = self.validChildren(spine2) 261 | if len(spine2Children) == 3: 262 | _,stail,_ = getHeadTailDir(spine2) 263 | limbs = [] 264 | for pb in spine2Children: 265 | _,tail,_ = getHeadTailDir(pb) 266 | limbs.append((tail[0],pb)) 267 | limbs.sort() 268 | self.findArm(limbs[0][1], ".R") 269 | self.findHead(limbs[1][1]) 270 | self.findArm(limbs[2][1], ".L") 271 | else: 272 | string = ("Could not auto-detect armature because:\nTop of spine %s has %d children:\n" % (spine2.name, len(spine2Children))) 273 | for child in spine2Children: 274 | string += " %s\n" % child.name 275 | raise MocapError(string) 276 | 277 | 278 | def validChildren(self, pb, muteIk=False): 279 | children = [] 280 | for child in pb.children: 281 | if validBone(child, self.rig, muteIk): 282 | children.append(child) 283 | return children 284 | 285 | 286 | def getChildCount(self, children): 287 | counts = [] 288 | for n,child in enumerate(children): 289 | counts.append((self.countChildren(child, 5), n, child)) 290 | counts.sort() 291 | return counts 292 | 293 | 294 | def countChildren(self, pb, depth): 295 | if depth < 0: 296 | return 0 297 | n = 1 298 | for child in pb.children: 299 | n += self.countChildren(child, depth-1) 300 | return n 301 | 302 | 303 | def chainEnd(self, pb): 304 | n = 1 305 | while pb and (len(self.validChildren(pb)) == 1): 306 | n += 1 307 | pb = self.validChildren(pb)[0] 308 | return n,pb 309 | 310 | 311 | def spineEnd(self, pb): 312 | n = 1 313 | while pb: 314 | children = self.validChildren(pb) 315 | if len(children) == 1: 316 | n += 1 317 | pb = children[0] 318 | elif len(children) == 0 and len(pb.children) == 1: 319 | n += 1 320 | pb = pb.children[0] 321 | else: 322 | return n,pb 323 | return n,pb 324 | 325 | 326 | def validBone(pb, rig=None, muteIk=False): 327 | if rig is not None: 328 | hidden = True 329 | for n in range(len(rig.data.layers)): 330 | if rig.data.layers[n] and pb.bone.layers[n]: 331 | hidden = False 332 | break 333 | if hidden: 334 | print("Hidden", pb.name) 335 | return False 336 | 337 | for cns in pb.constraints: 338 | if cns.mute or cns.influence < 0.2: 339 | pass 340 | elif cns.type[0:5] == 'LIMIT': 341 | #cns.influence = 0 342 | pass 343 | elif (cns.type == 'COPY_ROTATION' and 344 | cns.use_offset): 345 | pass 346 | elif cns.type == 'IK': 347 | if muteIk: 348 | cns.mute = True 349 | elif cns.target is None: 350 | pass 351 | else: 352 | return False 353 | else: 354 | return False 355 | return True 356 | 357 | 358 | def getHeadTailDir(pb): 359 | mat = pb.bone.matrix_local 360 | head = Vector(mat.col[3][:3]) 361 | vec = Vector(mat.col[1][:3]) 362 | tail = head + pb.bone.length * vec 363 | return head, tail, vec 364 | -------------------------------------------------------------------------------- /t_pose.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | from bpy.props import * 33 | 34 | import os 35 | import math 36 | from mathutils import Quaternion, Matrix 37 | from .utils import * 38 | from .io_json import * 39 | if bpy.app.version < (2,80,0): 40 | from .buttons27 import ProblemsString, LoadJson 41 | else: 42 | from .buttons28 import ProblemsString, LoadJson 43 | 44 | #------------------------------------------------------------------ 45 | # Define current pose as rest pose 46 | #------------------------------------------------------------------ 47 | 48 | def applyRestPose(context, value): 49 | rig = context.object 50 | children = [] 51 | for ob in getSceneObjects(context): 52 | if ob.type != 'MESH': 53 | continue 54 | 55 | setActiveObject(context, ob) 56 | if ob != context.object: 57 | raise StandardError("Context switch did not take:\nob = %s\nc.ob = %s\nc.aob = %s" % 58 | (ob, context.object, context.active_object)) 59 | 60 | if (ob.McpArmatureName == rig.name and 61 | ob.McpArmatureModifier != ""): 62 | mod = ob.modifiers[ob.McpArmatureModifier] 63 | ob.modifiers.remove(mod) 64 | ob.data.shape_keys.key_blocks[ob.McpArmatureModifier].value = value 65 | children.append(ob) 66 | else: 67 | for mod in ob.modifiers: 68 | if (mod.type == 'ARMATURE' and 69 | mod.object == rig): 70 | children.append(ob) 71 | bpy.ops.object.modifier_apply(apply_as='SHAPE', modifier=mod.name) 72 | ob.data.shape_keys.key_blocks[mod.name].value = value 73 | ob.McpArmatureName = rig.name 74 | ob.McpArmatureModifier = mod.name 75 | break 76 | 77 | setActiveObject(context, rig) 78 | bpy.ops.object.mode_set(mode='POSE') 79 | bpy.ops.pose.armature_apply() 80 | for ob in children: 81 | name = ob.McpArmatureModifier 82 | setActiveObject(context, ob) 83 | mod = ob.modifiers.new(name, 'ARMATURE') 84 | mod.object = rig 85 | mod.use_vertex_groups = True 86 | bpy.ops.object.modifier_move_up(modifier=name) 87 | #setShapeKey(ob, name, value) 88 | 89 | setActiveObject(context, rig) 90 | print("Applied pose as rest pose") 91 | 92 | 93 | def setShapeKey(ob, name, value): 94 | if not ob.data.shape_keys: 95 | return 96 | skey = ob.data.shape_keys.key_blocks[name] 97 | skey.value = value 98 | 99 | 100 | class MCP_OT_RestCurrentPose(bpy.types.Operator): 101 | bl_idname = "mcp.rest_current_pose" 102 | bl_label = "Current Pose => Rest Pose" 103 | bl_description = "Change rest pose to current pose" 104 | bl_options = {'UNDO'} 105 | 106 | def execute(self, context): 107 | try: 108 | initRig(context) 109 | applyRestPose(context, 1.0) 110 | print("Set current pose to rest pose") 111 | except MocapError: 112 | bpy.ops.mcp.error('INVOKE_DEFAULT') 113 | return{'FINISHED'} 114 | 115 | #------------------------------------------------------------------ 116 | # Automatic T-Pose 117 | #------------------------------------------------------------------ 118 | 119 | TPose = { 120 | "upper_arm.L" : (0, 0, -pi/2, 'XYZ'), 121 | "forearm.L" : (0, 0, -pi/2, 'XYZ'), 122 | #"hand.L" : (0, 0, -pi/2, 'XYZ'), 123 | 124 | "upper_arm.R" : (0, 0, pi/2, 'XYZ'), 125 | "forearm.R" : (0, 0, pi/2, 'XYZ'), 126 | #"hand.R" : (0, 0, pi/2, 'XYZ'), 127 | 128 | "thigh.L" : (-pi/2, 0, 0, 'XYZ'), 129 | "shin.L" : (-pi/2, 0, 0, 'XYZ'), 130 | #"foot.L" : (None, 0, 0, 'XYZ'), 131 | #"toe.L" : (pi, 0, 0, 'XYZ'), 132 | 133 | "thigh.R" : (-pi/2, 0, 0, 'XYZ'), 134 | "shin.R" : (-pi/2, 0, 0, 'XYZ'), 135 | #"foot.R" : (None, 0, 0, 'XYZ'), 136 | #"toe.R" : (pi, 0, 0, 'XYZ'), 137 | } 138 | 139 | def autoTPose(rig, context): 140 | print("Auto T-pose", rig.name) 141 | putInRestPose(rig, True) 142 | for pb in rig.pose.bones: 143 | try: 144 | ex,ey,ez,order = TPose[pb.McpBone] 145 | except KeyError: 146 | continue 147 | 148 | euler = pb.matrix.to_euler(order) 149 | if ex is None: 150 | ex = euler.x 151 | if ey is None: 152 | ey = euler.y 153 | if ez is None: 154 | ez = euler.z 155 | euler = Euler((ex,ey,ez), order) 156 | mat = euler.to_matrix().to_4x4() 157 | mat.col[3] = pb.matrix.col[3] 158 | 159 | loc = pb.bone.matrix_local 160 | if pb.parent: 161 | mat = Mult2(pb.parent.matrix.inverted(), mat) 162 | loc = Mult2(pb.parent.bone.matrix_local.inverted(), loc) 163 | mat = Mult2(loc.inverted(), mat) 164 | euler = mat.to_euler('YZX') 165 | euler.y = 0 166 | pb.matrix_basis = euler.to_matrix().to_4x4() 167 | bpy.ops.object.mode_set(mode='EDIT') 168 | bpy.ops.object.mode_set(mode='POSE') 169 | 170 | #------------------------------------------------------------------ 171 | # Set current pose to T-Pose 172 | #------------------------------------------------------------------ 173 | 174 | def setTPose(rig, context, filename=None, reload=False): 175 | if reload or not rig.McpTPoseDefined: 176 | if isMakeHumanRig(rig) and scn.McpMakeHumanTPose: 177 | if isMhOfficialRig(rig): 178 | filename = "target_rigs/mh_official_tpose.json" 179 | else: 180 | filename = "target_rigs/makehuman_tpose.json" 181 | elif filename is None: 182 | filename = rig.McpTPoseFile 183 | hasFile = loadPose(rig, filename) 184 | if not hasFile: 185 | autoTPose(rig, context) 186 | defineTPose(rig) 187 | else: 188 | getStoredTPose(rig) 189 | 190 | 191 | class MCP_OT_SetTPose(bpy.types.Operator): 192 | bl_idname = "mcp.set_t_pose" 193 | bl_label = "Put In T-pose" 194 | bl_description = "Set current pose to T-pose" 195 | bl_options = {'UNDO'} 196 | 197 | def execute(self, context): 198 | try: 199 | rig = initRig(context) 200 | isdefined = rig.McpTPoseDefined 201 | setTPose(rig, context, reload=True) 202 | rig.McpTPoseDefined = isdefined 203 | print("Pose set to T-pose") 204 | except MocapError: 205 | bpy.ops.mcp.error('INVOKE_DEFAULT') 206 | return{'FINISHED'} 207 | 208 | #------------------------------------------------------------------ 209 | # Set T-Pose 210 | #------------------------------------------------------------------ 211 | 212 | def getStoredTPose(rig): 213 | for pb in rig.pose.bones: 214 | pb.matrix_basis = getStoredBonePose(pb) 215 | 216 | 217 | def getStoredBonePose(pb): 218 | try: 219 | quat = Quaternion((pb.McpQuatW, pb.McpQuatX, pb.McpQuatY, pb.McpQuatZ)) 220 | except KeyError: 221 | quat = Quaternion() 222 | return quat.to_matrix().to_4x4() 223 | 224 | 225 | def addTPoseAtFrame0(rig, scn): 226 | from .source import getSourceTPoseFile 227 | 228 | scn.frame_current = 0 229 | if rig.McpTPoseDefined: 230 | getStoredTPose(rig) 231 | elif getSourceTPoseFile(): 232 | rig.McpTPoseFile = getSourceTPoseFile() 233 | defineTPose(rig) 234 | else: 235 | setRestPose(rig) 236 | defineTPose(rig) 237 | 238 | for pb in rig.pose.bones: 239 | if pb.rotation_mode == 'QUATERNION': 240 | pb.keyframe_insert('rotation_quaternion', group=pb.name) 241 | else: 242 | pb.keyframe_insert('rotation_euler', group=pb.name) 243 | 244 | #------------------------------------------------------------------ 245 | # Define current pose as T-Pose 246 | #------------------------------------------------------------------ 247 | 248 | def defineTPose(rig): 249 | for pb in rig.pose.bones: 250 | quat = pb.matrix_basis.to_quaternion() 251 | pb.McpQuatW = quat.w 252 | pb.McpQuatX = quat.x 253 | pb.McpQuatY = quat.y 254 | pb.McpQuatZ = quat.z 255 | rig.McpTPoseDefined = True 256 | 257 | 258 | class MCP_OT_DefineTPose(bpy.types.Operator, ProblemsString): 259 | bl_idname = "mcp.define_t_pose" 260 | bl_label = "Define T-pose" 261 | bl_description = "Define T-pose as current pose" 262 | bl_options = {'UNDO'} 263 | 264 | def execute(self, context): 265 | if self.problems: 266 | return{'FINISHED'} 267 | try: 268 | rig = initRig(context) 269 | defineTPose(rig) 270 | print("T-pose defined as current pose") 271 | except MocapError: 272 | bpy.ops.mcp.error('INVOKE_DEFAULT') 273 | return{'FINISHED'} 274 | 275 | def invoke(self, context, event): 276 | return checkObjectProblems(self, context) 277 | 278 | def draw(self, context): 279 | drawObjectProblems(self) 280 | 281 | #------------------------------------------------------------------ 282 | # Undefine stored T-pose 283 | #------------------------------------------------------------------ 284 | 285 | def setRestPose(rig): 286 | unit = Matrix() 287 | for pb in rig.pose.bones: 288 | pb.matrix_basis = unit 289 | 290 | 291 | class MCP_OT_UndefineTPose(bpy.types.Operator): 292 | bl_idname = "mcp.undefine_t_pose" 293 | bl_label = "Undefine T-pose" 294 | bl_description = "Remove definition of T-pose" 295 | bl_options = {'UNDO'} 296 | 297 | def execute(self, context): 298 | try: 299 | rig = initRig(context) 300 | rig.McpTPoseDefined = False 301 | print("Undefined T-pose") 302 | except MocapError: 303 | bpy.ops.mcp.error('INVOKE_DEFAULT') 304 | return{'FINISHED'} 305 | 306 | #------------------------------------------------------------------ 307 | # Load T-pose from file 308 | #------------------------------------------------------------------ 309 | 310 | def loadPose(rig, filename): 311 | if filename: 312 | filepath = os.path.join(os.path.dirname(__file__), filename) 313 | filepath = os.path.normpath(filepath) 314 | print("Loading %s" % filepath) 315 | struct = loadJson(filepath) 316 | rig.McpTPoseFile = filename 317 | else: 318 | return False 319 | 320 | setRestPose(rig) 321 | 322 | for name,value in struct: 323 | bname = getBoneName(rig, name) 324 | try: 325 | pb = rig.pose.bones[bname] 326 | except KeyError: 327 | continue 328 | quat = Quaternion(value) 329 | pb.matrix_basis = quat.to_matrix().to_4x4() 330 | 331 | return True 332 | 333 | 334 | def getBoneName(rig, name): 335 | if rig.McpIsSourceRig: 336 | return name 337 | else: 338 | pb = getTrgBone(name, rig) 339 | if pb: 340 | return pb.name 341 | else: 342 | return "" 343 | 344 | 345 | class MCP_OT_LoadPose(bpy.types.Operator, LoadJson): 346 | bl_idname = "mcp.load_pose" 347 | bl_label = "Load Pose" 348 | bl_description = "Load pose from file" 349 | bl_options = {'UNDO'} 350 | 351 | def execute(self, context): 352 | rig = initRig(context) 353 | filename = os.path.relpath(self.filepath, os.path.dirname(__file__)) 354 | try: 355 | loadPose(rig, filename) 356 | except MocapError: 357 | bpy.ops.mcp.error('INVOKE_DEFAULT') 358 | print("Loaded pose") 359 | return{'FINISHED'} 360 | 361 | def invoke(self, context, event): 362 | context.window_manager.fileselect_add(self) 363 | return {'RUNNING_MODAL'} 364 | 365 | #------------------------------------------------------------------ 366 | # Save current pose to file 367 | #------------------------------------------------------------------ 368 | 369 | def savePose(context, filepath): 370 | rig = context.object 371 | struct = [] 372 | for pb in rig.pose.bones: 373 | bmat = pb.matrix 374 | rmat = pb.bone.matrix_local 375 | if pb.parent: 376 | bmat = Mult2(pb.parent.matrix.inverted(), bmat) 377 | rmat = Mult2(pb.parent.bone.matrix_local.inverted(), rmat) 378 | mat = Mult2(rmat.inverted(), bmat) 379 | q = mat.to_quaternion() 380 | magn = math.sqrt( (q.w-1)*(q.w-1) + q.x*q.x + q.y*q.y + q.z*q.z ) 381 | if magn > 1e-4: 382 | if pb.McpBone: 383 | struct.append((pb.McpBone, tuple(q))) 384 | 385 | if os.path.splitext(filepath)[1] != ".json": 386 | filepath = filepath + ".json" 387 | filepath = os.path.join(os.path.dirname(__file__), filepath) 388 | print("Saving %s" % filepath) 389 | saveJson(struct, filepath) 390 | 391 | 392 | class MCP_OT_SavePose(bpy.types.Operator, LoadJson): 393 | bl_idname = "mcp.save_pose" 394 | bl_label = "Save Pose" 395 | bl_description = "Save current pose as .json file" 396 | bl_options = {'UNDO'} 397 | 398 | def execute(self, context): 399 | try: 400 | savePose(context, self.filepath) 401 | except MocapError: 402 | bpy.ops.mcp.error('INVOKE_DEFAULT') 403 | print("Saved current pose") 404 | return{'FINISHED'} 405 | 406 | def invoke(self, context, event): 407 | context.window_manager.fileselect_add(self) 408 | return {'RUNNING_MODAL'} 409 | 410 | #------------------------------------------------------------------ 411 | # Utils 412 | #------------------------------------------------------------------ 413 | 414 | def initRig(context): 415 | from . import target 416 | from . import source 417 | from .fkik import setRigifyFKIK, setRigify2FKIK 418 | 419 | rig = context.object 420 | pose = [(pb, pb.matrix_basis.copy()) for pb in rig.pose.bones] 421 | 422 | if rig.McpIsSourceRig: 423 | source.findSrcArmature(context, rig) 424 | else: 425 | target.getTargetArmature(rig, context) 426 | 427 | for pb,mat in pose: 428 | pb.matrix_basis = mat 429 | 430 | if isRigify(rig): 431 | setRigifyFKIK(rig, 0.0) 432 | elif isRigify2(rig): 433 | setRigify2FKIK(rig, 1.0) 434 | 435 | return rig 436 | 437 | #---------------------------------------------------------- 438 | # Initialize 439 | #---------------------------------------------------------- 440 | 441 | classes = [ 442 | MCP_OT_RestCurrentPose, 443 | MCP_OT_SetTPose, 444 | MCP_OT_DefineTPose, 445 | MCP_OT_UndefineTPose, 446 | MCP_OT_LoadPose, 447 | MCP_OT_SavePose, 448 | ] 449 | 450 | def initialize(): 451 | for cls in classes: 452 | bpy.utils.register_class(cls) 453 | 454 | 455 | def uninitialize(): 456 | for cls in classes: 457 | bpy.utils.unregister_class(cls) 458 | -------------------------------------------------------------------------------- /edit.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | from bpy.props import * 33 | from math import pi, sqrt 34 | from mathutils import * 35 | from . import load, simplify, props, action 36 | #from .target_rigs import rig_mhx 37 | from .utils import * 38 | if bpy.app.version < (2,80,0): 39 | from .buttons27 import AnswerString, LocRotDel, LeftLast 40 | else: 41 | from .buttons28 import AnswerString, LocRotDel, LeftLast 42 | 43 | 44 | _Markers = [] 45 | _EditLoc = None 46 | _EditRot = None 47 | 48 | #---------------------------------------------------------------- 49 | # Markers 50 | #---------------------------------------------------------------- 51 | 52 | def saveMarkers(scn): 53 | global _Markers 54 | _Markers = [(mrk.camera, mrk.frame, mrk.name, mrk.select) for mrk in scn.timeline_markers] 55 | scn.timeline_markers.clear() 56 | 57 | 58 | def restoreMarkers(scn): 59 | global _Markers 60 | scn.timeline_markers.clear() 61 | for (camera, frame, name, select) in _Markers: 62 | mrk = scn.timeline_markers.new(name) 63 | mrk.camera = camera 64 | mrk.frame = frame 65 | mrk.select = select 66 | 67 | 68 | def setMarker(scn, frame): 69 | mrk = scn.timeline_markers.new("F_%d" % int(frame)) 70 | mrk.frame = frame 71 | 72 | 73 | def removeMarker(scn, frame): 74 | marker = None 75 | for mrk in scn.timeline_markers: 76 | if mrk.frame == frame: 77 | scn.timeline_markers.remove(mrk) 78 | return 79 | raise MocapError("No keys at frame %d" % int(frame)) 80 | 81 | 82 | ######################################################################## 83 | # 84 | # startEdit(context): 85 | # class MCP_OT_StartEdit(bpy.types.Operator): 86 | # 87 | 88 | def getUndoAction(rig): 89 | try: 90 | return bpy.data.actions[rig.McpUndoAction] 91 | except: 92 | return None 93 | 94 | 95 | def startEdit(context): 96 | global _EditLoc, _EditRot 97 | 98 | rig = context.object 99 | scn = context.scene 100 | if getUndoAction(rig): 101 | raise MocapError("Action already being edited. Undo or confirm edit first") 102 | act = getAction(rig) 103 | if not act: 104 | raise MocapError("Object %s has no action" % rig.name) 105 | aname = act.name 106 | act.name = '#'+aname 107 | nact = bpy.data.actions.new(aname) 108 | rig.animation_data.action = nact 109 | rig.McpUndoAction = act.name 110 | rig.McpActionName = aname 111 | 112 | saveMarkers(scn) 113 | _EditLoc = quadDict() 114 | _EditRot = quadDict() 115 | 116 | for fcu in act.fcurves: 117 | (name, mode) = fCurveIdentity(fcu) 118 | nfcu = nact.fcurves.new(fcu.data_path, index=fcu.array_index, action_group=name) 119 | n = len(fcu.keyframe_points) 120 | nfcu.keyframe_points.add(count=n) 121 | for i in range(n): 122 | nfcu.keyframe_points[i].co = fcu.keyframe_points[i].co 123 | setInterpolation(rig) 124 | print("Action editing started") 125 | return nact 126 | 127 | 128 | class MCP_OT_StartEdit(bpy.types.Operator): 129 | bl_idname = "mcp.start_edit" 130 | bl_label = "Start Edit" 131 | bl_options = {'UNDO'} 132 | 133 | @classmethod 134 | def poll(self, context): 135 | return (context.object.McpUndoAction == "") 136 | 137 | def execute(self, context): 138 | try: 139 | startEdit(context) 140 | setKeyMap(context, "mcp.insert_locrot", True) 141 | except MocapError: 142 | bpy.ops.mcp.error('INVOKE_DEFAULT') 143 | return{'FINISHED'} 144 | 145 | 146 | def setKeyMap(context, idname, doAdd): 147 | return 148 | km = context.window_manager.keyconfigs.active.keymaps['3D View'] 149 | if doAdd: 150 | km.keymap_items.new(idname, 'SPACE', 'PRESS') 151 | else: 152 | try: 153 | item = km.keymap_items[idname] 154 | except KeyError: 155 | return 156 | km.keymap_items.remove(item) 157 | 158 | # 159 | # undoEdit(context): 160 | # class MCP_OT_UndoEdit(bpy.types.Operator): 161 | # 162 | 163 | def undoEdit(context): 164 | global _EditLoc, _EditRot 165 | 166 | rig = context.object 167 | restoreMarkers(context.scene) 168 | oact = getUndoAction(rig) 169 | if not oact: 170 | clearUndoAction(rig) 171 | raise MocapError("No action to undo") 172 | clearUndoAction(rig) 173 | act = rig.animation_data.action 174 | act.name = "#Delete" 175 | oact.name = rig.McpActionName 176 | rig.animation_data.action = oact 177 | deleteAction(act) 178 | print("Action changes undone") 179 | return 180 | 181 | 182 | class MCP_OT_UndoEdit(bpy.types.Operator, AnswerString): 183 | bl_idname = "mcp.undo_edit" 184 | bl_label = "Undo Edit" 185 | bl_options = {'UNDO'} 186 | 187 | @classmethod 188 | def poll(self, context): 189 | return (context.object.McpUndoAction != "") 190 | 191 | def execute(self, context): 192 | setKeyMap(context, "mcp.insert_locrot", False) 193 | try: 194 | undoEdit(context) 195 | except MocapError: 196 | bpy.ops.mcp.error('INVOKE_DEFAULT') 197 | return{'FINISHED'} 198 | 199 | def invoke(self, context, event): 200 | wm = context.window_manager 201 | return wm.invoke_props_dialog(self, width=200, height=20) 202 | 203 | def draw(self, context): 204 | self.layout.label(text="Really undo editing?") 205 | 206 | # 207 | # getActionPair(context): 208 | # 209 | 210 | def clearUndoAction(rig): 211 | global _EditLoc, _EditRot 212 | rig.McpUndoAction = "" 213 | _EditLoc = None 214 | _EditRot = None 215 | rig.McpActionName = "" 216 | 217 | 218 | def getActionPair(context): 219 | global _EditLoc, _EditRot 220 | 221 | rig = context.object 222 | oact = getUndoAction(rig) 223 | if not oact: 224 | clearUndoAction(rig) 225 | raise MocapError("No action is currently being edited") 226 | return None 227 | if not _EditLoc: 228 | _EditLoc = quadDict() 229 | if not _EditRot: 230 | _EditRot = quadDict() 231 | act = getAction(rig) 232 | if act: 233 | return (act, oact) 234 | else: 235 | return None 236 | 237 | # 238 | # confirmEdit(context): 239 | # class MCP_OT_ConfirmEdit(bpy.types.Operator): 240 | # 241 | 242 | def confirmEdit(context): 243 | global _EditLoc, _EditRot 244 | 245 | rig = context.object 246 | pair = getActionPair(context) 247 | if not pair: 248 | return 249 | (act, oact) = pair 250 | 251 | for fcu in act.fcurves: 252 | ofcu = findFCurve(fcu.data_path, fcu.array_index, oact.fcurves) 253 | if not ofcu: 254 | continue 255 | (name,mode) = fCurveIdentity(fcu) 256 | if isRotation(mode): 257 | try: 258 | edit = _EditRot[fcu.array_index][name] 259 | except: 260 | continue 261 | displaceFCurve(fcu, ofcu, edit) 262 | elif isLocation(mode): 263 | try: 264 | edit = _EditLoc[fcu.array_index][name] 265 | except: 266 | continue 267 | displaceFCurve(fcu, ofcu, edit) 268 | 269 | rig.McpUndoAction = "" 270 | rig.McpActionName = "" 271 | restoreMarkers(context.scene) 272 | _EditLoc = None 273 | _EditRot = None 274 | deleteAction(oact) 275 | print("Action changed") 276 | return 277 | 278 | 279 | class MCP_OT_ConfirmEdit(bpy.types.Operator): 280 | bl_idname = "mcp.confirm_edit" 281 | bl_label = "Confirm Edit" 282 | bl_options = {'UNDO'} 283 | 284 | @classmethod 285 | def poll(self, context): 286 | return (context.object.McpUndoAction != "") 287 | 288 | def execute(self, context): 289 | setKeyMap(context, "mcp.insert_locrot", False) 290 | try: 291 | confirmEdit(context) 292 | except MocapError: 293 | bpy.ops.mcp.error('INVOKE_DEFAULT') 294 | return{'FINISHED'} 295 | 296 | # 297 | # setEditDict(editDict, frame, name, channel, index): 298 | # insertKey(context, useLoc, useRot): 299 | # class MCP_OT_InsertLoc(bpy.types.Operator): 300 | # class MCP_OT_InsertRot(bpy.types.Operator): 301 | # class MCP_OT_InsertLocRot(bpy.types.Operator): 302 | # 303 | 304 | def setEditDict(editDict, frame, name, channel, n): 305 | for index in range(n): 306 | try: 307 | edit = editDict[index][name] 308 | except: 309 | edit = editDict[index][name] = {} 310 | edit[frame] = channel[index] 311 | return 312 | 313 | 314 | def removeEditDict(editDict, frame, name, n): 315 | for index in range(n): 316 | try: 317 | del editDict[index][name][frame] 318 | except: 319 | editDict[index][name] = {} 320 | 321 | 322 | def insertKey(context, useLoc, useRot, delete): 323 | global _EditLoc, _EditRot 324 | 325 | rig = context.object 326 | pair = getActionPair(context) 327 | if not pair: 328 | raise MocapError("No action is currently being edited") 329 | (act, oact) = pair 330 | 331 | scn = context.scene 332 | frame = scn.frame_current 333 | if delete: 334 | removeMarker(scn, frame) 335 | else: 336 | setMarker(scn, frame) 337 | 338 | for pb in rig.pose.bones: 339 | if not pb.bone.select: 340 | continue 341 | 342 | if delete: 343 | removeEditDict(_EditLoc, frame, pb.name, 3) 344 | removeEditDict(_EditRot, frame, pb.name, 4) 345 | else: 346 | if useLoc: 347 | setEditDict(_EditLoc, frame, pb.name, pb.location, 3) 348 | if useRot: 349 | if pb.rotation_mode == 'QUATERNION': 350 | setEditDict(_EditRot, frame, pb.name, pb.rotation_quaternion, 4) 351 | else: 352 | setEditDict(_EditRot, frame, pb.name, pb.rotation_euler, 3) 353 | 354 | for fcu in act.fcurves: 355 | ofcu = findFCurve(fcu.data_path, fcu.array_index, oact.fcurves) 356 | if not ofcu: 357 | continue 358 | (name,mode) = fCurveIdentity(fcu) 359 | if name == pb.name: 360 | if isRotation(mode) and useRot: 361 | displaceFCurve(fcu, ofcu, _EditRot[fcu.array_index][name]) 362 | if isLocation(mode) and useLoc: 363 | displaceFCurve(fcu, ofcu, _EditLoc[fcu.array_index][name]) 364 | 365 | 366 | class MCP_OT_InsertKey(bpy.types.Operator, LocRotDel): 367 | bl_idname = "mcp.insert_key" 368 | bl_label = "Key" 369 | bl_options = {'UNDO'} 370 | 371 | @classmethod 372 | def poll(self, context): 373 | return (context.object.McpUndoAction != "") 374 | 375 | def execute(self, context): 376 | try: 377 | insertKey(context, self.properties.loc, self.properties.rot, self.properties.delete) 378 | except MocapError: 379 | bpy.ops.mcp.error('INVOKE_DEFAULT') 380 | return{'FINISHED'} 381 | 382 | 383 | def move2marker(context, left, last): 384 | scn = context.scene 385 | frames = [mrk.frame for mrk in scn.timeline_markers] 386 | frames.sort() 387 | if frames == []: 388 | return 389 | if last: 390 | if left: 391 | scn.frame_current = frames[0] 392 | else: 393 | scn.frame_current = frames[-1] 394 | else: 395 | if left: 396 | frames.reverse() 397 | for frame in frames: 398 | if frame < scn.frame_current: 399 | scn.frame_current = frame 400 | break 401 | else: 402 | for frame in frames: 403 | if frame > scn.frame_current: 404 | scn.frame_current = frame 405 | break 406 | 407 | 408 | class MCP_OT_MoveToMarker(bpy.types.Operator, LeftLast): 409 | bl_idname = "mcp.move_to_marker" 410 | bl_label = "Move" 411 | bl_description = "Move to time marker" 412 | bl_options = {'UNDO'} 413 | 414 | @classmethod 415 | def poll(self, context): 416 | return (context.object.McpUndoAction != "") 417 | 418 | def execute(self, context): 419 | try: 420 | move2marker(context, self.properties.left, self.properties.last) 421 | except MocapError: 422 | bpy.ops.mcp.error('INVOKE_DEFAULT') 423 | return{'FINISHED'} 424 | 425 | 426 | # 427 | # displaceFCurve(fcu, ofcu, edits): 428 | # setupCatmullRom(kpoints, modified): 429 | # evalCatmullRom(t, fcn): 430 | # 431 | 432 | def displaceFCurve(fcu, ofcu, edits): 433 | modified = [] 434 | editList = list(edits.items()) 435 | editList.sort() 436 | for (t,y) in editList: 437 | dy = y - ofcu.evaluate(t) 438 | modified.append((t,dy)) 439 | 440 | if len(modified) >= 1: 441 | kp = fcu.keyframe_points[0].co 442 | t0 = int(kp[0]) 443 | (t1,y1) = modified[0] 444 | kp = fcu.keyframe_points[-1].co 445 | tn = int(kp[0]) 446 | (tn_1,yn_1) = modified[-1] 447 | modified = [(t0, y1)] + modified 448 | modified.append( (tn, yn_1) ) 449 | fcn = setupCatmullRom(modified) 450 | for kp in fcu.keyframe_points: 451 | t = kp.co[0] 452 | y = ofcu.evaluate(t) 453 | dy = evalCatmullRom(t, fcn) 454 | kp.co[1] = y+dy 455 | return 456 | 457 | 458 | def setupCatmullRom(points): 459 | points.sort() 460 | n = len(points)-1 461 | fcn = [] 462 | tension = 0.5 463 | 464 | # First interval 465 | (t0,y0) = points[0] 466 | (t1,y1) = points[1] 467 | (t2,y2) = points[2] 468 | if t1-t0 < 0.5: 469 | t0 = t1-1 470 | d = y0 471 | a = y1 472 | c = 3*d + tension*(y1-y0) 473 | b = 3*a - tension*(y2-y0) 474 | tfac = 1.0/(t1-t0) 475 | fcn.append((t0, t1, tfac, (a,b,c,d))) 476 | 477 | # Inner intervals 478 | for i in range(1,n-1): 479 | (t_1,y_1) = points[i-1] 480 | (t0,y0) = points[i] 481 | (t1,y1) = points[i+1] 482 | (t2,y2) = points[i+2] 483 | d = y0 484 | a = y1 485 | c = 3*d + tension*(y1-y_1) 486 | b = 3*a - tension*(y2-y0) 487 | tfac = 1.0/(t1-t0) 488 | fcn.append((t0, t1, tfac, (a,b,c,d))) 489 | 490 | # Last interval 491 | (t_1,y_1) = points[n-2] 492 | (t0,y0) = points[n-1] 493 | (t1,y1) = points[n] 494 | if t1-t0 < 0.5: 495 | t1 = t0+1 496 | d = y0 497 | a = y1 498 | c = 3*d + tension*(y1-y_1) 499 | b = 3*a - tension*(y1-y0) 500 | tfac = 1.0/(t1-t0) 501 | fcn.append((t0, t1, tfac, (a,b,c,d))) 502 | 503 | return fcn 504 | 505 | def evalCatmullRom(t, fcn): 506 | (t0, t1, tfac, params) = fcn[0] 507 | if t < t0: 508 | return evalCRInterval(t, t0, t1, tfac, params) 509 | for (t0, t1, tfac, params) in fcn: 510 | if t >= t0 and t < t1: 511 | return evalCRInterval(t, t0, t1, tfac, params) 512 | return evalCRInterval(t, t0, t1, tfac, params) 513 | 514 | def evalCRInterval(t, t0, t1, tfac, params): 515 | (a,b,c,d) = params 516 | x = tfac*(t-t0) 517 | x1 = 1-x 518 | f = x*x*(a*x + b*x1) + x1*x1*(c*x+d*x1) 519 | return f 520 | 521 | #---------------------------------------------------------- 522 | # Initialize 523 | #---------------------------------------------------------- 524 | 525 | classes = [ 526 | MCP_OT_StartEdit, 527 | MCP_OT_UndoEdit, 528 | MCP_OT_ConfirmEdit, 529 | MCP_OT_InsertKey, 530 | MCP_OT_MoveToMarker, 531 | ] 532 | 533 | def initialize(): 534 | for cls in classes: 535 | bpy.utils.register_class(cls) 536 | 537 | 538 | def uninitialize(): 539 | for cls in classes: 540 | bpy.utils.unregister_class(cls) 541 | -------------------------------------------------------------------------------- /loop.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | from math import pi, sqrt 33 | from mathutils import * 34 | from . import load, simplify, props, action 35 | from .utils import * 36 | 37 | # 38 | # normalizeRotCurves(scn, rig, fcurves, frames) 39 | # 40 | 41 | 42 | def normalizeRotCurves(scn, rig, fcurves, frames): 43 | hasQuat = {} 44 | for fcu in fcurves: 45 | (name, mode) = fCurveIdentity(fcu) 46 | if mode == 'rotation_quaternion': 47 | hasQuat[name] = rig.pose.bones[name] 48 | 49 | nFrames = len(frames) 50 | for n,frame in enumerate(frames): 51 | scn.frame_set(frame) 52 | showProgress(n, frame, nFrames) 53 | for (name, pb) in hasQuat.items(): 54 | pb.rotation_quaternion.normalize() 55 | pb.keyframe_insert("rotation_quaternion", group=name) 56 | 57 | # 58 | # loopFCurves(context): 59 | # loopFCurve(fcu, minTime, maxTime, scn): 60 | # class MCP_OT_LoopFCurves(bpy.types.Operator): 61 | # 62 | 63 | def loopFCurves(context): 64 | scn = context.scene 65 | rig = context.object 66 | act = getAction(rig) 67 | if not act: 68 | return 69 | (fcurves, minTime, maxTime) = simplify.getActionFCurves(act, False, True, scn) 70 | if not fcurves: 71 | return 72 | 73 | frames = getActiveFrames(rig, minTime, maxTime) 74 | nFrames = len(frames) 75 | normalizeRotCurves(scn, rig, fcurves, frames) 76 | 77 | hasLocation = {} 78 | for n,fcu in enumerate(fcurves): 79 | (name, mode) = fCurveIdentity(fcu) 80 | if isRotation(mode): 81 | loopFCurve(fcu, minTime, maxTime, scn) 82 | 83 | if scn.McpLoopInPlace: 84 | iknames = [pb.name for pb in getIkBoneList(rig)] 85 | ikbones = {} 86 | for fcu in fcurves: 87 | (name, mode) = fCurveIdentity(fcu) 88 | if isLocation(mode) and name in iknames: 89 | ikbones[name] = rig.pose.bones[name] 90 | 91 | for pb in ikbones.values(): 92 | print("IK bone %s" % pb.name) 93 | scn.frame_set(minTime) 94 | head0 = pb.head.copy() 95 | scn.frame_set(maxTime) 96 | head1 = pb.head.copy() 97 | offs = (head1-head0)/(maxTime-minTime) 98 | 99 | restMat = pb.bone.matrix_local.to_3x3() 100 | restInv = restMat.inverted() 101 | 102 | heads = {} 103 | for n,frame in enumerate(frames): 104 | scn.frame_set(frame) 105 | showProgress(n, frame, nFrames) 106 | heads[frame] = pb.head.copy() 107 | 108 | for n,frame in enumerate(frames): 109 | showProgress(n, frame, nFrames) 110 | scn.frame_set(frame) 111 | head = heads[frame] - (frame-minTime)*offs 112 | diff = head - pb.bone.head_local 113 | pb.location = Mult2(restInv, diff) 114 | pb.keyframe_insert("location", group=pb.name) 115 | 116 | return 117 | for fcu in fcurves: 118 | (name, mode) = fCurveIdentity(fcu) 119 | if isLocation(mode): 120 | loopFCurve(fcu, minTime, maxTime, scn) 121 | 122 | 123 | def loopFCurve(fcu, t0, tn, scn): 124 | delta = scn.McpLoopBlendRange 125 | 126 | v0 = fcu.evaluate(t0) 127 | vn = fcu.evaluate(tn) 128 | fcu.keyframe_points.insert(frame=t0, value=v0) 129 | fcu.keyframe_points.insert(frame=tn, value=vn) 130 | (mode, upper, lower, diff) = simplify.getFCurveLimits(fcu) 131 | if mode == 'location': 132 | dv = vn-v0 133 | else: 134 | dv = 0.0 135 | 136 | newpoints = [] 137 | for dt in range(delta): 138 | eps = 0.5*(1-dt/delta) 139 | 140 | t1 = t0+dt 141 | v1 = fcu.evaluate(t1) 142 | tm = tn+dt 143 | vm = fcu.evaluate(tm) - dv 144 | if (v1 > upper) and (vm < lower): 145 | vm += diff 146 | elif (v1 < lower) and (vm > upper): 147 | vm -= diff 148 | pt1 = (t1, (eps*vm + (1-eps)*v1)) 149 | 150 | t1 = t0-dt 151 | v1 = fcu.evaluate(t1) + dv 152 | tm = tn-dt 153 | vm = fcu.evaluate(tm) 154 | if (v1 > upper) and (vm < lower): 155 | v1 -= diff 156 | elif (v1 < lower) and (vm > upper): 157 | v1 += diff 158 | ptm = (tm, eps*v1 + (1-eps)*vm) 159 | 160 | newpoints.extend([pt1,ptm]) 161 | 162 | newpoints.sort() 163 | for (t,v) in newpoints: 164 | fcu.keyframe_points.insert(frame=t, value=v) 165 | return 166 | 167 | class MCP_OT_LoopFCurves(bpy.types.Operator): 168 | bl_idname = "mcp.loop_fcurves" 169 | bl_label = "Loop F-curves" 170 | bl_description = "Make the beginning and end of the selected time range connect smoothly. Use before repeating." 171 | bl_options = {'UNDO'} 172 | 173 | def execute(self, context): 174 | try: 175 | startProgress("Loop F-curves") 176 | loopFCurves(context) 177 | endProgress("F-curves looped") 178 | except MocapError: 179 | bpy.ops.mcp.error('INVOKE_DEFAULT') 180 | return{'FINISHED'} 181 | 182 | # 183 | # repeatFCurves(context, nRepeats): 184 | # 185 | 186 | def repeatFCurves(context, nRepeats): 187 | startProgress("Repeat F-curves %d times" % nRepeats) 188 | act = getAction(context.object) 189 | if not act: 190 | return 191 | (fcurves, minTime, maxTime) = simplify.getActionFCurves(act, False, True, context.scene) 192 | if not fcurves: 193 | return 194 | 195 | dt0 = maxTime-minTime 196 | for fcu in fcurves: 197 | (name, mode) = fCurveIdentity(fcu) 198 | dy0 = fcu.evaluate(maxTime) - fcu.evaluate(minTime) 199 | points = [] 200 | for kp in fcu.keyframe_points: 201 | t = kp.co[0] 202 | if t >= minTime and t < maxTime: 203 | points.append((t, kp.co[1])) 204 | for n in range(1,nRepeats): 205 | dt = n*dt0 206 | dy = n*dy0 207 | for (t,y) in points: 208 | fcu.keyframe_points.insert(t+dt, y+dy, options={'FAST'}) 209 | 210 | endProgress("F-curves repeated %d times" % nRepeats) 211 | 212 | 213 | class MCP_OT_RepeatFCurves(bpy.types.Operator): 214 | bl_idname = "mcp.repeat_fcurves" 215 | bl_label = "Repeat Animation" 216 | bl_description = "Repeat the part of the animation between selected markers n times" 217 | bl_options = {'UNDO'} 218 | 219 | def execute(self, context): 220 | try: 221 | repeatFCurves(context, context.scene.McpRepeatNumber) 222 | except MocapError: 223 | bpy.ops.mcp.error('INVOKE_DEFAULT') 224 | return{'FINISHED'} 225 | 226 | 227 | # 228 | # stitchActions(context): 229 | # 230 | 231 | def stitchActions(context): 232 | from .retarget import getLocks, correctMatrixForLocks 233 | 234 | action.listAllActions(context) 235 | scn = context.scene 236 | rig = context.object 237 | act1 = action.getAction(scn.McpFirstAction) 238 | act2 = action.getAction(scn.McpSecondAction) 239 | frame1 = scn.McpFirstEndFrame 240 | frame2 = scn.McpSecondStartFrame 241 | delta = scn.McpLoopBlendRange 242 | factor = 1.0/delta 243 | shift = frame1 - frame2 - delta 244 | 245 | if rig.animation_data: 246 | rig.animation_data.action = None 247 | 248 | first1,last1 = getActionExtent(act1) 249 | first2,last2 = getActionExtent(act2) 250 | frames1 = range(first1, frame1) 251 | frames2 = range(frame2, last2+1) 252 | frames = range(first1, last2+shift+1) 253 | bmats1,_ = getBaseMatrices(act1, frames1, rig, True) 254 | bmats2,useLoc = getBaseMatrices(act2, frames2, rig, True) 255 | 256 | deletes = [] 257 | for bname in bmats2.keys(): 258 | try: 259 | bmats1[bname] 260 | except KeyError: 261 | deletes.append(bname) 262 | for bname in deletes: 263 | del bmats2[bname] 264 | 265 | orders = {} 266 | locks = {} 267 | for bname in bmats2.keys(): 268 | pb = rig.pose.bones[bname] 269 | orders[bname],locks[bname] = getLocks(pb, context) 270 | 271 | nFrames = len(frames) 272 | for n,frame in enumerate(frames): 273 | scn.frame_set(frame) 274 | showProgress(n, frame, nFrames) 275 | 276 | if frame <= frame1-delta: 277 | n1 = frame - first1 278 | for bname,mats in bmats1.items(): 279 | pb = rig.pose.bones[bname] 280 | mat = mats[n1] 281 | if useLoc[bname]: 282 | insertLocation(pb, mat) 283 | insertRotation(pb, mat) 284 | 285 | elif frame >= frame1: 286 | n2 = frame - frame1 287 | for bname,mats in bmats2.items(): 288 | pb = rig.pose.bones[bname] 289 | mat = mats[n2] 290 | if useLoc[bname]: 291 | insertLocation(pb, mat) 292 | insertRotation(pb, mat) 293 | 294 | else: 295 | n1 = frame - first1 296 | n2 = frame - frame1 + delta 297 | eps = factor*n2 298 | for bname,mats2 in bmats2.items(): 299 | pb = rig.pose.bones[bname] 300 | mats1 = bmats1[bname] 301 | mat1 = mats1[n1] 302 | mat2 = mats2[n2] 303 | mat = (1-eps)*mat1 + eps*mat2 304 | mat = correctMatrixForLocks(mat, orders[bname], locks[bname], pb, scn.McpUseLimits) 305 | if useLoc[bname]: 306 | insertLocation(pb, mat) 307 | insertRotation(pb, mat) 308 | 309 | setInterpolation(rig) 310 | act = rig.animation_data.action 311 | act.name = scn.McpOutputActionName 312 | 313 | 314 | def getActionExtent(act): 315 | first = 10000 316 | last = -10000 317 | for fcu in act.fcurves: 318 | t0 = int(fcu.keyframe_points[0].co[0]) 319 | t1 = int(fcu.keyframe_points[-1].co[0]) 320 | if t0 < first: 321 | first = t0 322 | if t1 > last: 323 | last = t1 324 | return first,last 325 | 326 | 327 | class MCP_OT_StitchActions(bpy.types.Operator): 328 | bl_idname = "mcp.stitch_actions" 329 | bl_label = "Stitch Actions" 330 | bl_description = "Stitch two action together seamlessly" 331 | bl_options = {'UNDO'} 332 | 333 | def execute(self, context): 334 | try: 335 | startProgress("Stitch actions") 336 | stitchActions(context) 337 | endProgress("Actions stitched") 338 | except MocapError: 339 | bpy.ops.mcp.error('INVOKE_DEFAULT') 340 | return{'FINISHED'} 341 | 342 | 343 | # 344 | # shiftBoneFCurves(rig, context): 345 | # class MCP_OT_ShiftBoneFCurves(bpy.types.Operator): 346 | # 347 | 348 | def getBaseMatrices(act, frames, rig, useAll): 349 | locFcurves = {} 350 | quatFcurves = {} 351 | eulerFcurves = {} 352 | for fcu in act.fcurves: 353 | (bname, mode) = fCurveIdentity(fcu) 354 | if bname in rig.pose.bones.keys(): 355 | pb = rig.pose.bones[bname] 356 | else: 357 | continue 358 | if useAll or pb.bone.select: 359 | if mode == "location": 360 | try: 361 | fcurves = locFcurves[bname] 362 | except KeyError: 363 | fcurves = locFcurves[bname] = [None,None,None] 364 | elif mode == "rotation_euler": 365 | try: 366 | fcurves = eulerFcurves[bname] 367 | except KeyError: 368 | fcurves = eulerFcurves[bname] = [None,None,None] 369 | elif mode == "rotation_quaternion": 370 | try: 371 | fcurves = quatFcurves[bname] 372 | except KeyError: 373 | fcurves = quatFcurves[bname] = [None,None,None,None] 374 | else: 375 | continue 376 | 377 | fcurves[fcu.array_index] = fcu 378 | 379 | basemats = {} 380 | useLoc = {} 381 | for bname,fcurves in eulerFcurves.items(): 382 | useLoc[bname] = False 383 | order = rig.pose.bones[bname].rotation_mode 384 | fcu0,fcu1,fcu2 = fcurves 385 | rmats = basemats[bname] = [] 386 | for frame in frames: 387 | euler = Euler((fcu0.evaluate(frame), fcu1.evaluate(frame), fcu2.evaluate(frame))) 388 | rmats.append(euler.to_matrix().to_4x4()) 389 | 390 | for bname,fcurves in quatFcurves.items(): 391 | useLoc[bname] = False 392 | fcu0,fcu1,fcu2,fcu3 = fcurves 393 | rmats = basemats[bname] = [] 394 | for frame in frames: 395 | quat = Quaternion((fcu0.evaluate(frame), fcu1.evaluate(frame), fcu2.evaluate(frame), fcu3.evaluate(frame))) 396 | rmats.append(quat.to_matrix().to_4x4()) 397 | 398 | for bname,fcurves in locFcurves.items(): 399 | useLoc[bname] = True 400 | fcu0,fcu1,fcu2 = fcurves 401 | tmats = [] 402 | for frame in frames: 403 | loc = (fcu0.evaluate(frame), fcu1.evaluate(frame), fcu2.evaluate(frame)) 404 | tmats.append(Matrix.Translation(loc)) 405 | try: 406 | rmats = basemats[bname] 407 | except KeyError: 408 | basemats[bname] = tmats 409 | rmats = None 410 | if rmats: 411 | mats = [] 412 | for n,rmat in enumerate(rmats): 413 | tmat = tmats[n] 414 | mats.append( Mult2(tmat, rmat) ) 415 | basemats[bname] = mats 416 | 417 | return basemats, useLoc 418 | 419 | 420 | def shiftBoneFCurves(rig, context): 421 | from .retarget import getLocks, correctMatrixForLocks 422 | 423 | scn = context.scene 424 | frames = [scn.frame_current] + getActiveFrames(rig) 425 | nFrames = len(frames) 426 | act = getAction(rig) 427 | if not act: 428 | return 429 | basemats, useLoc = getBaseMatrices(act, frames, rig, False) 430 | 431 | deltaMat = {} 432 | orders = {} 433 | locks = {} 434 | for bname,bmats in basemats.items(): 435 | pb = rig.pose.bones[bname] 436 | bmat = bmats[0] 437 | deltaMat[pb.name] = Mult2(pb.matrix_basis, bmat.inverted()) 438 | orders[pb.name], locks[pb.name] = getLocks(pb, context) 439 | 440 | for n,frame in enumerate(frames[1:]): 441 | scn.frame_set(frame) 442 | showProgress(n, frame, nFrames) 443 | for bname,bmats in basemats.items(): 444 | pb = rig.pose.bones[bname] 445 | mat = Mult2(deltaMat[pb.name], bmats[n+1]) 446 | mat = correctMatrixForLocks(mat, orders[bname], locks[bname], pb, scn.McpUseLimits) 447 | if useLoc[bname]: 448 | insertLocation(pb, mat) 449 | insertRotation(pb, mat) 450 | 451 | 452 | def printmat(mat): 453 | print(" (%.4f %.4f %.4f %.4f)" % tuple(mat.to_quaternion())) 454 | 455 | 456 | class MCP_OT_ShiftBoneFCurves(bpy.types.Operator): 457 | bl_idname = "mcp.shift_bone" 458 | bl_label = "Shift Animation" 459 | bl_description = "Shift the animation globally for selected boens" 460 | bl_options = {'UNDO'} 461 | 462 | def execute(self, context): 463 | try: 464 | startProgress("Shift animation") 465 | shiftBoneFCurves(context.object, context) 466 | endProgress("Animation shifted") 467 | except MocapError: 468 | bpy.ops.mcp.error('INVOKE_DEFAULT') 469 | return{'FINISHED'} 470 | 471 | 472 | def fixateBoneFCurves(rig, scn): 473 | act = getAction(rig) 474 | if not act: 475 | return 476 | 477 | frame = scn.frame_current 478 | minTime,maxTime = getMarkedTime(scn) 479 | if minTime is None: 480 | minTime = -1e6 481 | if maxTime is None: 482 | maxTime = 1e6 483 | fixArray = [False,False,False] 484 | if scn.McpFixX: 485 | fixArray[0] = True 486 | if scn.McpFixY: 487 | fixArray[1] = True 488 | if scn.McpFixZ: 489 | fixArray[2] = True 490 | 491 | for fcu in act.fcurves: 492 | (bname, mode) = fCurveIdentity(fcu) 493 | pb = rig.pose.bones[bname] 494 | if pb.bone.select and isLocation(mode) and fixArray[fcu.array_index]: 495 | value = fcu.evaluate(frame) 496 | for kp in fcu.keyframe_points: 497 | if kp.co[0] >= minTime and kp.co[0] <= maxTime: 498 | kp.co[1] = value 499 | 500 | 501 | class MCP_OT_FixateBoneFCurves(bpy.types.Operator): 502 | bl_idname = "mcp.fixate_bone" 503 | bl_label = "Fixate Bone Location" 504 | bl_description = "Keep bone location fixed (local coordinates)" 505 | bl_options = {'UNDO'} 506 | 507 | def execute(self, context): 508 | try: 509 | startProgress("Fixate bone locations") 510 | fixateBoneFCurves(context.object, context.scene) 511 | endProgress("Bone locations fixed") 512 | except MocapError: 513 | bpy.ops.mcp.error('INVOKE_DEFAULT') 514 | return{'FINISHED'} 515 | 516 | #---------------------------------------------------------- 517 | # Initialize 518 | #---------------------------------------------------------- 519 | 520 | classes = [ 521 | MCP_OT_LoopFCurves, 522 | MCP_OT_RepeatFCurves, 523 | MCP_OT_StitchActions, 524 | MCP_OT_ShiftBoneFCurves, 525 | MCP_OT_FixateBoneFCurves, 526 | ] 527 | 528 | def initialize(): 529 | for cls in classes: 530 | bpy.utils.register_class(cls) 531 | 532 | 533 | def uninitialize(): 534 | for cls in classes: 535 | bpy.utils.unregister_class(cls) 536 | -------------------------------------------------------------------------------- /props.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy, os 32 | from bpy.props import * 33 | 34 | # 35 | # ensureInited(context): 36 | # 37 | 38 | def ensureInited(context): 39 | try: 40 | context.scene.McpBvhScale 41 | inited = True 42 | except: 43 | inited = False 44 | if not inited: 45 | initInterface(context) 46 | return 47 | 48 | #---------------------------------------------------------- 49 | # Get path to My Documents 50 | #---------------------------------------------------------- 51 | 52 | def getMyDocuments(): 53 | import sys 54 | if sys.platform == 'win32': 55 | import winreg 56 | try: 57 | k = winreg.HKEY_CURRENT_USER 58 | for x in ['Software', 'Microsoft', 'Windows', 'CurrentVersion', 'Explorer', 'Shell Folders']: 59 | k = winreg.OpenKey(k, x) 60 | 61 | name, type = winreg.QueryValueEx(k, 'Personal') 62 | 63 | if type == 1: 64 | print("Found My Documents folder: %s" % name) 65 | return name 66 | except Exception as e: 67 | print("Did not find path to My Documents folder") 68 | 69 | return os.path.expanduser("~") 70 | 71 | 72 | def getMHDirectory(): 73 | return os.path.join(getMyDocuments(), "makehuman", "v1") 74 | 75 | 76 | def getMHBlenderDirectory(): 77 | return os.path.join(getMyDocuments(), "makehuman", "blendertools") 78 | 79 | 80 | def settingsFile(): 81 | outdir = os.path.join(getMHBlenderDirectory(), "settings") 82 | if not os.path.isdir(outdir): 83 | os.makedirs(outdir) 84 | return os.path.join(outdir, "mocap.defaults") 85 | 86 | #---------------------------------------------------------- 87 | # Load and save defaults 88 | #---------------------------------------------------------- 89 | 90 | def loadDefaults(context): 91 | if not context.scene: 92 | return 93 | filename = settingsFile() 94 | try: 95 | fp = open(filename, "r") 96 | except: 97 | print("Unable to open %s for reading" % filename) 98 | return 99 | for line in fp: 100 | words = line.split() 101 | if len(words) < 2: 102 | continue 103 | try: 104 | val = eval(words[1]) 105 | except: 106 | val = words[1] 107 | context.scene[words[0]] = val 108 | fp.close() 109 | print("Defaults loaded from %s" % filename) 110 | return 111 | 112 | # 113 | # saveDefaults(context): 114 | # 115 | 116 | def saveDefaults(context): 117 | if not context.scene: 118 | return 119 | filename = settingsFile() 120 | try: 121 | fp = open(filename, "w", encoding="utf-8", newline="\n") 122 | except: 123 | print("Unable to open %s for writing" % filename) 124 | return 125 | for (key,value) in context.scene.items(): 126 | if key[:3] == "Mcp": 127 | fp.write("%s %s\n" % (key, value)) 128 | fp.close() 129 | print("Defaults saved to %s" % filename) 130 | return 131 | 132 | 133 | ######################################################################## 134 | # 135 | # class MCP_OT_InitInterface(bpy.types.Operator): 136 | # class MCP_OT_SaveDefaults(bpy.types.Operator): 137 | # class MCP_OT_LoadDefaults(bpy.types.Operator): 138 | # 139 | 140 | class MCP_OT_InitInterface(bpy.types.Operator): 141 | bl_idname = "mcp.init_interface" 142 | bl_label = "Initialize" 143 | bl_options = {'UNDO'} 144 | 145 | def execute(self, context): 146 | initInterface(context) 147 | print("Interface initialized") 148 | return{"FINISHED"} 149 | 150 | 151 | class MCP_OT_SaveDefaults(bpy.types.Operator): 152 | bl_idname = "mcp.save_defaults" 153 | bl_label = "Save defaults" 154 | bl_options = {'UNDO'} 155 | 156 | def execute(self, context): 157 | saveDefaults(context) 158 | return{"FINISHED"} 159 | 160 | 161 | class MCP_OT_LoadDefaults(bpy.types.Operator): 162 | bl_idname = "mcp.load_defaults" 163 | bl_label = "Load defaults" 164 | bl_options = {'UNDO'} 165 | 166 | def execute(self, context): 167 | loadDefaults(context) 168 | return{"FINISHED"} 169 | 170 | # 171 | # class MCP_OT_CopyAnglesIK(bpy.types.Operator): 172 | # 173 | 174 | class MCP_OT_CopyAnglesIK(bpy.types.Operator): 175 | bl_idname = "mcp.copy_angles_fk_ik" 176 | bl_label = "Angles --> IK" 177 | bl_options = {'UNDO'} 178 | 179 | def execute(self, context): 180 | copyAnglesIK(context) 181 | print("Angles copied") 182 | return{"FINISHED"} 183 | 184 | 185 | # 186 | # readDirectory(directory, prefix): 187 | # class MCP_OT_Batch(bpy.types.Operator): 188 | # 189 | 190 | def readDirectory(directory, prefix): 191 | realdir = os.path.realpath(os.path.expanduser(directory)) 192 | files = os.listdir(realdir) 193 | n = len(prefix) 194 | paths = [] 195 | for fileName in files: 196 | (name, ext) = os.path.splitext(fileName) 197 | if name[:n] == prefix and ext == ".bvh": 198 | paths.append("%s/%s" % (realdir, fileName)) 199 | return paths 200 | 201 | 202 | class MCP_OT_Batch(bpy.types.Operator): 203 | bl_idname = "mcp.batch" 204 | bl_label = "Batch run" 205 | bl_options = {'UNDO'} 206 | 207 | def execute(self, context): 208 | paths = readDirectory(context.scene.McpDirectory, context.scene.McpPrefix) 209 | trgRig = context.object 210 | for filepath in paths: 211 | setActiveObject(context, trgRig) 212 | loadRetargetSimplify(context, filepath) 213 | return{"FINISHED"} 214 | 215 | 216 | #---------------------------------------------------------- 217 | # Initialize 218 | #---------------------------------------------------------- 219 | 220 | classes = [ 221 | MCP_OT_InitInterface, 222 | MCP_OT_SaveDefaults, 223 | MCP_OT_LoadDefaults, 224 | MCP_OT_CopyAnglesIK, 225 | MCP_OT_Batch, 226 | ] 227 | 228 | def initialize(): 229 | # Showing 230 | 231 | bpy.types.Scene.McpShowDetailSteps = BoolProperty( 232 | name="Detailed Steps", 233 | description="Show retarget steps", 234 | default=False) 235 | 236 | bpy.types.Scene.McpShowIK = BoolProperty( 237 | name="Inverse Kinematics", 238 | description="Show inverse kinematics", 239 | default=False) 240 | 241 | bpy.types.Scene.McpShowGlobal = BoolProperty( 242 | name="Global Edit", 243 | description="Show global edit", 244 | default=False) 245 | 246 | bpy.types.Scene.McpShowDisplace = BoolProperty( 247 | name="Local Edit", 248 | description="Show local edit", 249 | default=False) 250 | 251 | bpy.types.Scene.McpShowFeet = BoolProperty( 252 | name="Feet", 253 | description="Show feet", 254 | default=False) 255 | 256 | bpy.types.Scene.McpShowLoop = BoolProperty( 257 | name="Loop And Repeat", 258 | description="Show loop and repeat", 259 | default=False) 260 | 261 | bpy.types.Scene.McpShowStitch = BoolProperty( 262 | name="Stitching", 263 | description="Show stitching", 264 | default=False) 265 | 266 | bpy.types.Scene.McpShowDefaultSettings = BoolProperty( 267 | name="Default Settings", 268 | description="Show default settings", 269 | default=False) 270 | 271 | bpy.types.Scene.McpShowActions = BoolProperty( 272 | name="Manage Actions", 273 | description="Show manage actions", 274 | default=False) 275 | 276 | bpy.types.Scene.McpShowPosing = BoolProperty( 277 | name="Posing", 278 | description="Show posing", 279 | default=False) 280 | 281 | 282 | # Load and retarget 283 | 284 | bpy.types.Scene.McpBvhScale = FloatProperty( 285 | name="Scale", 286 | description="Scale the BVH by this value", 287 | min=0.0001, max=1000000.0, 288 | soft_min=0.001, soft_max=100.0, 289 | default=0.65) 290 | 291 | bpy.types.Scene.McpAutoScale = BoolProperty( 292 | name="Auto Scale", 293 | description="Rescale skeleton to match target", 294 | default=True) 295 | 296 | bpy.types.Scene.McpUseLimits = BoolProperty( 297 | name="Use Limits", 298 | description="Restrict angles to Limit Rotation constraints", 299 | default=True) 300 | 301 | bpy.types.Scene.McpStartFrame = IntProperty( 302 | name="Start Frame", 303 | description="Starting frame for the animation", 304 | default=1) 305 | 306 | bpy.types.Scene.McpEndFrame = IntProperty( 307 | name="Last Frame", 308 | description="Last frame for the animation", 309 | default=250) 310 | 311 | bpy.types.Scene.McpRot90Anim = BoolProperty( 312 | name="Rotate 90 deg", 313 | description="Rotate 90 degress so Z points up", 314 | default=True) 315 | 316 | bpy.types.Scene.McpClearLocks = BoolProperty( 317 | name="Unlock Rotation", 318 | description="Clear X and Z rotation locks", 319 | default=False) 320 | 321 | bpy.types.Scene.McpFlipYAxis = BoolProperty( 322 | name="Flix Y Axis", 323 | description="Rotate 180 degress so Y points down (for Ni-Mate)", 324 | default=False) 325 | 326 | bpy.types.Scene.McpDoSimplify = BoolProperty( 327 | name="Simplify FCurves", 328 | description="Simplify FCurves", 329 | default=True) 330 | 331 | bpy.types.Object.McpIsTargetRig = BoolProperty( 332 | name="Is Target Rig", 333 | default=False) 334 | 335 | bpy.types.Object.McpIsSourceRig = BoolProperty( 336 | name="Is Source Rig", 337 | default=False) 338 | 339 | # Subsample and rescale 340 | 341 | bpy.types.Scene.McpSubsample = BoolProperty( 342 | name="Subsample", 343 | default=True) 344 | 345 | bpy.types.Scene.McpSSFactor = IntProperty( 346 | name="Subsample Factor", 347 | description="Sample only every n:th frame", 348 | min=1, default=1) 349 | 350 | bpy.types.Scene.McpRescale = BoolProperty( 351 | name="Rescale", 352 | description="Rescale F-curves after loading", 353 | default=False) 354 | 355 | bpy.types.Scene.McpRescaleFactor = FloatProperty( 356 | name="Rescale Factor", 357 | description="Factor for rescaling time", 358 | min=0.01, max=100, default=1.0) 359 | 360 | bpy.types.Scene.McpDefaultSS = BoolProperty( 361 | name="Use default subsample", 362 | default=True) 363 | 364 | # Simplify 365 | 366 | bpy.types.Scene.McpSimplifyVisible = BoolProperty( 367 | name="Only Visible", 368 | description="Simplify only visible F-curves", 369 | default=False) 370 | 371 | bpy.types.Scene.McpSimplifyMarkers = BoolProperty( 372 | name="Only Between Markers", 373 | description="Simplify only between markers", 374 | default=False) 375 | 376 | bpy.types.Scene.McpErrorLoc = FloatProperty( 377 | name="Max Loc Error", 378 | description="Max error for location FCurves when doing simplification", 379 | min=0.001, 380 | default=0.01) 381 | 382 | bpy.types.Scene.McpErrorRot = FloatProperty( 383 | name="Max Rot Error", 384 | description="Max error for rotation (degrees) FCurves when doing simplification", 385 | min=0.001, 386 | default=0.1) 387 | 388 | # Inverse kinematics 389 | 390 | bpy.types.Scene.McpIkAdjustXY = BoolProperty( 391 | name="IK Adjust XY", 392 | description="Adjust XY coordinates of IK handle", 393 | default=True) 394 | 395 | bpy.types.Scene.McpFkIkArms = BoolProperty( 396 | name="Arms", 397 | description="Include arms in FK/IK snapping", 398 | default=False) 399 | 400 | bpy.types.Scene.McpFkIkLegs = BoolProperty( 401 | name="Legs", 402 | description="Include legs in FK/IK snapping", 403 | default=True) 404 | 405 | # Floor 406 | 407 | bpy.types.Scene.McpFloorLeft = BoolProperty( 408 | name="Left", 409 | description="Keep left foot above floor", 410 | default=True) 411 | 412 | bpy.types.Scene.McpFloorRight = BoolProperty( 413 | name="Right", 414 | description="Keep right foot above floor", 415 | default=True) 416 | 417 | bpy.types.Scene.McpFloorHips = BoolProperty( 418 | name="Hips", 419 | description="Also adjust character COM when keeping feet above floor", 420 | default=True) 421 | 422 | bpy.types.Scene.McpBendElbows = BoolProperty( 423 | name="Elbows", 424 | description="Keep elbow bending positive", 425 | default=True) 426 | 427 | bpy.types.Scene.McpBendKnees = BoolProperty( 428 | name="Knees", 429 | description="Keep knee bending positive", 430 | default=True) 431 | 432 | bpy.types.Scene.McpDoBendPositive = BoolProperty( 433 | name="Bend Positive", 434 | description="Ensure that elbow and knee bending is positive", 435 | default=True) 436 | 437 | # Loop 438 | 439 | bpy.types.Scene.McpLoopBlendRange = IntProperty( 440 | name="Blend Range", 441 | min=1, 442 | default=5) 443 | 444 | bpy.types.Scene.McpLoopInPlace = BoolProperty( 445 | name="Loop in place", 446 | description="Remove Location F-curves", 447 | default=False) 448 | 449 | bpy.types.Scene.McpLoopZInPlace = BoolProperty( 450 | name="In Place Affects Z", 451 | default=False) 452 | 453 | bpy.types.Scene.McpRepeatNumber = IntProperty( 454 | name="Repeat Number", 455 | min=1, 456 | default=1) 457 | 458 | bpy.types.Scene.McpFirstEndFrame = IntProperty( 459 | name="First End Frame", 460 | default=1) 461 | 462 | bpy.types.Scene.McpSecondStartFrame = IntProperty( 463 | name="Second Start Frame", 464 | default=1) 465 | 466 | bpy.types.Scene.McpActionTarget = EnumProperty( 467 | items = [('Stitch new', 'Stitch new', 'Stitch new'), 468 | ('Prepend second', 'Prepend second', 'Prepend second')], 469 | name = "Action Target") 470 | 471 | bpy.types.Scene.McpOutputActionName = StringProperty( 472 | name="Output Action Name", 473 | maxlen=24, 474 | default="Action") 475 | 476 | bpy.types.Scene.McpFixX = BoolProperty( 477 | name="X", 478 | description="Fix Local X Location", 479 | default=True) 480 | 481 | bpy.types.Scene.McpFixY = BoolProperty( 482 | name="Y", 483 | description="Fix Local Y Location", 484 | default=True) 485 | 486 | bpy.types.Scene.McpFixZ = BoolProperty( 487 | name="Z", 488 | description="Fix Local Z Location", 489 | default=True) 490 | 491 | # Edit 492 | 493 | bpy.types.Object.McpUndoAction = StringProperty( 494 | default="") 495 | 496 | bpy.types.Object.McpActionName = StringProperty( 497 | default="") 498 | 499 | # Props 500 | 501 | bpy.types.Scene.McpDirectory = StringProperty( 502 | name="Directory", 503 | description="Directory", 504 | maxlen=1024, 505 | default="") 506 | 507 | bpy.types.Scene.McpPrefix = StringProperty( 508 | name="Prefix", 509 | description="Prefix", 510 | maxlen=24, 511 | default="") 512 | 513 | # T_Pose 514 | 515 | bpy.types.Scene.McpAutoCorrectTPose = BoolProperty( 516 | name = "Auto Correct T-Pose", 517 | description = "Automatically F-curves to fit T-pose at frame 0", 518 | default = True) 519 | 520 | bpy.types.Object.McpTPoseDefined = BoolProperty( 521 | default = False) 522 | 523 | bpy.types.Object.McpTPoseFile = StringProperty( 524 | default = "") 525 | 526 | bpy.types.Object.McpArmatureName = StringProperty( 527 | default = "") 528 | 529 | bpy.types.Object.McpArmatureModifier = StringProperty( 530 | default = "") 531 | 532 | bpy.types.PoseBone.McpQuatW = FloatProperty( 533 | default = 1.0 ) 534 | 535 | bpy.types.PoseBone.McpQuatX = FloatProperty( 536 | default = 0.0 ) 537 | 538 | bpy.types.PoseBone.McpQuatY = FloatProperty( 539 | default = 0.0 ) 540 | 541 | bpy.types.PoseBone.McpQuatZ = FloatProperty( 542 | default = 0.0 ) 543 | 544 | # Source and Target 545 | 546 | bpy.types.Scene.McpAutoSourceRig = BoolProperty( 547 | name = "Auto Source Rig", 548 | description = "Detect source rig automatically", 549 | default = True) 550 | 551 | bpy.types.Scene.McpAutoTargetRig = BoolProperty( 552 | name = "Auto Target Rig", 553 | description = "Detect target rig automatically", 554 | default = True) 555 | 556 | bpy.types.Scene.McpMakeHumanTPose = BoolProperty( 557 | name = "MakeHuman T-pose", 558 | description = "Use MakeHuman T-pose for MakeHuman characters", 559 | default = True) 560 | 561 | bpy.types.Object.MhReverseHip = BoolProperty( 562 | name = "Reverse Hip", 563 | description = "The rig has a reverse hip", 564 | default = False) 565 | 566 | bpy.types.Scene.McpIgnoreHiddenLayers = BoolProperty( 567 | name = "Ignore Hidden Layers", 568 | description = "Ignore bones on hidden layers when identifying target rig", 569 | default = True) 570 | 571 | bpy.types.Scene.McpSaveTargetTPose = BoolProperty( 572 | name = "Save T-Pose", 573 | description = "Save the current pose as T-pose when saving target file", 574 | default = False) 575 | 576 | bpy.types.PoseBone.McpBone = StringProperty( 577 | description = "MakeHuman bone corresponding to this bone", 578 | default = "") 579 | 580 | bpy.types.PoseBone.McpParent = StringProperty( 581 | description = "Parent of this bone for retargeting purposes", 582 | default = "") 583 | 584 | 585 | # Manage actions 586 | 587 | bpy.types.Scene.McpFilterActions = BoolProperty( 588 | name="Filter", 589 | description="Filter action names", 590 | default=False) 591 | 592 | bpy.types.Scene.McpReallyDelete = BoolProperty( 593 | name="Really Delete", 594 | description="Delete button deletes action permanently", 595 | default=False) 596 | 597 | bpy.types.Scene.McpActions = EnumProperty( 598 | items = [], 599 | name = "Actions") 600 | 601 | bpy.types.Scene.McpFirstAction = EnumProperty( 602 | items = [], 603 | name = "First Action") 604 | 605 | bpy.types.Scene.McpSecondAction = EnumProperty( 606 | items = [], 607 | name = "Second Action") 608 | 609 | bpy.types.Object.McpArmature = StringProperty() 610 | bpy.types.Object.McpLimitsOn = BoolProperty(default=True) 611 | bpy.types.Object.McpChildOfsOn = BoolProperty(default=False) 612 | bpy.types.Object.MhAlpha8 = BoolProperty(default=False) 613 | 614 | for cls in classes: 615 | bpy.utils.register_class(cls) 616 | 617 | 618 | def uninitialize(): 619 | for cls in classes: 620 | bpy.utils.unregister_class(cls) 621 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | import bpy 32 | from bpy.props import * 33 | from math import sin, cos, atan, pi 34 | from mathutils import * 35 | 36 | Deg2Rad = pi/180 37 | Rad2Deg = 180/pi 38 | 39 | 40 | #------------------------------------------------------------- 41 | # Blender 2.8 compatibility 42 | #------------------------------------------------------------- 43 | 44 | if bpy.app.version < (2,80,0): 45 | 46 | HideViewport = "hide" 47 | DrawType = "draw_type" 48 | ShowXRay = "show_x_ray" 49 | 50 | def getCollection(context): 51 | return context.scene 52 | 53 | def getSceneObjects(context): 54 | return context.scene.objects 55 | 56 | def getSelected(ob): 57 | return ob.select 58 | 59 | def setSelected(ob, value): 60 | ob.select = value 61 | 62 | def setActiveObject(context, ob): 63 | scn = context.scene 64 | scn.objects.active = ob 65 | scn.update() 66 | 67 | def putOnHiddenLayer(ob, coll=None, hidden=None): 68 | ob.layers = 19*[False] + [True] 69 | 70 | def createHiddenCollection(context): 71 | return context.scene 72 | 73 | def inSceneLayer(context, ob): 74 | scn = context.scene 75 | for n in range(len(scn.layers)): 76 | if (ob.layers[n] and scn.layers[n]): 77 | return True 78 | return False 79 | 80 | def activateObject(context, ob): 81 | scn = context.scene 82 | for ob1 in scn.objects: 83 | ob1.select = False 84 | ob.select = True 85 | scn.objects.active = ob 86 | 87 | def Mult2(x, y): 88 | return x * y 89 | 90 | def Mult3(x, y, z): 91 | return x * y * z 92 | 93 | def Mult4(x, y, z, u): 94 | return x * y * z * u 95 | 96 | def splitLayout(layout, factor): 97 | return layout.split(factor) 98 | 99 | def deleteObject(context, ob): 100 | for scn in bpy.data.scenes: 101 | if ob in scn.objects.values(): 102 | scn.objects.unlink(ob) 103 | if ob.users == 0: 104 | bpy.data.objects.remove(ob) 105 | del ob 106 | 107 | else: 108 | 109 | HideViewport = "hide_viewport" 110 | DrawType = "display_type" 111 | ShowXRay = "show_in_front" 112 | 113 | def getCollection(context): 114 | return context.scene.collection 115 | 116 | def getSceneObjects(context): 117 | return context.view_layer.objects 118 | 119 | def getSelected(ob): 120 | return ob.select_get() 121 | 122 | def setSelected(ob, value): 123 | ob.select_set(value) 124 | 125 | def setActiveObject(context, ob): 126 | vly = context.view_layer 127 | vly.objects.active = ob 128 | vly.update() 129 | 130 | def putOnHiddenLayer(ob, coll=None, hidden=None): 131 | if coll: 132 | coll.objects.unlink(ob) 133 | if hidden: 134 | hidden.objects.link(ob) 135 | 136 | def createHiddenCollection(context): 137 | coll = bpy.data.collections.new(name="Hidden") 138 | context.scene.collection.children.link(coll) 139 | coll.hide_viewport = True 140 | coll.hide_render = True 141 | return coll 142 | 143 | def inSceneLayer(context, ob): 144 | coll = context.scene.collection 145 | return (ob in coll.objects.values()) 146 | 147 | def activateObject(context, ob): 148 | scn = context.scene 149 | for ob1 in scn.collection.objects: 150 | ob1.select_set(False) 151 | ob.select_set(True) 152 | context.view_layer.objects.active = ob 153 | 154 | def printActive(name, context): 155 | coll = context.scene.collection 156 | print(name, context.object, coll) 157 | sel = [ob for ob in coll.objects if ob.select_get()] 158 | print(" ", sel) 159 | 160 | def Mult2(x, y): 161 | return x @ y 162 | 163 | def Mult3(x, y, z): 164 | return x @ y @ z 165 | 166 | def Mult4(x, y, z, u): 167 | return x @ y @ z @ u 168 | 169 | def splitLayout(layout, factor): 170 | return layout.split(factor=factor) 171 | 172 | def deleteObject(context, ob): 173 | for coll in bpy.data.collections: 174 | if ob in coll.objects.values(): 175 | coll.objects.unlink(ob) 176 | if True or ob.users == 0: 177 | bpy.data.objects.remove(ob) 178 | del ob 179 | 180 | #------------------------------------------------------------- 181 | # 182 | #------------------------------------------------------------- 183 | 184 | def updateScene(context, updateDepsGraph=False): 185 | scn = context.scene 186 | if hasattr(scn, "update"): 187 | scn.update() 188 | if updateDepsGraph and bpy.app.version >= (2,80,0): 189 | depth = context.evaluated_depsgraph_get() 190 | depth.update() 191 | scn.frame_current = scn.frame_current 192 | 193 | # 194 | # printMat3(string, mat) 195 | # 196 | 197 | def printMat3(string, mat, pad=""): 198 | if not mat: 199 | print("%s None" % string) 200 | return 201 | print("%s " % string) 202 | mc = "%s [" % pad 203 | for m in range(3): 204 | s = mc 205 | for n in range(3): 206 | s += " %6.3f" % mat[m][n] 207 | print(s+"]") 208 | 209 | def printMat4(string, mat, pad=""): 210 | if not mat: 211 | print("%s None" % string) 212 | return 213 | print("%s%s " % (pad, string)) 214 | mc = "%s [" % pad 215 | for m in range(4): 216 | s = mc 217 | for n in range(4): 218 | s += " %6.3f" % mat[m][n] 219 | print(s+"]") 220 | 221 | # 222 | # quadDict(): 223 | # 224 | 225 | def quadDict(): 226 | return { 227 | 0: {}, 228 | 1: {}, 229 | 2: {}, 230 | 3: {}, 231 | } 232 | 233 | MhxLayers = 8*[True] + 8*[False] + 8*[True] + 8*[False] 234 | RigifyLayers = 27*[True] + 5*[False] 235 | 236 | # 237 | # Identify rig type 238 | # 239 | 240 | def hasAllBones(blist, rig): 241 | for bname in blist: 242 | if bname not in rig.pose.bones.keys(): 243 | return False 244 | return True 245 | 246 | def isMhxRig(rig): 247 | return hasAllBones(["foot.rev.L"], rig) 248 | 249 | def isMhOfficialRig(rig): 250 | return hasAllBones(["risorius03.R"], rig) 251 | 252 | def isMhx7Rig(rig): 253 | return hasAllBones(["FootRev_L"], rig) 254 | 255 | def isRigify(rig): 256 | return hasAllBones(["MCH-spine.flex"], rig) 257 | 258 | def isRigify2(rig): 259 | return hasAllBones(["MCH-upper_arm_ik.L"], rig) 260 | 261 | def isGenesis3(rig): 262 | return (hasAllBones(["abdomenLower", "lShldrBend"], rig) and 263 | not isGenesis(rig)) 264 | 265 | def isGenesis(rig): 266 | return hasAllBones(["abdomen2", "lShldr"], rig) 267 | 268 | def isMakeHumanRig(rig): 269 | return ("MhAlpha8" in rig.keys()) 270 | 271 | # 272 | # nameOrNone(string): 273 | # 274 | 275 | def nameOrNone(string): 276 | if string == "None": 277 | return None 278 | else: 279 | return string 280 | 281 | 282 | def canonicalName(string): 283 | return string.lower().replace(' ','_').replace('-','_') 284 | 285 | 286 | # 287 | # getRoll(bone): 288 | # 289 | 290 | def getRoll(bone): 291 | return getRollMat(bone.matrix_local) 292 | 293 | 294 | def getRollMat(mat): 295 | quat = mat.to_3x3().to_quaternion() 296 | if abs(quat.w) < 1e-4: 297 | roll = pi 298 | else: 299 | roll = -2*atan(quat.y/quat.w) 300 | return roll 301 | 302 | 303 | # 304 | # getTrgBone(b): 305 | # 306 | 307 | def getTrgBone(bname, rig): 308 | for pb in rig.pose.bones: 309 | if pb.McpBone == bname: 310 | return pb 311 | return None 312 | 313 | 314 | # 315 | # getIkBoneList(rig): 316 | # 317 | 318 | def getIkBoneList(rig): 319 | hips = getTrgBone('hips', rig) 320 | if hips is None: 321 | if isMhxRig(rig): 322 | hips = rig.pose.bones["root"] 323 | elif isRigify(rig): 324 | hips = rig.pose.bones["hips"] 325 | elif isRigify2(rig): 326 | hips = rig.pose.bones["torso"] 327 | else: 328 | for bone in rig.data.bones: 329 | if bone.parent is None: 330 | hips = bone 331 | break 332 | blist = [hips] 333 | for bname in ['hand.ik.L', 'hand.ik.R', 'foot.ik.L', 'foot.ik.R']: 334 | try: 335 | blist.append(rig.pose.bones[bname]) 336 | except KeyError: 337 | pass 338 | return blist 339 | 340 | # 341 | # getAction(ob): 342 | # 343 | 344 | def getAction(ob): 345 | try: 346 | return ob.animation_data.action 347 | except: 348 | print("%s has no action" % ob) 349 | return None 350 | 351 | # 352 | # deleteAction(act): 353 | # 354 | 355 | def deleteAction(act): 356 | act.use_fake_user = False 357 | if act.users == 0: 358 | bpy.data.actions.remove(act) 359 | else: 360 | print("%s has %d users" % (act, act.users)) 361 | 362 | # 363 | # copyAction(act1, name): 364 | # 365 | 366 | def copyAction(act1, name): 367 | act2 = bpy.data.actions.new(name) 368 | for fcu1 in act1.fcurves: 369 | fcu2 = act2.fcurves.new(fcu1.data_path, fcu1.array_index) 370 | for kp1 in fcu1.keyframe_points: 371 | fcu2.keyframe_points.insert(kp1.co[0], kp1.co[1], options={'FAST'}) 372 | return act2 373 | 374 | # 375 | # 376 | # 377 | 378 | def insertLocation(pb, mat): 379 | pb.location = mat.to_translation() 380 | pb.keyframe_insert("location", group=pb.name) 381 | 382 | 383 | def insertRotation(pb, mat): 384 | q = mat.to_quaternion() 385 | if pb.rotation_mode == 'QUATERNION': 386 | pb.rotation_quaternion = q 387 | pb.keyframe_insert("rotation_quaternion", group=pb.name) 388 | else: 389 | pb.rotation_euler = q.to_euler(pb.rotation_mode) 390 | pb.keyframe_insert("rotation_euler", group=pb.name) 391 | 392 | 393 | def isRotationMatrix(mat): 394 | mat = mat.to_3x3() 395 | prod = Mult2(mat, mat.transposed()) 396 | diff = prod - Matrix().to_3x3() 397 | for i in range(3): 398 | for j in range(3): 399 | if abs(diff[i][j]) > 1e-3: 400 | print("Not a rotation matrix") 401 | print(mat) 402 | print(prod) 403 | return False 404 | return True 405 | 406 | 407 | # 408 | # getActiveFrames(ob): 409 | # 410 | 411 | def getActiveFrames0(ob): 412 | active = {} 413 | if ob.animation_data is None: 414 | return active 415 | action = ob.animation_data.action 416 | if action is None: 417 | return active 418 | for fcu in action.fcurves: 419 | for kp in fcu.keyframe_points: 420 | active[kp.co[0]] = True 421 | return active 422 | 423 | 424 | def getActiveFrames(ob, minTime=None, maxTime=None): 425 | active = getActiveFrames0(ob) 426 | frames = list(active.keys()) 427 | frames.sort() 428 | if minTime is not None: 429 | while frames[0] < minTime: 430 | frames = frames[1:] 431 | if maxTime is not None: 432 | frames.reverse() 433 | while frames[0] > maxTime: 434 | frames = frames[1:] 435 | frames.reverse() 436 | return frames 437 | 438 | 439 | def getActiveFramesBetweenMarkers(ob, scn): 440 | minTime,maxTime = getMarkedTime(scn) 441 | if minTime is None: 442 | return getActiveFrames(ob) 443 | active = getActiveFrames0(ob) 444 | frames = [] 445 | for time in active.keys(): 446 | if time >= minTime and time <= maxTime: 447 | frames.append(time) 448 | frames.sort() 449 | return frames 450 | 451 | # 452 | # getMarkedTime(scn): 453 | # 454 | 455 | def getMarkedTime(scn): 456 | markers = [] 457 | for mrk in scn.timeline_markers: 458 | if mrk.select: 459 | markers.append(mrk.frame) 460 | markers.sort() 461 | if len(markers) >= 2: 462 | return (markers[0], markers[-1]) 463 | else: 464 | return (None, None) 465 | 466 | # 467 | # fCurveIdentity(fcu): 468 | # 469 | 470 | def fCurveIdentity(fcu): 471 | words = fcu.data_path.split('"') 472 | if len(words) < 2: 473 | return (None, None) 474 | name = words[1] 475 | words = fcu.data_path.split('.') 476 | mode = words[-1] 477 | return (name, mode) 478 | 479 | # 480 | # findFCurve(path, index, fcurves): 481 | # 482 | 483 | def findFCurve(path, index, fcurves): 484 | for fcu in fcurves: 485 | if (fcu.data_path == path and 486 | fcu.array_index == index): 487 | return fcu 488 | print('F-curve "%s" not found.' % path) 489 | return None 490 | 491 | 492 | def findBoneFCurve(pb, rig, index, mode='rotation'): 493 | if mode == 'rotation': 494 | if pb.rotation_mode == 'QUATERNION': 495 | mode = "rotation_quaternion" 496 | else: 497 | mode = "rotation_euler" 498 | path = 'pose.bones["%s"].%s' % (pb.name, mode) 499 | 500 | if rig.animation_data is None: 501 | return None 502 | action = rig.animation_data.action 503 | if action is None: 504 | return None 505 | return findFCurve(path, index, action.fcurves) 506 | 507 | 508 | def fillKeyFrames(pb, rig, frames, nIndices, mode='rotation'): 509 | for index in range(nIndices): 510 | fcu = findBoneFCurve(pb, rig, index, mode) 511 | if fcu is None: 512 | return 513 | for frame in frames: 514 | y = fcu.evaluate(frame) 515 | fcu.keyframe_points.insert(frame, y, options={'FAST'}) 516 | 517 | # 518 | # isRotation(mode): 519 | # isLocation(mode): 520 | # 521 | 522 | def isRotation(mode): 523 | return (mode[0:3] == 'rot') 524 | 525 | def isLocation(mode): 526 | return (mode[0:3] == 'loc') 527 | 528 | 529 | # 530 | # setRotation(pb, mat, frame, group): 531 | # 532 | 533 | def setRotation(pb, rot, frame, group): 534 | if pb.rotation_mode == 'QUATERNION': 535 | try: 536 | quat = rot.to_quaternion() 537 | except: 538 | quat = rot 539 | pb.rotation_quaternion = quat 540 | pb.keyframe_insert('rotation_quaternion', frame=frame, group=group) 541 | else: 542 | try: 543 | euler = rot.to_euler(pb.rotation_mode) 544 | except: 545 | euler = rot 546 | pb.rotation_euler = euler 547 | pb.keyframe_insert('rotation_euler', frame=frame, group=group) 548 | 549 | 550 | # 551 | # putInRestPose(rig, useSetKeys): 552 | # 553 | 554 | def putInRestPose(rig, useSetKeys): 555 | for pb in rig.pose.bones: 556 | pb.matrix_basis = Matrix() 557 | if useSetKeys: 558 | if pb.rotation_mode == 'QUATERNION': 559 | pb.keyframe_insert('rotation_quaternion') 560 | else: 561 | pb.keyframe_insert('rotation_euler') 562 | pb.keyframe_insert('location') 563 | 564 | # 565 | # setInterpolation(rig): 566 | # 567 | 568 | def setInterpolation(rig): 569 | if not rig.animation_data: 570 | return 571 | act = rig.animation_data.action 572 | if not act: 573 | return 574 | for fcu in act.fcurves: 575 | for pt in fcu.keyframe_points: 576 | pt.interpolation = 'LINEAR' 577 | fcu.extrapolation = 'CONSTANT' 578 | return 579 | 580 | # 581 | # insertRotationKeyFrame(pb, frame): 582 | # 583 | 584 | def insertRotationKeyFrame(pb, frame): 585 | rotMode = pb.rotation_mode 586 | grp = pb.name 587 | if rotMode == "QUATERNION": 588 | pb.keyframe_insert("rotation_quaternion", frame=frame, group=grp) 589 | elif rotMode == "AXIS_ANGLE": 590 | pb.keyframe_insert("rotation_axis_angle", frame=frame, group=grp) 591 | else: 592 | pb.keyframe_insert("rotation_euler", frame=frame, group=grp) 593 | 594 | # 595 | # checkObjectProblems(self, context): 596 | # 597 | 598 | def getObjectProblems(self, context): 599 | self.problems = "" 600 | epsilon = 1e-2 601 | rig = context.object 602 | 603 | eu = rig.rotation_euler 604 | print(eu) 605 | if abs(eu.x) + abs(eu.y) + abs(eu.z) > epsilon: 606 | self.problems += "object rotation\n" 607 | 608 | vec = rig.scale - Vector((1,1,1)) 609 | print(vec, vec.length) 610 | if vec.length > epsilon: 611 | self.problems += "object scaling\n" 612 | 613 | if self.problems: 614 | wm = context.window_manager 615 | return wm.invoke_props_dialog(self, width=300, height=20) 616 | else: 617 | return False 618 | 619 | 620 | def checkObjectProblems(self, context): 621 | problems = getObjectProblems(self, context) 622 | if problems: 623 | return problems 624 | else: 625 | return self.execute(context) 626 | 627 | 628 | def problemFreeFileSelect(self, context): 629 | problems = getObjectProblems(self, context) 630 | if problems: 631 | return problems 632 | context.window_manager.fileselect_add(self) 633 | return {'RUNNING_MODAL'} 634 | 635 | 636 | def drawObjectProblems(self): 637 | if self.problems: 638 | self.layout.label(text="MakeWalk cannot use this rig because it has:") 639 | for problem in self.problems.split("\n"): 640 | self.layout.label(text=" %s" % problem) 641 | self.layout.label(text="Apply object transformations before using MakeWalk") 642 | 643 | # 644 | # showProgress(n, frame): 645 | # 646 | 647 | def startProgress(string): 648 | print("%s (0 pct)" % string) 649 | 650 | 651 | def endProgress(string): 652 | print("%s (100 pct)" % string) 653 | 654 | 655 | def showProgress(n, frame, nFrames, step=20): 656 | if n % step == 0: 657 | print("%d (%.1f pct)" % (int(frame), (100.0*n)/nFrames)) 658 | 659 | # 660 | # 661 | # 662 | 663 | _category = "" 664 | _errorLines = "" 665 | 666 | def setCategory(string): 667 | global _category 668 | _category = string 669 | 670 | def clearCategory(): 671 | global _category 672 | _category = "General error" 673 | 674 | clearCategory() 675 | 676 | 677 | class MocapError(Exception): 678 | def __init__(self, value): 679 | global _errorLines 680 | self.value = value 681 | _errorLines = ( 682 | ["Category: %s" % _category] + 683 | value.split("\n") + 684 | ["" + 685 | "For corrective actions see:", 686 | "http://www.makehuman.org/doc/node/", 687 | " makewalk_errors_and_corrective_actions.html"] 688 | ) 689 | print("*** Mocap error ***") 690 | for line in _errorLines: 691 | print(line) 692 | 693 | def __str__(self): 694 | return repr(self.value) 695 | 696 | 697 | class ErrorOperator(bpy.types.Operator): 698 | bl_idname = "mcp.error" 699 | bl_label = "Mocap error" 700 | 701 | def execute(self, context): 702 | clearCategory() 703 | return {'RUNNING_MODAL'} 704 | 705 | def invoke(self, context, event): 706 | wm = context.window_manager 707 | return wm.invoke_props_dialog(self) 708 | 709 | def draw(self, context): 710 | global _errorLines 711 | for line in _errorLines: 712 | self.layout.label(text=line) 713 | 714 | -------------------------------------------------------------------------------- /retarget.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # BSD 2-Clause License 3 | # 4 | # Copyright (c) 2019, Thomas Larsson 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # ------------------------------------------------------------------------------ 28 | 29 | 30 | 31 | # 32 | # M_b = global bone matrix, relative world (PoseBone.matrix) 33 | # L_b = local bone matrix, relative parent and rest (PoseBone.matrix_local) 34 | # R_b = bone rest matrix, relative armature (Bone.matrix_local) 35 | # T_b = global T-pose marix, relative world 36 | # 37 | # M_b = M_p R_p^-1 R_b L_b 38 | # M_b = A_b M'_b 39 | # T_b = A_b T'_b 40 | # A_b = T_b T'^-1_b 41 | # B_b = R^-1_b R_p 42 | # 43 | # L_b = R^-1_b R_p M^-1_p A_b M'_b 44 | # L_b = B_b M^-1_p A_b M'_b 45 | # 46 | 47 | 48 | import bpy 49 | import mathutils 50 | import time 51 | from collections import OrderedDict 52 | from mathutils import * 53 | from bpy.props import * 54 | 55 | from .simplify import simplifyFCurves, rescaleFCurves 56 | from .utils import * 57 | from . import t_pose 58 | if bpy.app.version < (2,80,0): 59 | from .buttons27 import ProblemsString, LoadBVH 60 | else: 61 | from .buttons28 import ProblemsString, LoadBVH 62 | 63 | 64 | class CAnimation: 65 | 66 | def __init__(self, srcRig, trgRig, boneAssoc, context): 67 | self.srcRig = srcRig 68 | self.trgRig = trgRig 69 | self.scene = context.scene 70 | self.boneAnims = OrderedDict() 71 | 72 | for (trgName, srcName) in boneAssoc: 73 | try: 74 | trgBone = trgRig.pose.bones[trgName] 75 | srcBone = srcRig.pose.bones[srcName] 76 | except KeyError: 77 | print(" -", trgName, srcName) 78 | continue 79 | banim = self.boneAnims[trgName] = CBoneAnim(srcBone, trgBone, self, context) 80 | 81 | 82 | def printResult(self, scn, frame): 83 | scn.frame_set(frame) 84 | for name in ["LeftHip"]: 85 | banim = self.boneAnims[name] 86 | banim.printResult(frame) 87 | 88 | 89 | def setTPose(self, context): 90 | putInRestPose(self.srcRig, True) 91 | t_pose.setTPose(self.srcRig, context) 92 | putInRestPose(self.trgRig, True) 93 | t_pose.setTPose(self.trgRig, context) 94 | for banim in self.boneAnims.values(): 95 | banim.insertTPoseFrame() 96 | context.scene.frame_set(0) 97 | for banim in self.boneAnims.values(): 98 | banim.getTPoseMatrix() 99 | 100 | 101 | def retarget(self, frames, context): 102 | objects = hideObjects(context, self.srcRig) 103 | scn = context.scene 104 | try: 105 | for frame in frames: 106 | scn.frame_set(frame) 107 | for banim in self.boneAnims.values(): 108 | banim.retarget(frame) 109 | finally: 110 | unhideObjects(objects) 111 | 112 | 113 | class CBoneAnim: 114 | 115 | def __init__(self, srcBone, trgBone, anim, context): 116 | self.name = srcBone.name 117 | self.srcMatrices = {} 118 | self.trgMatrices = {} 119 | self.srcMatrix = None 120 | self.trgMatrix = None 121 | self.srcBone = srcBone 122 | self.trgBone = trgBone 123 | self.order,self.locks = getLocks(trgBone, context) 124 | self.aMatrix = None 125 | self.parent = self.getParent(trgBone, anim) 126 | if self.parent: 127 | self.trgBone.McpParent = self.parent.trgBone.name 128 | trgParent = self.parent.trgBone 129 | self.bMatrix = Mult2(trgBone.bone.matrix_local.inverted(), trgParent.bone.matrix_local) 130 | else: 131 | self.bMatrix = trgBone.bone.matrix_local.inverted() 132 | self.useLimits = anim.scene.McpUseLimits 133 | 134 | 135 | def __repr__(self): 136 | if self.parent: 137 | parname = self.parent.name 138 | else: 139 | parname = None 140 | return ( 141 | " %s\n" % (self.srcBone.name, self.trgBone.name) + 152 | "S %s\n" % self.srcBone.matrix + 153 | "T %s\n" % self.trgBone.matrix + 154 | "R %s\n" % Mult2(self.trgBone.matrix, self.srcBone.matrix.inverted()) 155 | ) 156 | 157 | 158 | def getParent(self, pb, anim): 159 | pb = pb.parent 160 | while pb: 161 | if pb.McpBone: 162 | try: 163 | return anim.boneAnims[pb.name] 164 | except KeyError: 165 | pass 166 | 167 | subtar = None 168 | for cns in pb.constraints: 169 | if (cns.type[0:4] == "COPY" and 170 | cns.type != "COPY_SCALE" and 171 | cns.influence > 0.8): 172 | subtar = cns.subtarget 173 | 174 | if subtar: 175 | pb = anim.trgRig.pose.bones[subtar] 176 | else: 177 | pb = pb.parent 178 | return None 179 | 180 | 181 | def insertKeyFrame(self, mat, frame): 182 | pb = self.trgBone 183 | setRotation(pb, mat, frame, pb.name) 184 | if not self.parent: 185 | pb.location = mat.to_translation() 186 | pb.keyframe_insert("location", frame=frame, group=pb.name) 187 | 188 | 189 | def insertTPoseFrame(self): 190 | mat = t_pose.getStoredBonePose(self.trgBone) 191 | self.insertKeyFrame(mat, 0) 192 | 193 | 194 | def getTPoseMatrix(self): 195 | self.aMatrix = Mult2(self.srcBone.matrix.inverted(), self.trgBone.matrix) 196 | if not isRotationMatrix(self.trgBone.matrix): 197 | raise RuntimeError("Target %s not rotation matrix %s" % (self.trgBone.name, self.trgBone.matrix)) 198 | if not isRotationMatrix(self.srcBone.matrix): 199 | raise RuntimeError("Source %s not rotation matrix %s" % (self.srcBone.name, self.srcBone.matrix)) 200 | if not isRotationMatrix(self.aMatrix): 201 | raise RuntimeError("A %s not rotation matrix %s" % (self.trgBone.name, self.aMatrix.matrix)) 202 | 203 | 204 | def retarget(self, frame): 205 | self.srcMatrix = self.srcBone.matrix.copy() 206 | self.trgMatrix = Mult2(self.srcMatrix, self.aMatrix) 207 | self.trgMatrix.col[3] = self.srcMatrix.col[3] 208 | if self.parent: 209 | mat1 = Mult2(self.parent.trgMatrix.inverted(), self.trgMatrix) 210 | else: 211 | mat1 = self.trgMatrix 212 | mat2 = Mult2(self.bMatrix, mat1) 213 | mat3 = correctMatrixForLocks(mat2, self.order, self.locks, self.trgBone, self.useLimits) 214 | self.insertKeyFrame(mat3, frame) 215 | 216 | self.srcMatrices[frame] = self.srcMatrix 217 | mat1 = Mult2(self.bMatrix.inverted(), mat3) 218 | if self.parent: 219 | self.trgMatrix = Mult2(self.parent.trgMatrix, mat1) 220 | else: 221 | self.trgMatrix = mat1 222 | self.trgMatrices[frame] = self.trgMatrix 223 | 224 | return 225 | 226 | if self.name == "upper_arm.L": 227 | print() 228 | print(self) 229 | print("S ", self.srcMatrix) 230 | print("T ", self.trgMatrix) 231 | print(self.parent.name) 232 | print("TP", self.parent.trgMatrix) 233 | print("M1", mat1) 234 | print("M2", mat2) 235 | print("MB2", self.trgBone.matrix) 236 | 237 | 238 | def getLocks(pb, context): 239 | scn = context.scene 240 | locks = [] 241 | order = 'XYZ' 242 | if scn.McpClearLocks: 243 | pb.lock_rotation[0] = pb.lock_rotation[2] = False 244 | for cns in pb.constraints: 245 | if cns.type == 'LIMIT_ROTATION': 246 | cns.use_limit_x = cns.use_limit_z = 0 247 | 248 | if pb.lock_rotation[1]: 249 | locks.append(1) 250 | order = 'YZX' 251 | if pb.lock_rotation[0]: 252 | order = 'YXZ' 253 | locks.append(0) 254 | if pb.lock_rotation[2]: 255 | locks.append(2) 256 | elif pb.lock_rotation[2]: 257 | locks.append(2) 258 | order = 'ZYX' 259 | if pb.lock_rotation[0]: 260 | order = 'ZXY' 261 | locks.append(0) 262 | elif pb.lock_rotation[0]: 263 | locks.append(0) 264 | order = 'XYZ' 265 | 266 | if pb.rotation_mode != 'QUATERNION': 267 | order = pb.rotation_mode 268 | 269 | return order,locks 270 | 271 | 272 | def correctMatrixForLocks(mat, order, locks, pb, useLimits): 273 | head = Vector(mat.col[3]) 274 | 275 | if locks: 276 | euler = mat.to_3x3().to_euler(order) 277 | for n in locks: 278 | euler[n] = 0 279 | mat = euler.to_matrix().to_4x4() 280 | 281 | if not useLimits: 282 | mat.col[3] = head 283 | return mat 284 | 285 | for cns in pb.constraints: 286 | if (cns.type == 'LIMIT_ROTATION' and 287 | cns.owner_space == 'LOCAL' and 288 | not cns.mute and 289 | cns.influence > 0.5): 290 | euler = mat.to_3x3().to_euler(order) 291 | if cns.use_limit_x: 292 | euler.x = min(cns.max_x, max(cns.min_x, euler.x)) 293 | if cns.use_limit_y: 294 | euler.y = min(cns.max_y, max(cns.min_y, euler.y)) 295 | if cns.use_limit_z: 296 | euler.z = min(cns.max_z, max(cns.min_z, euler.z)) 297 | mat = euler.to_matrix().to_4x4() 298 | 299 | mat.col[3] = head 300 | return mat 301 | 302 | 303 | def hideObjects(context, rig): 304 | if bpy.app.version >= (2,80,0): 305 | return None 306 | objects = [] 307 | for ob in getSceneObjects(context): 308 | if ob != rig: 309 | objects.append((ob, list(ob.layers))) 310 | ob.layers = 20*[False] 311 | return objects 312 | 313 | 314 | def unhideObjects(objects): 315 | if bpy.app.version >= (2,80,0): 316 | return 317 | for (ob,layers) in objects: 318 | ob.layers = layers 319 | 320 | 321 | def clearMcpProps(rig): 322 | keys = list(rig.keys()) 323 | for key in keys: 324 | if key[0:3] == "Mcp": 325 | del rig[key] 326 | 327 | for pb in rig.pose.bones: 328 | keys = list(pb.keys()) 329 | for key in keys: 330 | if key[0:3] == "Mcp": 331 | del pb[key] 332 | 333 | 334 | def retargetAnimation(context, srcRig, trgRig): 335 | from . import source, target 336 | from .fkik import setMhxIk, setRigifyFKIK, setRigify2FKIK 337 | 338 | startProgress("Retargeting") 339 | scn = context.scene 340 | setMhxIk(trgRig, True, True, 0.0) 341 | frames = getActiveFrames(srcRig) 342 | nFrames = len(frames) 343 | setActiveObject(context, trgRig) 344 | if trgRig.animation_data: 345 | trgRig.animation_data.action = None 346 | 347 | if isRigify(trgRig): 348 | setRigifyFKIK(trgRig, 0.0) 349 | elif isRigify2(trgRig): 350 | setRigify2FKIK(trgRig, 1.0) 351 | 352 | try: 353 | scn.frame_current = frames[0] 354 | except: 355 | raise MocapError("No frames found.") 356 | oldData = changeTargetData(trgRig, scn) 357 | 358 | source.ensureSourceInited(scn) 359 | source.setArmature(srcRig, scn) 360 | print("Retarget %s --> %s" % (srcRig.name, trgRig.name)) 361 | 362 | target.ensureTargetInited(scn) 363 | boneAssoc = target.getTargetArmature(trgRig, context) 364 | anim = CAnimation(srcRig, trgRig, boneAssoc, context) 365 | anim.setTPose(context) 366 | 367 | setCategory("Retarget") 368 | frameBlock = frames[0:100] 369 | index = 0 370 | try: 371 | while frameBlock: 372 | showProgress(index, frames[index], nFrames) 373 | anim.retarget(frameBlock, context) 374 | index += 100 375 | frameBlock = frames[index:index+100] 376 | 377 | scn.frame_current = frames[0] 378 | finally: 379 | restoreTargetData(trgRig, oldData) 380 | 381 | #anim.printResult(scn, 1) 382 | 383 | setInterpolation(trgRig) 384 | act = trgRig.animation_data.action 385 | act.name = trgRig.name[:4] + srcRig.name[2:] 386 | act.use_fake_user = True 387 | clearCategory() 388 | endProgress("Retargeted %s --> %s" % (srcRig.name, trgRig.name)) 389 | 390 | 391 | # 392 | # changeTargetData(rig, scn): 393 | # restoreTargetData(rig, data): 394 | # 395 | 396 | def changeTargetData(rig, scn): 397 | tempProps = [ 398 | ("MhaRotationLimits", 0.0), 399 | ("MhaArmIk_L", 0.0), 400 | ("MhaArmIk_R", 0.0), 401 | ("MhaLegIk_L", 0.0), 402 | ("MhaLegIk_R", 0.0), 403 | ("MhaSpineIk", 0), 404 | ("MhaSpineInvert", 0), 405 | ("MhaElbowPlant_L", 0), 406 | ("MhaElbowPlant_R", 0), 407 | ] 408 | 409 | props = [] 410 | for (key, value) in tempProps: 411 | try: 412 | props.append((key, rig[key])) 413 | rig[key] = value 414 | except KeyError: 415 | pass 416 | 417 | permProps = [ 418 | ("MhaElbowFollowsShoulder", 0), 419 | ("MhaElbowFollowsWrist", 0), 420 | ("MhaKneeFollowsHip", 0), 421 | ("MhaKneeFollowsFoot", 0), 422 | ("MhaArmHinge", 0), 423 | ("MhaLegHinge", 0), 424 | ] 425 | 426 | for (key, value) in permProps: 427 | try: 428 | rig[key+"_L"] 429 | rig[key+"_L"] = value 430 | rig[key+"_R"] = value 431 | except KeyError: 432 | pass 433 | 434 | layers = list(rig.data.layers) 435 | if rig.MhAlpha8: 436 | rig.data.layers = MhxLayers 437 | elif isRigify(rig): 438 | rig.data.layers = RigifyLayers 439 | 440 | locks = [] 441 | for pb in rig.pose.bones: 442 | constraints = [] 443 | if not scn.McpUseLimits: 444 | for cns in pb.constraints: 445 | if cns.type == 'LIMIT_DISTANCE': 446 | cns.mute = True 447 | elif cns.type[0:5] == 'LIMIT': 448 | constraints.append( (cns, cns.mute) ) 449 | cns.mute = True 450 | locks.append( (pb, constraints) ) 451 | 452 | norotBones = [] 453 | return (props, layers, locks, norotBones) 454 | 455 | 456 | def restoreTargetData(rig, data): 457 | (props, rig.data.layers, locks, norotBones) = data 458 | 459 | for (key,value) in props: 460 | rig[key] = value 461 | 462 | for b in norotBones: 463 | b.use_inherit_rotation = True 464 | 465 | for lock in locks: 466 | (pb, constraints) = lock 467 | for (cns, mute) in constraints: 468 | cns.mute = mute 469 | 470 | 471 | # 472 | # loadRetargetSimplify(context, filepath): 473 | # 474 | 475 | def loadRetargetSimplify(context, filepath): 476 | from . import load 477 | from .fkik import limbsBendPositive 478 | 479 | print("\nLoad and retarget %s" % filepath) 480 | time1 = time.clock() 481 | scn = context.scene 482 | trgRig = context.object 483 | data = changeTargetData(trgRig, scn) 484 | try: 485 | #clearMcpProps(trgRig) 486 | srcRig = load.readBvhFile(context, filepath, scn, False) 487 | try: 488 | load.renameAndRescaleBvh(context, srcRig, trgRig) 489 | retargetAnimation(context, srcRig, trgRig) 490 | scn = context.scene 491 | if scn.McpDoBendPositive: 492 | limbsBendPositive(trgRig, True, True, (0,1e6)) 493 | if scn.McpDoSimplify: 494 | simplifyFCurves(context, trgRig, False, False) 495 | if scn.McpRescale: 496 | rescaleFCurves(context, trgRig, scn.McpRescaleFactor) 497 | finally: 498 | load.deleteSourceRig(context, srcRig, 'Y_') 499 | finally: 500 | restoreTargetData(trgRig, data) 501 | time2 = time.clock() 502 | print("%s finished in %.3f s" % (filepath, time2-time1)) 503 | return 504 | 505 | 506 | ######################################################################## 507 | # 508 | # Buttons 509 | # 510 | 511 | class MCP_OT_RetargetMhx(bpy.types.Operator, ProblemsString): 512 | bl_idname = "mcp.retarget_mhx" 513 | bl_label = "Retarget Selected To Active" 514 | bl_description = "Retarget animation to the active (target) armature from the other selected (source) armature" 515 | bl_options = {'UNDO'} 516 | 517 | def execute(self, context): 518 | from . import target 519 | 520 | if self.problems: 521 | return{'FINISHED'} 522 | 523 | trgRig = context.object 524 | scn = context.scene 525 | data = changeTargetData(trgRig, scn) 526 | rigList = list(context.selected_objects) 527 | 528 | try: 529 | target.getTargetArmature(trgRig, context) 530 | for srcRig in rigList: 531 | if srcRig != trgRig: 532 | retargetAnimation(context, srcRig, trgRig) 533 | except MocapError: 534 | bpy.ops.mcp.error('INVOKE_DEFAULT') 535 | finally: 536 | restoreTargetData(trgRig, data) 537 | return{'FINISHED'} 538 | 539 | def invoke(self, context, event): 540 | return checkObjectProblems(self, context) 541 | 542 | def draw(self, context): 543 | drawObjectProblems(self) 544 | 545 | 546 | class MCP_OT_LoadAndRetarget(bpy.types.Operator, ProblemsString, LoadBVH): 547 | bl_idname = "mcp.load_and_retarget" 548 | bl_label = "Load And Retarget" 549 | bl_description = "Load animation from bvh file to the active armature" 550 | bl_options = {'UNDO'} 551 | 552 | @classmethod 553 | def poll(self, context): 554 | return (context.object and context.object.type == 'ARMATURE') 555 | 556 | def execute(self, context): 557 | if self.problems: 558 | return{'FINISHED'} 559 | 560 | try: 561 | loadRetargetSimplify(context, self.properties.filepath) 562 | except MocapError: 563 | bpy.ops.mcp.error('INVOKE_DEFAULT') 564 | return{'FINISHED'} 565 | 566 | def invoke(self, context, event): 567 | return problemFreeFileSelect(self, context) 568 | 569 | def draw(self, context): 570 | drawObjectProblems(self) 571 | 572 | 573 | class MCP_OT_ClearTempProps(bpy.types.Operator): 574 | bl_idname = "mcp.clear_temp_props" 575 | bl_label = "Clear Temporary Properties" 576 | bl_description = "Clear properties used by MakeWalk. Animation editing may fail after this." 577 | bl_options = {'UNDO'} 578 | 579 | def execute(self, context): 580 | try: 581 | clearMcpProps(context.object) 582 | except MocapError: 583 | bpy.ops.mcp.error('INVOKE_DEFAULT') 584 | return{'FINISHED'} 585 | 586 | #---------------------------------------------------------- 587 | # Initialize 588 | #---------------------------------------------------------- 589 | 590 | classes = [ 591 | MCP_OT_RetargetMhx, 592 | MCP_OT_LoadAndRetarget, 593 | MCP_OT_ClearTempProps, 594 | ] 595 | 596 | def initialize(): 597 | for cls in classes: 598 | bpy.utils.register_class(cls) 599 | 600 | 601 | def uninitialize(): 602 | for cls in classes: 603 | bpy.utils.unregister_class(cls) 604 | --------------------------------------------------------------------------------