├── .gitignore ├── LICENSE ├── README.md ├── bodylabs_rigger ├── __init__.py ├── bodykit │ ├── __init__.py │ └── mesh_generator.py ├── factory.py ├── fbx_util.py ├── joint_positions.py ├── rig_assets.py └── static │ ├── __init__.py │ └── rig_assets.json ├── examples └── meshes_from_bodykit.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | venv/ 3 | 4 | bodylabs_rigger.egg-info/ 5 | build/ 6 | dist/ 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Body Labs, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Body Labs mesh rigger 2 | 3 | This library enables developers to automatically rig any mesh generated by the 4 | [Body Labs Instant API][mesh-docs]. The rig is [Mixamo][mixamo]-compatible, 5 | allowing you to easily [create control rigs][mixamo-scripts] and apply motion 6 | capture files provided by Mixamo. 7 | 8 | ```python 9 | from bodylabs_rigger.factory import RiggedModelFactory 10 | from bodylabs_rigger.fbx_util import ( 11 | create_fbx_manager, 12 | export_fbx_scene, 13 | ) 14 | 15 | # Create a rigging factory. 16 | factory = RiggedModelFactory.create_default() 17 | 18 | fbx_manager = create_fbx_manager() 19 | 20 | # Rig and export a mesh (which is a numpy Vx3 array of vertices). 21 | rigged_mesh = factory.construct_rig(mesh_vertices, fbx_manager) 22 | export_fbx_scene(fbx_manager, rigged_mesh, output_path) 23 | 24 | # Clean up Fbx objects. 25 | rigged_mesh.Destroy() 26 | fbx_manager.Destroy() 27 | ``` 28 | 29 | We also provide a `MeshGenerator` library, which allows you to create 30 | riggable meshes from body measurements using [BodyKit][bodykit]. 31 | 32 | ```python 33 | import os 34 | from bodylabs_rigger.bodykit.mesh_generator import MeshGenerator 35 | 36 | # Make sure to set BODYKIT_ACCESS_KEY and BODYKIT_SECRET 37 | # in your execution environment. 38 | mesh_generator = MeshGenerator( 39 | os.environ['BODYKIT_ACCESS_KEY'], 40 | os.environ['BODYKIT_SECRET'] 41 | ) 42 | 43 | mesh = mesh_generator.get_mesh_for_measurements( 44 | {'height': 70, 'weight': 150}, 45 | unit_system='unitedStates', 46 | gender='male' 47 | ) 48 | 49 | rigged_mesh = factory.construct_rig(mesh, fbx_manager) 50 | ``` 51 | 52 | [`examples/meshes_from_bodykit.py`][example-script] puts all the pieces 53 | together to randomly generate and rig a set of meshes. 54 | 55 | ``` 56 | python examples/meshes_from_bodykit.py \ 57 | ~/Desktop/bodylabs_rig_examples \ 58 | --num_meshes 5 59 | ``` 60 | 61 | [mesh-docs]: http://developer.bodylabs.com/instant_api_reference.html#Mesh 62 | [mixamo]: https://www.mixamo.com/ 63 | [mixamo-scripts]: https://www.mixamo.com/scripts 64 | [bodykit]: http://www.bodylabs.com/bodykit.html 65 | [example-script]: https://github.com/bodylabs/rigger/blob/master/examples/meshes_from_bodykit.py 66 | 67 | ## Installation 68 | 69 | 1. Install [Python FBX][python-fbx] 70 | 2. `pip install -e git+https://github.com/bodylabs/rigger.git#egg=bodylabs_rigger` 71 | 72 | This library has been tested on Mac OS X and Linux with Python 2.7. 73 | Windows is not officially supported at this time. 74 | 75 | [python-fbx]: http://help.autodesk.com/view/FBX/2015/ENU/?guid=__files_GUID_2F3A42FA_4C19_42F2_BC4F_B9EC64EA16AA_htm 76 | 77 | ## Contribute 78 | 79 | * Issue tracker: http://github.com/bodylabs/rigger/issues 80 | * Source code: http://github.com/bodylabs/rigger 81 | 82 | ## Support 83 | 84 | If you have questions or run into any issues, please contact us via the 85 | [BodyKit developer support page][bodykit-support]. 86 | 87 | [bodykit-support]: http://developer.bodylabs.com/help_and_support.html 88 | 89 | ## License 90 | 91 | This project is licensed under the BSD license. 92 | -------------------------------------------------------------------------------- /bodylabs_rigger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bodylabs/rigger/9b5340d8f07e29a6ff976d512e4b2e5a86f3d519/bodylabs_rigger/__init__.py -------------------------------------------------------------------------------- /bodylabs_rigger/bodykit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bodylabs/rigger/9b5340d8f07e29a6ff976d512e4b2e5a86f3d519/bodylabs_rigger/bodykit/__init__.py -------------------------------------------------------------------------------- /bodylabs_rigger/bodykit/mesh_generator.py: -------------------------------------------------------------------------------- 1 | # Generates meshes using the the BodyKit API. 2 | # 3 | # For relevant API documentation, see 4 | # http://developer.bodylabs.com/instant_api_reference.html#Mesh 5 | 6 | 7 | class MeshGenerator(object): 8 | _BODYKIT_MESH_ENDPOINT = 'https://api.bodylabs.com/instant/mesh' 9 | _EXPECTED_VERTICES_PER_MESH = 4916 10 | 11 | def __init__(self, bodykit_access_key, bodykit_secret): 12 | import random 13 | 14 | self._api_access_key = bodykit_access_key 15 | self._api_secret = bodykit_secret 16 | self._api_headers = { 17 | 'Authorization': 'SecretPair accesskey={},secret={}'.format( 18 | bodykit_access_key, bodykit_secret) 19 | } 20 | 21 | random.seed(self._api_access_key) 22 | 23 | def _request_mesh_obj(self, measurements, unit_system, gender): 24 | """Requests a mesh from measurements. 25 | 26 | Returns a mesh in OBJ format or None if the request failed. 27 | """ 28 | import requests 29 | 30 | params = { 31 | 'measurements': measurements, 32 | 'unitSystem': unit_system, 33 | 'gender': gender, 34 | # Use non-standard measurement set and predict missing 35 | # measurements. 36 | 'scheme': 'flexible', 37 | # The rigging code expects a T-posed mesh with quad-based topology. 38 | 'pose': 'T', 39 | 'meshFaces': 'quads', 40 | } 41 | 42 | response = requests.post(MeshGenerator._BODYKIT_MESH_ENDPOINT, 43 | headers=self._api_headers, 44 | json=params) 45 | if response.status_code != 200: 46 | response.raise_for_status() 47 | return response.text 48 | 49 | def _parse_vertices(self, mesh_obj): 50 | """Parses the vertices from an OBJ mesh string. 51 | 52 | For background on the OBJ format, see 53 | http://en.wikipedia.org/wiki/Wavefront_.obj_file 54 | 55 | Returns a Vx3 numpy array, where V is the number of vertices. 56 | """ 57 | import re 58 | import numpy as np 59 | 60 | vertex_line_re = re.compile(r'^v (\S+) (\S+) (\S+)$') 61 | vertices = [] 62 | for line in mesh_obj.split('\n'): 63 | vertex_match = re.match(vertex_line_re, line) 64 | if vertex_match is None: 65 | continue 66 | try: 67 | vertices.extend([ 68 | float(coord) for coord in vertex_match.groups() 69 | ]) 70 | except ValueError: 71 | raise ValueError( 72 | "Failed to parse float in vertex line: '{}'".format(line)) 73 | 74 | # Since we parsed the line, we can assume this is evenly divisible. 75 | num_vertices = len(vertices) / 3 76 | 77 | # Since the mesh topology doesn't change, we can double check our 78 | # line parsing by verifying the expected number of vertices. 79 | if num_vertices != MeshGenerator._EXPECTED_VERTICES_PER_MESH: 80 | raise ValueError( 81 | 'Mesh has wrong number of vertices: {} vs {}'.format( 82 | num_vertices, MeshGenerator._EXPECTED_VERTICES_PER_MESH)) 83 | 84 | return np.array(vertices).reshape(-1, 3) 85 | 86 | def get_mesh_for_measurements(self, measurements, unit_system, gender): 87 | from requests import HTTPError 88 | 89 | try: 90 | mesh_obj = self._request_mesh_obj( 91 | measurements, 'unitedStates', gender) 92 | except HTTPError as e: 93 | print 'Mesh request failed: {}'.format(e) 94 | return None 95 | 96 | try: 97 | return self._parse_vertices(mesh_obj) 98 | except ValueError as e: 99 | print 'Failed to parse OBJ vertices: {}'.format(e) 100 | return None 101 | 102 | def get_random_mesh(self): 103 | import random 104 | 105 | measurements = { 106 | 'height': random.uniform(60, 80), 107 | 'weight': random.uniform(120, 220), 108 | } 109 | gender = random.choice(['male', 'female']) 110 | 111 | return self.get_mesh_for_measurements( 112 | measurements, 'unitedStates', gender) 113 | -------------------------------------------------------------------------------- /bodylabs_rigger/factory.py: -------------------------------------------------------------------------------- 1 | class RiggedModelFactory(object): 2 | """Generates rigged models from vertices. 3 | 4 | The factory is initialized with the static data for the model rig: the mesh 5 | topology and texture map, the joint hierarchy, and the vertex weight map. 6 | The RiggedModelFactory can then be used to generate FbxScene objects 7 | binding the rig to a set of mesh vertices. 8 | """ 9 | 10 | def __init__(self, textured_mesh, joint_tree, joint_position_spec, 11 | clusters): 12 | """Initializes the RiggedModelFactory. 13 | 14 | textured_mesh: a TexturedMesh object 15 | joint_tree: the JointTree at the root of the joint hierarchy 16 | joint_position_spec: dict mapping joint name to position specification. 17 | See `joint_positions.py` for more details. 18 | clusters: dict mapping joint name to ControlPointCluster 19 | """ 20 | self._textured_mesh = textured_mesh 21 | self._joint_tree = joint_tree 22 | self._joint_position_spec = joint_position_spec 23 | self._clusters = clusters 24 | 25 | def _set_mesh(self, v, fbx_scene, root): 26 | """Set the FbxMesh for the given scene. 27 | 28 | v: the mesh vertices 29 | fbx_scene: the FbxScene to which this mesh should be added 30 | root: the FbxNode off which the mesh will be added 31 | 32 | Returns the FbxNode to which the mesh was added. 33 | """ 34 | from fbx import ( 35 | FbxLayerElement, 36 | FbxMesh, 37 | FbxNode, 38 | FbxVector2, 39 | FbxVector4, 40 | ) 41 | 42 | # Create a new node in the scene. 43 | fbx_mesh_node = FbxNode.Create(fbx_scene, self._textured_mesh.name) 44 | root.AddChild(fbx_mesh_node) 45 | 46 | fbx_mesh = FbxMesh.Create(fbx_scene, '') 47 | fbx_mesh_node.SetNodeAttribute(fbx_mesh) 48 | 49 | # Vertices. 50 | num_vertices = v.shape[0] 51 | fbx_mesh.InitControlPoints(num_vertices) 52 | for vi in range(num_vertices): 53 | new_control_point = FbxVector4(*v[vi, :]) 54 | fbx_mesh.SetControlPointAt(new_control_point, vi) 55 | 56 | # Faces. 57 | faces = self._textured_mesh.faces 58 | for fi in range(faces.shape[0]): 59 | face = faces[fi, :] 60 | fbx_mesh.BeginPolygon(fi) 61 | for vi in range(faces.shape[1]): 62 | fbx_mesh.AddPolygon(face[vi]) 63 | fbx_mesh.EndPolygon() 64 | fbx_mesh.BuildMeshEdgeArray() 65 | 66 | # Vertex normals. 67 | fbx_mesh.GenerateNormals( 68 | False, # pOverwrite 69 | True, # pByCtrlPoint 70 | ) 71 | 72 | # UV map. 73 | uv_indices = self._textured_mesh.uv_indices.ravel() 74 | uv_values = self._textured_mesh.uv_values 75 | uv = fbx_mesh.CreateElementUV('') 76 | uv.SetMappingMode(FbxLayerElement.eByPolygonVertex) 77 | uv.SetReferenceMode(FbxLayerElement.eIndexToDirect) 78 | index_array = uv.GetIndexArray() 79 | direct_array = uv.GetDirectArray() 80 | index_array.SetCount(uv_indices.size) 81 | direct_array.SetCount(uv_values.shape[0]) 82 | for ei, uvi in enumerate(uv_indices): 83 | index_array.SetAt(ei, uvi) 84 | direct_array.SetAt(uvi, FbxVector2(*uv_values[uvi, :])) 85 | 86 | return fbx_mesh_node 87 | 88 | def _set_node_translation(self, location, fbx_node): 89 | """Translates a node to a location in world coordinates. 90 | 91 | location: an unpackable (x, y, z) vector 92 | fbx_node: the FbxNode to translate 93 | """ 94 | from fbx import ( 95 | FbxAMatrix, 96 | FbxDouble4, 97 | FbxVector4, 98 | ) 99 | 100 | # We want to affect a target global position change by modifying the 101 | # local node translation. If the global transformation were based 102 | # solely on translation, rotation, and scale, we could set each of 103 | # these individually and be done. However, the global transformation 104 | # matrix computation is more complicated. For details, see 105 | # http://help.autodesk.com/view/FBX/2015/ENU/ 106 | # ?guid=__files_GUID_10CDD63C_79C1_4F2D_BB28_AD2BE65A02ED_htm 107 | # 108 | # We get around this by setting the world transform to our desired 109 | # global translation matrix, then solving for the local translation. 110 | global_pos_mat = FbxAMatrix() 111 | global_pos_mat.SetIdentity() 112 | global_pos_mat.SetT(FbxVector4(*location)) 113 | 114 | current_global_pos_mat = fbx_node.EvaluateGlobalTransform() 115 | parent_global_pos_mat = fbx_node.GetParent().EvaluateGlobalTransform() 116 | current_local_translation = FbxAMatrix() 117 | current_local_translation.SetIdentity() 118 | current_local_translation.SetT( 119 | FbxVector4(fbx_node.LclTranslation.Get())) 120 | new_local_translation = ( 121 | parent_global_pos_mat.Inverse() * 122 | global_pos_mat * 123 | current_global_pos_mat.Inverse() * 124 | parent_global_pos_mat * 125 | current_local_translation 126 | ) 127 | fbx_node.LclTranslation.Set(FbxDouble4(*new_local_translation.GetT())) 128 | 129 | def _extend_skeleton(self, parent_fbx_node, reference_joint_tree, 130 | target_fbx_node_positions, fbx_scene): 131 | """Extend the FbxNode skeleton according to the reference JointTree. 132 | 133 | parent_fbx_node: the FbxNode off which the skeleton will be extended 134 | reference_joint_tree: the reference JointTree object providing the 135 | hierarchy 136 | target_fbx_node_positions: a mapping from joint name to the desired 137 | position for the respective FbxNode in the skeleton 138 | fbx_scene: the FbxScene to which the skeleton should be added 139 | 140 | Returns a map from node name to FbxNode. 141 | """ 142 | from fbx import ( 143 | FbxNode, 144 | FbxSkeleton, 145 | ) 146 | 147 | fbx_node_map = {} 148 | 149 | skeleton = FbxSkeleton.Create(fbx_scene, '') 150 | skeleton.SetSkeletonType(FbxSkeleton.eLimbNode) 151 | 152 | node_name = reference_joint_tree.name 153 | node = FbxNode.Create(fbx_scene, node_name) 154 | node.SetNodeAttribute(skeleton) 155 | parent_fbx_node.AddChild(node) 156 | fbx_node_map[node_name] = node 157 | 158 | node_position = target_fbx_node_positions.get(node_name, None) 159 | if node_position is not None: 160 | self._set_node_translation(node_position, node) 161 | else: 162 | print "Position information missing for '{}'".format(node_name) 163 | 164 | for child in reference_joint_tree.children: 165 | fbx_node_map.update(self._extend_skeleton( 166 | node, child, target_fbx_node_positions, fbx_scene)) 167 | return fbx_node_map 168 | 169 | def _add_skin_and_bind_pose(self, fbx_node_map, fbx_mesh_node, fbx_scene): 170 | """Adds a deformer skin and bind pose. 171 | 172 | fbx_node_map: a map from node name to FbxNode. These nodes will become 173 | the cluster links. 174 | fbx_mesh_node: the FbxNode where our mesh is attached (i.e. as the 175 | node attribute). The skin will be added as a deformer of this 176 | mesh. 177 | fbx_scene: the FbxScene to which the skin and bind pose should be 178 | added. 179 | """ 180 | from fbx import ( 181 | FbxCluster, 182 | FbxMatrix, 183 | FbxPose, 184 | FbxSkin, 185 | ) 186 | 187 | mesh = fbx_mesh_node.GetNodeAttribute() 188 | 189 | # Create the bind pose. We'll give the bind pose a unique name since 190 | # it is added at the level of the global scene. 191 | bind_pose = FbxPose.Create( 192 | fbx_scene, 'pose{}'.format(fbx_scene.GetPoseCount() + 1)) 193 | bind_pose.SetIsBindPose(True) 194 | bind_pose.Add(fbx_mesh_node, FbxMatrix( 195 | fbx_mesh_node.EvaluateGlobalTransform())) 196 | 197 | skin = FbxSkin.Create(fbx_scene, '') 198 | for node_name, node in fbx_node_map.iteritems(): 199 | cluster_info = self._clusters.get(node_name) 200 | if cluster_info is None: 201 | continue 202 | 203 | cluster = FbxCluster.Create(fbx_scene, '') 204 | cluster.SetLink(node) 205 | cluster.SetLinkMode(FbxCluster.eNormalize) 206 | 207 | vindices = cluster_info.indices 208 | weights = cluster_info.weights 209 | for vid, weight in zip(vindices, weights): 210 | cluster.AddControlPointIndex(vid, weight) 211 | 212 | transform = node.EvaluateGlobalTransform() 213 | cluster.SetTransformLinkMatrix(transform) 214 | bind_pose.Add(node, FbxMatrix(transform)) 215 | skin.AddCluster(cluster) 216 | mesh.AddDeformer(skin) 217 | fbx_scene.AddPose(bind_pose) 218 | 219 | def construct_rig(self, vertices, fbx_manager): 220 | """Construct rig for the given vertices. 221 | 222 | vertices: an Vx3 numpy array in centimeter units. 223 | 224 | Returns a new FbxScene. 225 | """ 226 | from joint_positions import calculate_joint_positions 227 | from fbx import FbxScene 228 | 229 | fbx_scene = FbxScene.Create(fbx_manager, '') 230 | 231 | # We'll build the rig off of this node. One child will root 232 | # the joint skeleton and another will contain the mesh and skin. 233 | rig_root_node = fbx_scene.GetRootNode() 234 | 235 | target_joint_positions = calculate_joint_positions( 236 | vertices, self._joint_position_spec) 237 | 238 | # Add the skeleton to the scene, saving the nodes by name. We'll 239 | # then use this map to link the nodes to their vertex clusters. 240 | fbx_node_map = self._extend_skeleton( 241 | rig_root_node, self._joint_tree, target_joint_positions, 242 | fbx_scene) 243 | 244 | # Add the mesh, skin, and bind pose. 245 | fbx_mesh_node = self._set_mesh(vertices, fbx_scene, rig_root_node) 246 | self._add_skin_and_bind_pose(fbx_node_map, fbx_mesh_node, fbx_scene) 247 | 248 | return fbx_scene 249 | 250 | @classmethod 251 | def create_default(cls): 252 | import os 253 | import bodylabs_rigger.static 254 | from bodylabs_rigger.rig_assets import RigAssets 255 | 256 | assets = RigAssets.load(os.path.join( 257 | os.path.dirname(bodylabs_rigger.static.__file__), 258 | 'rig_assets.json')) 259 | return cls(**assets.__dict__) 260 | -------------------------------------------------------------------------------- /bodylabs_rigger/fbx_util.py: -------------------------------------------------------------------------------- 1 | # Utilities functions for working with an FbxScene. 2 | # 3 | # Example usage: 4 | # 5 | # manager = create_fbx_manager() 6 | # scene = import_fbx_scene(manager, 'path/to/scene.fbx') 7 | # 8 | # # ... Make some changes ... 9 | # 10 | # export_fbx_scene(manager, scene, 'path/to/scene_modified.fbx') 11 | # manager.Destroy() 12 | # # Delete the (now unusable) FbxManager to avoid accidental 13 | # # usage in later code. 14 | # del manager 15 | 16 | 17 | def create_fbx_manager(): 18 | from fbx import ( 19 | FbxIOSettings, 20 | FbxManager, 21 | IOSROOT, 22 | ) 23 | 24 | fbx_manager = FbxManager.Create() 25 | if not fbx_manager: 26 | raise RuntimeError('Failed to create FbxManager.') 27 | ios = FbxIOSettings.Create(fbx_manager, IOSROOT) 28 | fbx_manager.SetIOSettings(ios) 29 | return fbx_manager 30 | 31 | 32 | def import_fbx_scene(fbx_manager, scene_path): 33 | from fbx import ( 34 | FbxImporter, 35 | FbxScene, 36 | ) 37 | 38 | importer = FbxImporter.Create(fbx_manager, '') 39 | # Import with any file format. 40 | if not importer.Initialize(scene_path, -1, fbx_manager.GetIOSettings()): 41 | raise IOError('Failed to import scene file: {}'.format(scene_path)) 42 | 43 | scene = FbxScene.Create(fbx_manager, '') 44 | importer.Import(scene) 45 | importer.Destroy() 46 | return scene 47 | 48 | 49 | def export_fbx_scene(fbx_manager, scene, output_path): 50 | import os 51 | from fbx import FbxExporter 52 | 53 | output_path = os.path.expanduser(output_path) 54 | 55 | exporter = FbxExporter.Create(fbx_manager, '') 56 | exporter.Initialize(output_path, -1, fbx_manager.GetIOSettings()) 57 | exporter.Export(scene) 58 | exporter.Destroy() 59 | return output_path 60 | -------------------------------------------------------------------------------- /bodylabs_rigger/joint_positions.py: -------------------------------------------------------------------------------- 1 | # Utility function for calculating joint positions relative to a mesh. 2 | # 3 | # Joint specification 4 | # ------------------- 5 | # We express the position of a joint by a list of reference vertices and a 6 | # relative position, e.g. 7 | # 8 | # "Neck": { 9 | # "reference_vertices": [2319, 482], 10 | # "relative_position": [0.66, 0.66, 0.66] 11 | # } 12 | # 13 | # The reference vertices are used to calculate two extrema points. The joint is 14 | # placed along the vector between these extrema points according to the 15 | # relative position. 16 | # 17 | # Extrema point calculation 18 | # ------------------------- 19 | # If there are exactly two reference vertices these become the extrema points. 20 | # If there are more than two reference vertices the extrema points are computed 21 | # as the min/max x, y, and z across all vertices. 22 | # 23 | # Relative positioning 24 | # -------------------- 25 | # If there is exactly one reference vertex the joint is placed at this vertex. 26 | # Otherwise, the position is calculated relative to the extrema points. A 27 | # relative position vector indicates how much of each dimension to shift from 28 | # the min extrema point to the max extrema point. E.g. if [0.5, 0.5 0.5] is 29 | # used, we'll position the joint half way between the extrema points. If 30 | # [0.25, 0.5, 0.5] is used, we'll position the Joint such that the Y and Z 31 | # positions are half way between the extrema, but the X position is a quarter 32 | # of the way from the min extrema to the max extrema. 33 | # 34 | # Exceptions 35 | # ---------- 36 | # 'LeftShoulder' and 'RightShoulder' are positioned 1/3 of the way from the 37 | # 'Neck' to the 'LeftArm' and 'RightArm' joints respectively. 38 | 39 | 40 | def calculate_joint_position(vertices, reference_vertices, 41 | relative_position=[0.5, 0.5, 0.5]): 42 | import numpy as np 43 | 44 | joint_vertices = vertices[reference_vertices, :].reshape(-1, 3) 45 | if joint_vertices.shape[0] > 2: 46 | v1 = np.min(joint_vertices, axis=0) 47 | v2 = np.max(joint_vertices, axis=0) 48 | else: 49 | v1 = joint_vertices[0, :] 50 | v2 = joint_vertices[-1, :] 51 | return v1 + (v2 - v1) * np.array(relative_position) 52 | 53 | 54 | def calculate_joint_positions(vertices, joint_position_spec): 55 | """Calculate the position of each joint relative to the given vertices. 56 | 57 | vertices: a Vx3 numpy array 58 | joint_position_spec: a dict mapping joint name to position specification 59 | (see above for details). 60 | 61 | Returns a map from joint name to target location (as a 3-element numpy 62 | array) in world coordinates. 63 | """ 64 | joint_location_map = {} 65 | for joint_name, joint_spec in joint_position_spec.iteritems(): 66 | joint_location_map[joint_name] = calculate_joint_position( 67 | vertices, **joint_spec) 68 | 69 | # 'LeftShoulder' and 'RightShoulder' are special cased. 70 | try: 71 | neck_pos = joint_location_map['Neck'] 72 | left_arm_pos = joint_location_map['LeftArm'] 73 | joint_location_map['LeftShoulder'] = ( 74 | neck_pos + (left_arm_pos - neck_pos) / 3. 75 | ) 76 | right_arm_pos = joint_location_map['RightArm'] 77 | joint_location_map['RightShoulder'] = ( 78 | neck_pos + (right_arm_pos - neck_pos) / 3. 79 | ) 80 | except KeyError as ke: 81 | print "Unrecognized joint name: '{}'".format(ke) 82 | 83 | return joint_location_map 84 | -------------------------------------------------------------------------------- /bodylabs_rigger/rig_assets.py: -------------------------------------------------------------------------------- 1 | class RigAssets(object): 2 | """Serializable wrapper for dependencies of a RiggedModelFactory.""" 3 | 4 | def __init__(self, textured_mesh, joint_tree, joint_position_spec, 5 | clusters): 6 | self.textured_mesh = textured_mesh 7 | self.joint_tree = joint_tree 8 | self.joint_position_spec = joint_position_spec 9 | self.clusters = clusters 10 | 11 | def to_json(self): 12 | return { 13 | 'textured_mesh': self.textured_mesh.to_json(), 14 | 'joint_tree': self.joint_tree.to_json(), 15 | # The joint position spec is already JSON serializable. 16 | 'joint_position_spec': self.joint_position_spec, 17 | 'clusters': { 18 | name: cluster.to_json() 19 | for name, cluster in self.clusters.iteritems() 20 | } 21 | } 22 | 23 | @classmethod 24 | def from_json(cls, o): 25 | return cls( 26 | textured_mesh=TexturedMesh.from_json(o['textured_mesh']), 27 | joint_tree=JointTree.from_json(o['joint_tree']), 28 | joint_position_spec=o['joint_position_spec'], 29 | clusters={ 30 | name: ControlPointCluster.from_json(cluster) 31 | for name, cluster in o['clusters'].iteritems() 32 | } 33 | ) 34 | 35 | def dump(self, filename): 36 | import json 37 | 38 | with open(filename, 'w') as f: 39 | json.dump(self.to_json(), f) 40 | 41 | @classmethod 42 | def load(cls, filename): 43 | import json 44 | 45 | with open(filename, 'r') as f: 46 | assets = cls.from_json(json.load(f)) 47 | return assets 48 | 49 | 50 | class JointTree(object): 51 | """A simple tree-based representation for a hierarchy of joints.""" 52 | 53 | def __init__(self, name, children=None): 54 | """Initialize the joint subtree. 55 | 56 | name: the name for this joint 57 | children: a list of JointTree objects 58 | """ 59 | self.name = name 60 | self.children = children 61 | if self.children is None: 62 | self.children = [] 63 | 64 | def to_json(self): 65 | return { 66 | 'name': self.name, 67 | 'children': [c.to_json() for c in self.children] 68 | } 69 | 70 | @classmethod 71 | def from_json(cls, o): 72 | return cls(o['name'], [cls.from_json(c) for c in o['children']]) 73 | 74 | 75 | class TexturedMesh(object): 76 | """Wrapper for the faces and corresponding texture map of a mesh.""" 77 | 78 | def __init__(self, faces, uv_indices, uv_values, name=None): 79 | """Initializes the TexturedMesh. 80 | 81 | Let F denote the number of faces in the mesh. 82 | 83 | faces: Fx4 numpy array of vertex indices (four per face). 84 | uv_indices: Fx4 numpy array of `uv_values` row indices. 85 | uv_values: each row gives the U and V coordinates for a particular 86 | face vertex. 87 | name: the name for this mesh 88 | """ 89 | self.faces = faces 90 | self.uv_indices = uv_indices 91 | self.uv_values = uv_values 92 | self.name = name or 'Bodylabs_body' 93 | 94 | def to_json(self): 95 | import numpy as np 96 | 97 | # Flatten each numpy array. 98 | return { 99 | k: v.ravel().tolist() if isinstance(v, np.ndarray) else v 100 | for k, v in self.__dict__.iteritems() 101 | } 102 | 103 | @classmethod 104 | def from_json(cls, o): 105 | import numpy as np 106 | return cls( 107 | faces=np.array(o['faces']).reshape(-1, 4), 108 | uv_indices=np.array(o['uv_indices']).reshape(-1, 4), 109 | uv_values=np.array(o['uv_values']).reshape(-1, 2), 110 | name=o.get('name'), # Allow None for backwards compatibility. 111 | ) 112 | 113 | 114 | class ControlPointCluster(object): 115 | """Wrapper for the indices and weights of a vertex control cluster.""" 116 | 117 | def __init__(self, indices, weights): 118 | self.indices = indices 119 | self.weights = weights 120 | 121 | def to_json(self): 122 | return self.__dict__ 123 | 124 | @classmethod 125 | def from_json(cls, o): 126 | return cls(**o) 127 | -------------------------------------------------------------------------------- /bodylabs_rigger/static/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bodylabs/rigger/9b5340d8f07e29a6ff976d512e4b2e5a86f3d519/bodylabs_rigger/static/__init__.py -------------------------------------------------------------------------------- /examples/meshes_from_bodykit.py: -------------------------------------------------------------------------------- 1 | # Randomly generates and rigs a set of meshes. 2 | # 3 | # Requires access to BodyKit, which can be requested at http://bodykit.io/. 4 | 5 | 6 | def main(): 7 | import os 8 | import argparse 9 | from bodylabs_rigger.bodykit.mesh_generator import MeshGenerator 10 | from bodylabs_rigger.factory import RiggedModelFactory 11 | from bodylabs_rigger.fbx_util import ( 12 | create_fbx_manager, 13 | export_fbx_scene, 14 | ) 15 | 16 | access_key = os.environ.get('BODYKIT_ACCESS_KEY', None) 17 | secret = os.environ.get('BODYKIT_SECRET', None) 18 | 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument( 21 | 'output_directory', default=None, 22 | help='The directory to write the rigged meshes.') 23 | 24 | parser.add_argument( 25 | '--num_meshes', default=5, type=int, required=False, 26 | help='The number of meshes to generate and rig.') 27 | parser.add_argument( 28 | '--bodykit_access_key', default=None, required=(access_key is None), 29 | help=('Access key for the BodyKit API. Required if BODYKIT_ACCESS_KEY ' 30 | 'environment variable is not set.')) 31 | parser.add_argument( 32 | '--bodykit_secret', default=None, required=(secret is None), 33 | help=('Secret for the BodyKit API. Required if BODYKIT_SECRET ' 34 | 'environment variable is not set.')) 35 | args = parser.parse_args() 36 | 37 | access_key = args.bodykit_access_key or access_key 38 | secret = args.bodykit_secret or secret 39 | 40 | mesh_generator = MeshGenerator(access_key, secret) 41 | 42 | factory = RiggedModelFactory.create_default() 43 | 44 | if not os.path.exists(args.output_directory): 45 | os.makedirs(args.output_directory) 46 | 47 | manager = create_fbx_manager() 48 | for mesh_index in range(args.num_meshes): 49 | print 'Generating rigged mesh {}'.format(mesh_index) 50 | mesh = mesh_generator.get_random_mesh() 51 | if mesh is None: 52 | continue 53 | 54 | rigged_mesh = factory.construct_rig(mesh, manager) 55 | output_path = os.path.join( 56 | args.output_directory, 'rigged_mesh_{:02}.fbx'.format(mesh_index)) 57 | export_fbx_scene(manager, rigged_mesh, output_path) 58 | rigged_mesh.Destroy() 59 | manager.Destroy() 60 | 61 | 62 | if __name__ == '__main__': 63 | main() 64 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.9.1 2 | requests>=2.6.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | version = '0.1.2' 2 | 3 | with open('requirements.txt', 'r') as f: 4 | install_requires = [x.strip() for x in f.readlines()] 5 | 6 | from setuptools import setup, find_packages 7 | 8 | setup( 9 | name='bodylabs-rigger', 10 | version=version, 11 | author='Body Labs', 12 | author_email='david.smith@bodylabs.com', 13 | description="Utilities for rigging a mesh from Body Labs' BodyKit API.", 14 | url='https://github.com/bodylabs/rigger', 15 | license='BSD', 16 | packages=find_packages(), 17 | package_data={ 18 | 'bodylabs_rigger.static': ['rig_assets.json'] 19 | }, 20 | install_requires=install_requires, 21 | classifiers=[ 22 | 'Development Status :: 2 - Pre-Alpha', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: BSD License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Topic :: Software Development :: Libraries :: Python Modules' 28 | ] 29 | ) 30 | --------------------------------------------------------------------------------