├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── common.py ├── requirements.txt ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | .cache 4 | *.pyc 5 | venv 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jo Chasinga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyfbx 2 | ====== 3 | 4 | A simple-to-use wrapper for Autodesk Python FBX SDK. 5 | 6 | FBX 7 | --- 8 | > FBX(Filmbox) is a proprietary file format (.fbx) developed by 9 | > Kaydara and owned by [Autodesk](https://en.wikipedia.org/wiki/Autodesk) 10 | > since 2006. It is used to provide interoperability between digital 11 | > content creation applications. 12 | > 13 | > from [Wikipedia](https://en.wikipedia.org/wiki/FBX) 14 | 15 | Why another wrapper? 16 | -------------------- 17 | I have had a chance to work on a project which needed Python to generate 18 | FBX file for Maya. Autodesk did a rather poor job at its Python FBX SDK such as: 19 | 20 | + Lack of documentation 21 | + C-style APIs instead of Pythonic 22 | + Tedious wrapper classes i.e. `FbxDouble3` to wrap around C++ array. 23 | 24 | How it works 25 | ------------ 26 | Classes in *pyfbx* does not inherit the original classes from the SDK. Each instance 27 | of the class have a private property `_me` which interact with the original class's 28 | instance while pyfbx "floats" above i.e. 29 | 30 | ```python 31 | 32 | from pyfbx import * 33 | 34 | manager = Manager() 35 | scene = Scene(manager) 36 | assert isinstance(scene, pyfbx.Scene) # True 37 | assert isinstance(scene._me, fbx.FbxScene) # True 38 | 39 | ``` 40 | 41 | Install 42 | ------- 43 | 44 | ```bash 45 | 46 | git clone https://github.com/jochasinga/pyfbx 47 | cd pyfbx && python setup.py install 48 | 49 | ``` 50 | 51 | Examples 52 | -------- 53 | 54 | ```python 55 | 56 | from pyfbx import * 57 | 58 | manager = Manager() 59 | scene = Scene(manager) 60 | node = Node(manager) 61 | mesh = Mesh(manager) 62 | 63 | # Set mesh attributes 64 | 65 | node.add_attribute(mesh) 66 | scene.root_node.add_child(node) 67 | 68 | ``` 69 | 70 | However, since `Manager` and `Scene` is a singleton in charge of all the nodes, it makes 71 | sense for them to be atomic. 72 | 73 | ```python 74 | 75 | manager = Manager() 76 | manager.create_scene() # equivalent to `Scene(manager)` 77 | manager.create_node() # equivalent to `Node(manager)` 78 | scene = manager.scene 79 | scene.create_node() # equivalent to `Node(scene) 80 | 81 | ``` 82 | 83 | Node Tree 84 | --------- 85 | 86 | FBX depends on node hierachy, with the scene's root node acting as the descendent of all. While 87 | you can always use the underlying API to traverse through nodes in the scene, `Node` also keeps 88 | track of its children using simple list. 89 | 90 | ```python 91 | 92 | from pyfbx import * 93 | 94 | manager = Manager() 95 | p_node = Node(manager, "parent_node") 96 | node.add_child(Node(manager, "child_node_1")) 97 | node.add_child(Node(manager, "child_node_2")) 98 | 99 | assert p_node.children[0].name == "child_node_1" 100 | 101 | c_node_1 = node.children[0] 102 | c_node_1.add_child(Node(manager, "grandchild_node")) 103 | 104 | assert p_node.children[0].children[0].name == "grandchild_node" 105 | 106 | ``` 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jochasinga/py-fbx/4d8c9446e87367f9d286d063a978c1c597aed0b7/__init__.py -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import fbx, fbxsip 4 | 5 | class Node: 6 | def __init__(self, parent, name='', children=None): 7 | self._me = fbx.FbxNode.Create(parent._me, name) 8 | self.name = name 9 | self.children = [] 10 | if children is not None: 11 | for child in children: 12 | self.add_child(child) 13 | 14 | def add_child(self, node): 15 | assert isinstance(node, Node) 16 | self.children.append(node) 17 | 18 | def add_attribute(self, attr): 19 | self.attribute = attr 20 | self.attribute._me = attr._me 21 | return self._me.AddNodeAttribute(attr._me) 22 | 23 | class Manager: 24 | 25 | nodes = [] 26 | 27 | def __init__(self): 28 | self._me = fbx.FbxManager.Create() 29 | 30 | def create_scene(self, name=""): 31 | self.scene = Scene(self, name) 32 | 33 | def create_node(self, name=""): 34 | node = Node(self, name) 35 | self.nodes.append(node) 36 | 37 | def get_nodes(self): 38 | return self.nodes 39 | 40 | class Scene: 41 | def __init__(self, manager, name=""): 42 | self._me = fbx.FbxScene.Create(manager._me, name) 43 | self.manager = manager 44 | self.name = name 45 | 46 | self.root_node = Node(self, name + "_root_node") 47 | self.root_node._me = self._me.GetRootNode() 48 | 49 | def create_node(self, name=""): 50 | node = Node(self, name) 51 | 52 | class Mesh: 53 | def __init__(self, parent, name=""): 54 | self.parent = parent 55 | self.name = name 56 | self._me = fbx.FbxMesh.Create(parent._me, name) 57 | 58 | def add_to(self, node): 59 | node.add_attribute(self) 60 | 61 | class FBox: 62 | """Wrap fbx's common Python classes""" 63 | manager = None 64 | importer = None 65 | exporter = None 66 | scene = None 67 | root_node = None 68 | cpt_count = 0 69 | 70 | def __init__(self): 71 | """Create an atomic manager, exporter, scene and its root node.""" 72 | FBox.manager = fbx.FbxManager.Create() 73 | if not FBox.manager: 74 | sys.exit(0) 75 | 76 | FBox.ios = fbx.FbxIOSettings.Create(FBox.manager, fbx.IOSROOT) 77 | FBox.exporter = fbx.FbxExporter.Create(FBox.manager, '') 78 | FBox.scene = fbx.FbxScene.Create(FBox.manager, '') 79 | FBox.root_node = FBox.scene.GetRootNode() 80 | 81 | def export(self, file_format='fbx'): 82 | """Export the scene to an fbx file.""" 83 | save_path = "./marker." + file_format 84 | 85 | if not FBox.manager.GetIOSettings(): 86 | FBox.ios = fbx.FbxIOSettings.Create(FBox.manager, fbx.IOSROOT) 87 | FBox.manager.SetIOSettings(FBox.ios) 88 | 89 | FBox.manager.GetIOSettings().SetBoolProp(fbx.EXP_FBX_MATERIAL, True) 90 | FBox.manager.GetIOSettings().SetBoolProp(fbx.EXP_FBX_TEXTURE, True) 91 | FBox.manager.GetIOSettings().SetBoolProp(fbx.EXP_FBX_EMBEDDED, True) 92 | FBox.manager.GetIOSettings().SetBoolProp(fbx.EXP_FBX_SHAPE, True) 93 | FBox.manager.GetIOSettings().SetBoolProp(fbx.EXP_FBX_GOBO, True) 94 | FBox.manager.GetIOSettings().SetBoolProp(fbx.EXP_FBX_ANIMATION, True) 95 | FBox.manager.GetIOSettings().SetBoolProp(fbx.EXP_FBX_GLOBAL_SETTINGS, True) 96 | 97 | exportstat = FBox.exporter.Initialize(save_path, 1, FBox.manager.GetIOSettings()) 98 | 99 | if not exportstat: 100 | try: 101 | raise IOError("Problem exporting file!") 102 | except IOError as e: 103 | print("An exception flew by!") 104 | 105 | exportstat = FBox.exporter.Export(FBox.scene) 106 | 107 | FBox.exporter.Destroy() 108 | 109 | return exportstat 110 | 111 | def create_node(self, nickname=''): 112 | """Create a free node and add to the root node.""" 113 | self.node = fbx.FbxNode.Create(FBox.manager, nickname) 114 | FBox.root_node.AddChild(self.node) 115 | 116 | def create_mesh(self, nickname=''): 117 | """Create a free mesh""" 118 | self.mesh = fbx.FbxMesh.Create(FBox.manager, nickname) 119 | 120 | def create_mesh_controlpoint(self, x, y, z): 121 | """Create a mesh controlpoint.""" 122 | self.mesh.SetControlPointAt(fbx.FbxVector4(x, y, z), FBox.cpt_count) 123 | FBox.cpt_count += 1 124 | 125 | def get_rnode_translation(self): 126 | """Get the root node's translation.""" 127 | return FBox.root_node.LclTranslation.Get() 128 | 129 | def get_node_translation(self): 130 | """Get the child node's translation.""" 131 | return self.node.LclTranslation.Get() 132 | 133 | def set_rnode_translation(self, coordinates): 134 | """Set the root node translation.""" 135 | FBox.root_node.LclTranslation.Set(fbx.FbxDouble3( 136 | float(coordinates[0]), 137 | float(coordinates[1]), 138 | float(coordinates[2]) 139 | )) 140 | 141 | def set_node_translation(self, coordinates): 142 | """Set the child node translation.""" 143 | self.node.LclTranslation.Set(fbx.FbxDouble3( 144 | float(coordinates[0]), 145 | float(coordinates[1]), 146 | float(coordinates[2]) 147 | )) 148 | 149 | def set_mesh_to_node(self): 150 | """Set the mesh to the child node's attribute.""" 151 | self.node.AddNodeAttribute(self.mesh) 152 | 153 | def destroy(self): 154 | """Free the manager's memory.""" 155 | FBox.manager.Destroy() 156 | 157 | class PyramidMarker: 158 | 159 | def __init__(self, scene, name): 160 | self.scene = scene 161 | self.name = name 162 | 163 | def create(self, base_width, height): 164 | self.base_width = base_width 165 | self.height = height 166 | 167 | pyramid = fbx.FbxMesh.Create(self.scene, self.name) 168 | 169 | # Calculate the vertices of the pyramid lying down 170 | base_width_half = base_width / 2 171 | controlpoints = [ 172 | fbx.FbxVector4(0, height, 0), 173 | fbx.FbxVector4(base_width_half, 0, base_width_half), 174 | fbx.FbxVector4(base_width_half, 0, -base_width_half), 175 | fbx.FbxVector4(-base_width_half, 0, -base_width_half), 176 | fbx.FbxVector4(-base_width_half, 0, base_width_half) 177 | ] 178 | 179 | # Initialize and set the control points of the mesh 180 | controlpoint_count = len(controlpoints) 181 | pyramid.InitControlPoints(controlpoint_count) 182 | for i, p in enumerate(controlpoints): 183 | pyramid.SetControlPointAt(p, i) 184 | 185 | # Set the control point indices of the bottom plane of the pyramid 186 | pyramid.BeginPolygon() 187 | pyramid.AddPolygon(1) 188 | pyramid.AddPolygon(4) 189 | pyramid.AddPolygon(3) 190 | pyramid.AddPolygon(2) 191 | pyramid.EndPolygon() 192 | 193 | # Set the control point indices of the front plane of the pyramid 194 | pyramid.BeginPolygon() 195 | pyramid.AddPolygon(0) 196 | pyramid.AddPolygon(1) 197 | pyramid.AddPolygon(2) 198 | pyramid.EndPolygon() 199 | 200 | # Set the control point indices of the left plane of the pyramid 201 | pyramid.BeginPolygon() 202 | pyramid.AddPolygon(0) 203 | pyramid.AddPolygon(2) 204 | pyramid.AddPolygon(3) 205 | pyramid.EndPolygon() 206 | 207 | # Set the control point indices of the back plane of the pyramid 208 | pyramid.BeginPolygon() 209 | pyramid.AddPolygon(0) 210 | pyramid.AddPolygon(3) 211 | pyramid.AddPolygon(4) 212 | pyramid.EndPolygon() 213 | 214 | # Set the control point indices of the right plane of the pyramid 215 | pyramid.BeginPolygon() 216 | pyramid.AddPolygon(0) 217 | pyramid.AddPolygon(4) 218 | pyramid.AddPolygon(1) 219 | pyramid.EndPolygon() 220 | 221 | # Attach the mesh to a node 222 | pyramid_node = fbx.FbxNode.Create(self.scene, '') 223 | pyramid_node.SetNodeAttribute(pyramid) 224 | 225 | self.pyramid_node = pyramid_node 226 | 227 | return pyramid_node 228 | 229 | def set_local_translation(self, coordinate): 230 | # TODO: Set the world coordinate to Z-up instead of Y-up 231 | x = float(coordinate[0]) 232 | y = float(coordinate[1]) 233 | z = float(coordinate[2]) 234 | self.pyramid_node.LclTranslation.Set(fbx.FbxDouble3(x, z, y)) 235 | 236 | def attach_to_rootnode(self): 237 | self.scene.GetRootNode().AddChild(self.pyramid_node) 238 | 239 | def set_rotation_pivot(self, coordinate): 240 | self.pyramid_node.SetRotationActive(True) 241 | x = float(coordinate[0]) 242 | y = float(coordinate[1]) 243 | z = float(coordinate[2]) 244 | self.pyramid_node.SetRotationPivot(fbx.FbxNode.eSourcePivot, fbx.FbxVector4(x, y, z)) 245 | 246 | def set_post_rotation(self, coordinate): 247 | x = float(coordinate[0]) 248 | y = float(coordinate[1]) 249 | z = float(coordinate[2]) 250 | self.pyramid_node.SetPostRotation(fbx.FbxNode.eSourcePivot, fbx.FbxVector4(x, y, z)) 251 | 252 | def main(): 253 | args = tuple(sys.argv[2:]) 254 | 255 | fb = FBox() 256 | fb.create_node() 257 | fb.create_mesh() 258 | fb.create_mesh_controlpoint(0, 0, 0) 259 | 260 | marker = PyramidMarker(fb.scene, "marker") 261 | marker.create(4, 4) 262 | marker.attach_to_rootnode() 263 | 264 | if sys.argv[1] == 'test': 265 | print("TEST MODE: Creating a vertex at provided x, y, z cartesian coordinates.") 266 | fb.set_rnode_translation( (0, 0, 0,) ) 267 | #fb.set_node_translation(args) 268 | marker.set_local_translation(args) 269 | 270 | elif sys.argv[1] == 'geo': 271 | print("GEO MODE: Creating a vertex at provided lat, lon, ele geolocation.\n") 272 | print("Remember in origin point in FBX will represent the origin of the respective UTM zone the location is at. See `https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system` for info.\n") 273 | geo = ProjGeo(args) 274 | geo_args = geo.get_projected_coordinates() 275 | print("Your zone is UTM {}".format(geo.get_utm_zone())) 276 | print("Your zone's origin real lat, lon is: ", geo.get_utm_zone_origin()) 277 | fb.set_rnode_translation((0, 0, 0,)) 278 | #fb.set_node_translation(geo_args) 279 | marker.set_local_translation(geo_args) 280 | print("Marker is set at the projected coordinate: ({0}, {1}, {2})".format( 281 | geo_args[0], geo_args[1], geo_args[2])) 282 | 283 | else: 284 | raise ValueError("Please provide `test` or `geo` as first parameter") 285 | 286 | marker.set_rotation_pivot((0, marker.height / 2, 0)) 287 | marker.set_post_rotation((0, 0, 180)) 288 | 289 | print("Exporting file...\n") 290 | fb.export() 291 | fb.destroy() 292 | print("Check marker.fbx in this current directory.\n") 293 | 294 | if __name__ == '__main__': 295 | main() 296 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Downloading/unpacking pytest 2 | Downloading/unpacking py>=1.4.29 (from pytest) 3 | Installing collected packages: pytest, py 4 | Successfully installed pytest py 5 | Cleaning up... 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | with open('README.md') as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name='pyfbx', 10 | version='0.1.0', 11 | description="A simple-to-use Python wrapper for FBX", 12 | long_description=long_description, 13 | author='Jo Chasinga', 14 | author_email='jo.chasinga@gmail.com', 15 | packages=['pyfbx'], 16 | include_package_data=True, 17 | install_requires=[ 18 | 'pytest', 19 | ], 20 | zip_safe=False, 21 | url='https://github.com/jochasinga/pyfbx', 22 | download_url = 'https://github.com/jochasinga/pyfbx/tarball/0.1.0', 23 | keywords = ['autodesk', 'fbx', 'maya', 'filmbox', '3d'], 24 | classifiers=[ 25 | 'Development Status :: Alpha', 26 | 'Environment :: Other Environment', 27 | 'Intended Audience :: Developers :: Digital Artists', 28 | 'License :: MIT License', 29 | 'Operating System :: OS Independent', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2.6', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Topic :: Utilities', 34 | 'Topic :: 3d digital media', 35 | ], 36 | ) 37 | 38 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import fbx 3 | import pyfbx 4 | 5 | def test_node(): 6 | manager = pyfbx.Manager() 7 | node = pyfbx.Node(manager, "my_node") 8 | 9 | assert isinstance(node._me, fbx.FbxNode) 10 | assert len(node.children) == 0 11 | 12 | node.add_child( pyfbx.Node(manager, "child_node")) 13 | assert len(node.children) == 1 14 | assert node.children[0].name == "child_node" 15 | 16 | node.children[0].add_child(pyfbx.Node(manager, "grandchild_susan")) 17 | node.children[0].add_child(pyfbx.Node(manager, "grandchild_eleanor")) 18 | 19 | assert len(node.children[0].children) == 2 20 | assert node.children[0].children[0].name == "grandchild_susan" 21 | assert node.children[0].children[1].name == "grandchild_eleanor" 22 | 23 | node.add_attribute(pyfbx.Mesh(manager, '')) 24 | assert isinstance(node.attribute, pyfbx.Mesh) 25 | assert isinstance(node.attribute._me, fbx.FbxMesh) 26 | 27 | def test_manager(): 28 | 29 | manager = pyfbx.Manager() 30 | manager.create_scene() 31 | manager.create_node() 32 | 33 | assert isinstance(manager._me, fbx.FbxManager) 34 | assert isinstance(manager.scene._me, fbx.FbxScene) 35 | assert isinstance(manager.nodes[0]._me, fbx.FbxNode) 36 | 37 | def test_scene(): 38 | 39 | manager = pyfbx.Manager() 40 | scene = pyfbx.Scene(manager, name="ma_scene") 41 | 42 | assert isinstance(scene._me, fbx.FbxScene) 43 | assert scene.name == "ma_scene" 44 | assert isinstance(scene.manager, pyfbx.Manager) 45 | assert isinstance(scene.root_node, pyfbx.Node) 46 | assert isinstance(scene.root_node._me, fbx.FbxNode) 47 | 48 | --------------------------------------------------------------------------------