├── .gitignore ├── README.md ├── docs ├── .DS_Store ├── bvh_exporter_new_features.jpg ├── bvh_importer_new_features.jpg ├── enhanced_bvh_exporter_menu.jpg ├── enhanced_bvh_importer_menu.jpg ├── install_addon_1.jpg ├── install_addon_2.jpg └── load_onto_existing_armature.jpg ├── io_anim_bvh_enhanced.zip └── io_anim_bvh_enhanced ├── __init__.py ├── export_bvh_enhanced.py └── import_bvh_enhanced.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enhanced bvh add-on (importer/exporter) for blender 2 | 3 | - [Enhanced bvh add-on (importer/exporter) for blender](#enhanced-bvh-add-on-importerexporter-for-blender) 4 | - [Enhanced bvh importer](#enhanced-bvh-importer) 5 | - [Enhanced bvh exporter](#enhanced-bvh-exporter) 6 | - [How to](#how-to) 7 | - [Install](#install) 8 | - [Load .bvh data onto an existing armature](#load-bvh-data-onto-an-existing-armature) 9 | 10 | **Author**: zhaoyafei0210@gmail.com 11 | 12 | **github repo**: https://github.com/walkoncross/blender_bvh_addon_enhanced 13 | 14 | Refer to for more information: 15 | https://docs.blender.org/manual/en/2.92/addons/import_export/anim_bvh.html 16 | 17 | Enhanced version of blender's bvh add-on with more settings supported. 18 | 19 | The bvh's rest pose should have the same handedness as the armature while could use a different up/forward definiton 20 | 21 | ## Enhanced bvh importer 22 | New features: 23 | 24 | - **Target**: support to load .bvh data onto an existing armature 25 | - **Translation**: choose to load translation (a.k.a position) for all bones/only root bone/none from the MOTION part of .bvh file 26 | - **Global Axis Conversion**: whether to convert from bvh axis orientions into Blender's default axis orientions(Z-up/Y forward) 27 | - **Skip frames**: skip first #N frames inthe the MOTION part of .bvh file 28 | - **Add rest pose**: whether to add rest pose as the first frame in the animation curves. 29 | 30 | ![bvh_importer_new_features](/docs/bvh_importer_new_features.jpg) 31 | 32 | 33 | ## Enhanced bvh exporter 34 | 35 | - **Forward and Up**: customerize the output axis orientations 36 | - **Add rest pose**: whether to add rest pose as the first frame in the animation curves. 37 | 38 | ![bvh_exporter_new_features](/docs/bvh_exporter_new_features.jpg) 39 | 40 | ## How to 41 | 42 | ### Install 43 | 44 | 1. download a release version .zip from: https://github.com/walkoncross/blender_bvh_addon_enhanced/raw/main/io_anim_bvh_enhanced.zip; (OR: git clone this repo and make a zip file (io_anim_bvh_enhanced.zip) from ./io_anim_bvh_enhanced); 45 | 46 | 2. Blender -> Edit -> Perferences -> Add-ons -> Install, get the path of io_anim_bvh_enhanced.zip, install and **enable** the add-on 47 | 48 | ![install_addon_1](/docs/install_addon_1.jpg) 49 | 50 | ![install_addon_2](/docs/install_addon_2.jpg) 51 | 52 | 3. After Installation: 53 | 54 | ![enhanced_bvh_importer_menu](/docs/enhanced_bvh_importer_menu.jpg) 55 | 56 | ![enhanced_bvh_exporter_menu](/docs/enhanced_bvh_exporter_menu.jpg) 57 | 58 | ### Load .bvh data onto an existing armature 59 | 1. Select an existing armature object 60 | 2. File -> Import -> Motion Capture (.bvh), enhanced 61 | 3. Make settings 62 | 4. Import BVH 63 | 64 | ![load_onto_existing_armature](/docs/load_onto_existing_armature.jpg) -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walkoncross/blender_bvh_addon_enhanced-zyf/0cabc066ec8756375b58eb78ba798a8f798cf256/docs/.DS_Store -------------------------------------------------------------------------------- /docs/bvh_exporter_new_features.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walkoncross/blender_bvh_addon_enhanced-zyf/0cabc066ec8756375b58eb78ba798a8f798cf256/docs/bvh_exporter_new_features.jpg -------------------------------------------------------------------------------- /docs/bvh_importer_new_features.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walkoncross/blender_bvh_addon_enhanced-zyf/0cabc066ec8756375b58eb78ba798a8f798cf256/docs/bvh_importer_new_features.jpg -------------------------------------------------------------------------------- /docs/enhanced_bvh_exporter_menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walkoncross/blender_bvh_addon_enhanced-zyf/0cabc066ec8756375b58eb78ba798a8f798cf256/docs/enhanced_bvh_exporter_menu.jpg -------------------------------------------------------------------------------- /docs/enhanced_bvh_importer_menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walkoncross/blender_bvh_addon_enhanced-zyf/0cabc066ec8756375b58eb78ba798a8f798cf256/docs/enhanced_bvh_importer_menu.jpg -------------------------------------------------------------------------------- /docs/install_addon_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walkoncross/blender_bvh_addon_enhanced-zyf/0cabc066ec8756375b58eb78ba798a8f798cf256/docs/install_addon_1.jpg -------------------------------------------------------------------------------- /docs/install_addon_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walkoncross/blender_bvh_addon_enhanced-zyf/0cabc066ec8756375b58eb78ba798a8f798cf256/docs/install_addon_2.jpg -------------------------------------------------------------------------------- /docs/load_onto_existing_armature.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walkoncross/blender_bvh_addon_enhanced-zyf/0cabc066ec8756375b58eb78ba798a8f798cf256/docs/load_onto_existing_armature.jpg -------------------------------------------------------------------------------- /io_anim_bvh_enhanced.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walkoncross/blender_bvh_addon_enhanced-zyf/0cabc066ec8756375b58eb78ba798a8f798cf256/io_anim_bvh_enhanced.zip -------------------------------------------------------------------------------- /io_anim_bvh_enhanced/__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | # By zhaoyafei0210@gmail.com (https://github.com/walkoncross) 20 | # Modified on blender's bvh addon (import_bvh.py) by Campbell Barton 21 | 22 | # 23 | 24 | bl_info = { 25 | "name": "BioVision Motion Capture (BVH) format, support more settings", 26 | "author": "Yafei Zhao", 27 | "version": (2, 0, 0), 28 | "blender": (2, 81, 6), 29 | "location": "File > Import-Export", 30 | "description": "Import-Export BVH from armature objects, support more settings", 31 | "warning": "", 32 | "doc_url": "https://github.com/walkoncross/blender_bvh_addon_enhanced/blob/main/README.md", 33 | "support": 'OFFICIAL', 34 | "category": "Import-Export", 35 | } 36 | 37 | if "bpy" in locals(): 38 | import importlib 39 | if "import_bvh_enhanced" in locals(): 40 | importlib.reload(import_bvh_enhanced) 41 | if "export_bvh_enhanced" in locals(): 42 | importlib.reload(export_bvh_enhanced) 43 | 44 | import bpy 45 | from bpy_extras.io_utils import ( 46 | ImportHelper, 47 | ExportHelper, 48 | orientation_helper, 49 | axis_conversion, 50 | ) 51 | from bpy.props import ( 52 | StringProperty, 53 | FloatProperty, 54 | IntProperty, 55 | BoolProperty, 56 | EnumProperty, 57 | ) 58 | 59 | 60 | @orientation_helper(axis_forward='-Z', axis_up='Y') 61 | class ImportBVHEnhanced(bpy.types.Operator, ImportHelper): 62 | """Load a BVH motion capture file""" 63 | bl_idname = "import_anim.bvh_enhanced" 64 | bl_label = "Import BVH" 65 | bl_options = {'REGISTER', 'UNDO'} 66 | 67 | filename_ext = ".bvh" 68 | filter_glob: StringProperty(default="*.bvh", options={'HIDDEN'}) 69 | 70 | # target: EnumProperty( 71 | # items=( 72 | # ('ARMATURE', "Armature", ""), 73 | # ('OBJECT', "Object", ""), 74 | # ), 75 | # name="Target", 76 | # description="Import target type", 77 | # default='ARMATURE', 78 | # ) 79 | 80 | items = ( 81 | ('NEW_ARMATURE', "New Armature", "Load bvh data to create a new armature and "), 82 | ('NEW_PARENTED_OBJECTS', "New Parented Objects", "Load bvh data to creat Parented Objects, each bone as an object"), 83 | ('EXISTING_ARMATURE', "Existing Armature", "Load bvh data onto existing armature, select an armature object before loading"), 84 | ) 85 | # for obj in bpy.context.scene.collection.all_objects: 86 | # if obj.type is "ARMATURE": 87 | # items += ((obj.name, obj.name, "Load bvh data onto existing armature"),) 88 | 89 | target: EnumProperty( 90 | items=items, 91 | name="Target", 92 | description="Import target type", 93 | default='NEW_ARMATURE', 94 | ) 95 | armature: StringProperty( 96 | name="Armature", 97 | description="The selected existing armature to load bvh onto", 98 | default='None', 99 | ) 100 | # target: StringProperty( 101 | # name="Target", 102 | # description="Import target type", 103 | # default='NEW_ARMATURE', 104 | # ) 105 | global_scale: FloatProperty( 106 | name="Scale", 107 | description="Scale the BVH by this value", 108 | min=0.0001, max=1000000.0, 109 | soft_min=0.001, soft_max=100.0, 110 | default=1.0, 111 | ) 112 | skip_frames: IntProperty( 113 | name="Skip Frames", 114 | description="Skip first #skip_frames frames in the .bvh file, e.g. skip the first frame as it may be the rest pose", 115 | default=0, 116 | ) 117 | frame_start: IntProperty( 118 | name="Start Frame", 119 | description="Starting frame for the animation curve", 120 | default=1, 121 | ) 122 | use_fps_scale: BoolProperty( 123 | name="Scale FPS", 124 | description=( 125 | "Scale the framerate from the BVH to the current scenes, " 126 | "otherwise each BVH frame maps directly to a Blender frame" 127 | ), 128 | default=False, 129 | ) 130 | update_scene_fps: BoolProperty( 131 | name="Update Scene FPS", 132 | description=( 133 | "Set the scene framerate to that of the BVH file (note that this " 134 | "nullifies the 'Scale FPS' option, as the scale will be 1:1)" 135 | ), 136 | default=False, 137 | ) 138 | update_scene_duration: BoolProperty( 139 | name="Update Scene Duration", 140 | description="Extend the scene's duration to the BVH duration (never shortens the scene)", 141 | default=False, 142 | ) 143 | use_cyclic: BoolProperty( 144 | name="Loop (Not Implemented)", 145 | description="Loop the animation playback", 146 | default=False, 147 | ) 148 | rotate_mode: EnumProperty( 149 | name="Rotation", 150 | description="Rotation conversion", 151 | items=( 152 | ('QUATERNION', "Quaternion", 153 | "Convert rotations to quaternions"), 154 | ('NATIVE', "Euler (Native)", 155 | "Use the rotation order defined in the BVH file"), 156 | ('XYZ', "Euler (XYZ)", "Convert rotations to euler XYZ"), 157 | ('XZY', "Euler (XZY)", "Convert rotations to euler XZY"), 158 | ('YXZ', "Euler (YXZ)", "Convert rotations to euler YXZ"), 159 | ('YZX', "Euler (YZX)", "Convert rotations to euler YZX"), 160 | ('ZXY', "Euler (ZXY)", "Convert rotations to euler ZXY"), 161 | ('ZYX', "Euler (ZYX)", "Convert rotations to euler ZYX"), 162 | ), 163 | default='NATIVE', 164 | ) 165 | translation_mode: EnumProperty( 166 | name="Translation", 167 | description="How to deal with translation", 168 | items=( 169 | ('TRANSLATION_FOR_ALL_BONES', "All bones", "Load translation for all bones"), 170 | ('TRANSLATION_FOR_ROOT_BONE', "Only root", "Only load translation for root bone"), 171 | ('TRANSLATION_FOR_NONE_BONE', "None", "Discard translation for all bones"), 172 | ), 173 | default='TRANSLATION_FOR_ALL_BONES', 174 | ) 175 | apply_axis_conversion: BoolProperty( 176 | name="Global Axis Conversion", 177 | description="Make global transform from BVH file's coordinates system into Blender's coordinates system", 178 | default=False, 179 | ) 180 | add_rest_pose_as_first_frame: BoolProperty( 181 | name="Add Rest Pose", 182 | description="Add rest pose as the first frame", 183 | default=False, 184 | ) 185 | def execute(self, context): 186 | keywords = self.as_keywords( 187 | ignore=( 188 | "axis_forward", 189 | "axis_up", 190 | "filter_glob", 191 | ) 192 | ) 193 | global_matrix = axis_conversion( 194 | from_forward=self.axis_forward, 195 | from_up=self.axis_up, 196 | ).to_4x4() 197 | 198 | keywords["global_matrix"] = global_matrix 199 | 200 | if keywords["target"] == "EXISTING_ARMATURE": 201 | if keywords["armature"] in bpy.data.objects.keys(): 202 | obj = bpy.data.objects[keywords["armature"]] 203 | 204 | if obj.type == 'ARMATURE': 205 | keywords["target"] = keywords["armature"] 206 | else: 207 | keywords["target"] = "NEW_ARMATURE" 208 | else: 209 | keywords["target"] = "NEW_ARMATURE" 210 | 211 | keywords.pop('armature', None) 212 | 213 | from . import import_bvh_enhanced 214 | return import_bvh_enhanced.load(context, report=self.report, **keywords) 215 | 216 | def draw(self, context): 217 | pass 218 | 219 | 220 | class BVH_ENHANCED_PT_import_main(bpy.types.Panel): 221 | bl_space_type = 'FILE_BROWSER' 222 | bl_region_type = 'TOOL_PROPS' 223 | bl_label = "" 224 | bl_parent_id = "FILE_PT_operator" 225 | bl_options = {'HIDE_HEADER'} 226 | 227 | @classmethod 228 | def poll(cls, context): 229 | sfile = context.space_data 230 | operator = sfile.active_operator 231 | 232 | return operator.bl_idname == "IMPORT_ANIM_OT_bvh_enhanced" 233 | 234 | def draw(self, context): 235 | layout = self.layout 236 | layout.use_property_split = True 237 | layout.use_property_decorate = False # No animation. 238 | 239 | sfile = context.space_data 240 | operator = sfile.active_operator 241 | 242 | obj = context.object 243 | if obj and obj.type == 'ARMATURE': 244 | operator.armature = obj.name 245 | 246 | layout.prop(operator, "target") 247 | 248 | if operator.target == "EXISTING_ARMATURE": 249 | layout.prop(operator, "armature") 250 | 251 | 252 | class BVH_ENHANCED_PT_import_transform(bpy.types.Panel): 253 | bl_space_type = 'FILE_BROWSER' 254 | bl_region_type = 'TOOL_PROPS' 255 | bl_label = "Transform" 256 | bl_parent_id = "FILE_PT_operator" 257 | 258 | @classmethod 259 | def poll(cls, context): 260 | sfile = context.space_data 261 | operator = sfile.active_operator 262 | 263 | # print('--> in BVH_ENHANCED_PT_import_transform.poll()') 264 | # print(operator) 265 | # print(operator.bl_idname) 266 | 267 | return operator.bl_idname == "IMPORT_ANIM_OT_bvh_enhanced" 268 | 269 | def draw(self, context): 270 | layout = self.layout 271 | layout.use_property_split = True 272 | layout.use_property_decorate = False # No animation. 273 | 274 | sfile = context.space_data 275 | operator = sfile.active_operator 276 | 277 | layout.prop(operator, "global_scale") 278 | layout.prop(operator, "rotate_mode") 279 | layout.prop(operator, "axis_forward") 280 | layout.prop(operator, "axis_up") 281 | layout.prop(operator, "translation_mode") 282 | layout.prop(operator, "apply_axis_conversion") 283 | 284 | 285 | class BVH_ENHANCED_PT_import_animation(bpy.types.Panel): 286 | bl_space_type = 'FILE_BROWSER' 287 | bl_region_type = 'TOOL_PROPS' 288 | bl_label = "Animation" 289 | bl_parent_id = "FILE_PT_operator" 290 | 291 | @classmethod 292 | def poll(cls, context): 293 | sfile = context.space_data 294 | operator = sfile.active_operator 295 | 296 | # print('--> in BVH_ENHANCED_PT_import_animation.poll()') 297 | # print(operator) 298 | # print(operator.bl_idname) 299 | 300 | return operator.bl_idname == "IMPORT_ANIM_OT_bvh_enhanced" 301 | 302 | def draw(self, context): 303 | layout = self.layout 304 | layout.use_property_split = True 305 | layout.use_property_decorate = False # No animation. 306 | 307 | sfile = context.space_data 308 | operator = sfile.active_operator 309 | 310 | layout.prop(operator, "skip_frames") 311 | layout.prop(operator, "frame_start") 312 | 313 | if operator.target != "NEW_PARENTED_OBJECTS": 314 | layout.prop(operator, "use_fps_scale") 315 | 316 | layout.prop(operator, "use_cyclic") 317 | layout.prop(operator, "add_rest_pose_as_first_frame") 318 | 319 | layout.prop(operator, "update_scene_fps") 320 | layout.prop(operator, "update_scene_duration") 321 | 322 | 323 | @orientation_helper(axis_forward='-Z', axis_up='Y') 324 | class ExportBVHEnhanced(bpy.types.Operator, ExportHelper): 325 | """Save a BVH motion capture file from an armature""" 326 | bl_idname = "export_anim.bvh_enhanced" 327 | bl_label = "Export BVH" 328 | 329 | filename_ext = ".bvh" 330 | filter_glob: StringProperty( 331 | default="*.bvh", 332 | options={'HIDDEN'}, 333 | ) 334 | # target: EnumProperty( 335 | # items=( 336 | # ('ARMATURE', "Armature", ""), 337 | # ('OBJECT', "Object", ""), 338 | # ), 339 | # name="Target", 340 | # description="Import target type", 341 | # default='ARMATURE', 342 | # ) 343 | global_scale: FloatProperty( 344 | name="Scale", 345 | description="Scale the BVH by this value", 346 | min=0.0001, max=1000000.0, 347 | soft_min=0.001, soft_max=100.0, 348 | default=1.0, 349 | ) 350 | frame_start: IntProperty( 351 | name="Start Frame", 352 | description="Starting frame to export", 353 | default=0, 354 | ) 355 | frame_end: IntProperty( 356 | name="End Frame", 357 | description="End frame to export", 358 | default=0, 359 | ) 360 | rotate_mode: EnumProperty( 361 | name="Rotation", 362 | description="Rotation conversion", 363 | items=( 364 | ('NATIVE', "Euler (Native)", 365 | "Use the rotation order defined in the armature object"), 366 | ('XYZ', "Euler (XYZ)", "Convert rotations to euler XYZ"), 367 | ('XZY', "Euler (XZY)", "Convert rotations to euler XZY"), 368 | ('YXZ', "Euler (YXZ)", "Convert rotations to euler YXZ"), 369 | ('YZX', "Euler (YZX)", "Convert rotations to euler YZX"), 370 | ('ZXY', "Euler (ZXY)", "Convert rotations to euler ZXY"), 371 | ('ZYX', "Euler (ZYX)", "Convert rotations to euler ZYX"), 372 | ), 373 | default='NATIVE', 374 | ) 375 | root_transform_only: BoolProperty( 376 | name="Root Translation Only", 377 | description="Only write out translation channels for the root bone", 378 | default=True, 379 | ) 380 | add_rest_pose_as_first_frame: BoolProperty( 381 | name="Add Rest Pose", 382 | description="Add rest pose as the first frame", 383 | default=False, 384 | ) 385 | 386 | @classmethod 387 | def poll(cls, context): 388 | obj = context.object 389 | return obj and obj.type == 'ARMATURE' 390 | 391 | def invoke(self, context, event): 392 | self.frame_start = context.scene.frame_start 393 | self.frame_end = context.scene.frame_end 394 | 395 | return super().invoke(context, event) 396 | 397 | def execute(self, context): 398 | if self.frame_start == 0 and self.frame_end == 0: 399 | self.frame_start = context.scene.frame_start 400 | self.frame_end = context.scene.frame_end 401 | 402 | keywords = self.as_keywords( 403 | ignore=( 404 | "axis_forward", 405 | "axis_up", 406 | "check_existing", 407 | "filter_glob", 408 | ) 409 | ) 410 | 411 | # global_matrix: from current Blender coordinates system to output coordinates system 412 | global_matrix = axis_conversion( 413 | from_forward=self.axis_forward, 414 | from_up=self.axis_up, 415 | ).to_4x4().inverted() 416 | 417 | keywords["global_matrix"] = global_matrix 418 | 419 | from . import export_bvh_enhanced 420 | return export_bvh_enhanced.save(context, **keywords) 421 | 422 | def draw(self, context): 423 | pass 424 | 425 | 426 | class BVH_ENHANCED_PT_export_transform(bpy.types.Panel): 427 | bl_space_type = 'FILE_BROWSER' 428 | bl_region_type = 'TOOL_PROPS' 429 | bl_label = "Transform" 430 | bl_parent_id = "FILE_PT_operator" 431 | 432 | @classmethod 433 | def poll(cls, context): 434 | sfile = context.space_data 435 | operator = sfile.active_operator 436 | 437 | return operator.bl_idname == "EXPORT_ANIM_OT_bvh_enhanced" 438 | 439 | def draw(self, context): 440 | layout = self.layout 441 | layout.use_property_split = True 442 | layout.use_property_decorate = False # No animation. 443 | 444 | sfile = context.space_data 445 | operator = sfile.active_operator 446 | 447 | layout.prop(operator, "global_scale") 448 | layout.prop(operator, "rotate_mode") 449 | layout.prop(operator, "root_transform_only") 450 | layout.prop(operator, "axis_forward") 451 | layout.prop(operator, "axis_up") 452 | 453 | 454 | class BVH_ENHANCED_PT_export_animation(bpy.types.Panel): 455 | bl_space_type = 'FILE_BROWSER' 456 | bl_region_type = 'TOOL_PROPS' 457 | bl_label = "Animation" 458 | bl_parent_id = "FILE_PT_operator" 459 | 460 | @classmethod 461 | def poll(cls, context): 462 | sfile = context.space_data 463 | operator = sfile.active_operator 464 | 465 | return operator.bl_idname == "EXPORT_ANIM_OT_bvh_enhanced" 466 | 467 | def draw(self, context): 468 | layout = self.layout 469 | layout.use_property_split = True 470 | layout.use_property_decorate = False # No animation. 471 | 472 | sfile = context.space_data 473 | operator = sfile.active_operator 474 | 475 | col = layout.column(align=True) 476 | col.prop(operator, "frame_start", text="Frame Start") 477 | col.prop(operator, "frame_end", text="End") 478 | 479 | layout.prop(operator, "add_rest_pose_as_first_frame") 480 | 481 | 482 | def menu_func_import(self, context): 483 | self.layout.operator(ImportBVHEnhanced.bl_idname, 484 | text="Motion Capture (.bvh), enhanced") 485 | 486 | def menu_func_export(self, context): 487 | self.layout.operator(ExportBVHEnhanced.bl_idname, text="Motion Capture (.bvh), enhanced") 488 | 489 | 490 | classes = ( 491 | ImportBVHEnhanced, 492 | BVH_ENHANCED_PT_import_main, 493 | BVH_ENHANCED_PT_import_transform, 494 | BVH_ENHANCED_PT_import_animation, 495 | ExportBVHEnhanced, 496 | BVH_ENHANCED_PT_export_transform, 497 | BVH_ENHANCED_PT_export_animation, 498 | ) 499 | 500 | 501 | def register(): 502 | for cls in classes: 503 | bpy.utils.register_class(cls) 504 | 505 | bpy.types.TOPBAR_MT_file_import.append(menu_func_import) 506 | bpy.types.TOPBAR_MT_file_export.append(menu_func_export) 507 | 508 | 509 | def unregister(): 510 | for cls in classes: 511 | bpy.utils.unregister_class(cls) 512 | 513 | bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) 514 | bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) 515 | 516 | 517 | if __name__ == "__main__": 518 | register() 519 | -------------------------------------------------------------------------------- /io_anim_bvh_enhanced/export_bvh_enhanced.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | # 20 | 21 | # Script copyright (C) Campbell Barton 22 | # fixes from Andrea Rugliancich 23 | 24 | import bpy 25 | 26 | 27 | def write_armature( 28 | context, 29 | filepath, 30 | frame_start, 31 | frame_end, 32 | global_scale=1.0, 33 | rotate_mode='NATIVE', 34 | root_transform_only=False, 35 | global_matrix=None, 36 | add_rest_pose_as_first_frame = False, 37 | ): 38 | 39 | def ensure_rot_order(rot_order_str): 40 | if set(rot_order_str) != {'X', 'Y', 'Z'}: 41 | rot_order_str = "XYZ" 42 | return rot_order_str 43 | 44 | from mathutils import Matrix, Euler 45 | from math import degrees 46 | 47 | file = open(filepath, "w", encoding="utf8", newline="\n") 48 | 49 | obj = context.object 50 | arm = obj.data 51 | 52 | # Build a dictionary of children. 53 | # None for parentless 54 | children = {None: []} 55 | 56 | # initialize with blank lists 57 | for bone in arm.bones: 58 | children[bone.name] = [] 59 | 60 | # keep bone order from armature, no sorting, not esspential but means 61 | # we can maintain order from import -> export which secondlife incorrectly expects. 62 | for bone in arm.bones: 63 | children[getattr(bone.parent, "name", None)].append(bone.name) 64 | 65 | # bone name list in the order that the bones are written 66 | serialized_names = [] 67 | 68 | node_locations = {} 69 | 70 | # global_matrix: from current Blender coordinates system to output coordinates system 71 | if global_matrix: 72 | global_matrix_inv = global_matrix.inverted() 73 | global_matrix_3x3 = global_matrix.to_3x3() 74 | 75 | file.write("HIERARCHY\n") 76 | 77 | def write_recursive_nodes(bone_name, indent): 78 | my_children = children[bone_name] 79 | 80 | indent_str = "\t" * indent 81 | 82 | bone = arm.bones[bone_name] 83 | pose_bone = obj.pose.bones[bone_name] 84 | loc = bone.head_local 85 | node_locations[bone_name] = loc 86 | 87 | if rotate_mode == "NATIVE": 88 | rot_order_str = ensure_rot_order(pose_bone.rotation_mode) 89 | else: 90 | rot_order_str = rotate_mode 91 | 92 | # make relative if we can 93 | if bone.parent: 94 | loc = loc - node_locations[bone.parent.name] 95 | 96 | if indent: 97 | file.write("%sJOINT %s\n" % (indent_str, bone_name)) 98 | else: 99 | file.write("%sROOT %s\n" % (indent_str, bone_name)) 100 | 101 | file.write("%s{\n" % indent_str) 102 | 103 | # global_matrix: from current Blender coordinates system to output coordinates system 104 | if global_matrix: 105 | loc = global_matrix_3x3 @ loc 106 | 107 | file.write("%s\tOFFSET %.6f %.6f %.6f\n" % (indent_str, *(loc * global_scale))) 108 | if (bone.use_connect or root_transform_only) and bone.parent: 109 | file.write("%s\tCHANNELS 3 %srotation %srotation %srotation\n" % (indent_str, *rot_order_str)) 110 | else: 111 | file.write("%s\tCHANNELS 6 Xposition Yposition Zposition %srotation %srotation %srotation\n" % (indent_str, *rot_order_str)) 112 | 113 | if my_children: 114 | # store the location for the children 115 | # to get their relative offset 116 | 117 | # Write children 118 | for child_bone in my_children: 119 | serialized_names.append(child_bone) 120 | write_recursive_nodes(child_bone, indent + 1) 121 | 122 | else: 123 | # Write the bone end. 124 | file.write("%s\tEnd Site\n" % indent_str) 125 | file.write("%s\t{\n" % indent_str) 126 | loc = bone.tail_local - node_locations[bone_name] 127 | 128 | # global_matrix: from current Blender coordinates system to output coordinates system 129 | if global_matrix: 130 | loc = global_matrix_3x3 @ loc 131 | 132 | file.write("%s\t\tOFFSET %.6f %.6f %.6f\n" % (indent_str, *(loc * global_scale))) 133 | file.write("%s\t}\n" % indent_str) 134 | 135 | file.write("%s}\n" % indent_str) 136 | 137 | if len(children[None]) == 1: 138 | key = children[None][0] 139 | serialized_names.append(key) 140 | indent = 0 141 | 142 | write_recursive_nodes(key, indent) 143 | 144 | else: 145 | # Write a dummy parent node, with a dummy key name 146 | # Just be sure it's not used by another bone! 147 | i = 0 148 | key = "__%d" % i 149 | while key in children: 150 | i += 1 151 | key = "__%d" % i 152 | file.write("ROOT %s\n" % key) 153 | file.write("{\n") 154 | file.write("\tOFFSET 0.0 0.0 0.0\n") 155 | file.write("\tCHANNELS 0\n") # Xposition Yposition Zposition Xrotation Yrotation Zrotation 156 | indent = 1 157 | 158 | # Write children 159 | for child_bone in children[None]: 160 | serialized_names.append(child_bone) 161 | write_recursive_nodes(child_bone, indent) 162 | 163 | file.write("}\n") 164 | 165 | # redefine bones as sorted by serialized_names 166 | # so we can write motion 167 | 168 | class DecoratedBone: 169 | __slots__ = ( 170 | # Bone name, used as key in many places. 171 | "name", 172 | "parent", # decorated bone parent, set in a later loop 173 | # Blender armature bone. 174 | "rest_bone", 175 | # Blender pose bone. 176 | "pose_bone", 177 | # Blender pose matrix. 178 | "pose_mat", 179 | # Blender rest matrix (armature space). 180 | "rest_arm_mat", 181 | # Blender rest matrix (local space). 182 | "rest_local_mat", 183 | # Pose_mat inverted. 184 | "pose_imat", 185 | # Rest_arm_mat inverted. 186 | "rest_arm_imat", 187 | # Rest_local_mat inverted. 188 | "rest_local_imat", 189 | # Last used euler to preserve euler compatibility in between keyframes. 190 | "prev_euler", 191 | # Is the bone disconnected to the parent bone? 192 | "skip_position", 193 | "rot_order", 194 | "rot_order_str", 195 | # Needed for the euler order when converting from a matrix. 196 | "rot_order_str_reverse", 197 | ) 198 | 199 | _eul_order_lookup = { 200 | 'XYZ': (0, 1, 2), 201 | 'XZY': (0, 2, 1), 202 | 'YXZ': (1, 0, 2), 203 | 'YZX': (1, 2, 0), 204 | 'ZXY': (2, 0, 1), 205 | 'ZYX': (2, 1, 0), 206 | } 207 | 208 | def __init__(self, bone_name): 209 | self.name = bone_name 210 | self.rest_bone = arm.bones[bone_name] 211 | self.pose_bone = obj.pose.bones[bone_name] 212 | 213 | if rotate_mode == "NATIVE": 214 | self.rot_order_str = ensure_rot_order(self.pose_bone.rotation_mode) 215 | else: 216 | self.rot_order_str = rotate_mode 217 | self.rot_order_str_reverse = self.rot_order_str[::-1] 218 | 219 | self.rot_order = DecoratedBone._eul_order_lookup[self.rot_order_str] 220 | 221 | self.pose_mat = self.pose_bone.matrix 222 | 223 | # mat = self.rest_bone.matrix # UNUSED 224 | self.rest_arm_mat = self.rest_bone.matrix_local 225 | self.rest_local_mat = self.rest_bone.matrix 226 | 227 | # inverted mats 228 | self.pose_imat = self.pose_mat.inverted() 229 | self.rest_arm_imat = self.rest_arm_mat.inverted() 230 | self.rest_local_imat = self.rest_local_mat.inverted() 231 | 232 | self.parent = None 233 | self.prev_euler = Euler((0.0, 0.0, 0.0), self.rot_order_str_reverse) 234 | self.skip_position = ((self.rest_bone.use_connect or root_transform_only) and self.rest_bone.parent) 235 | 236 | def update_posedata(self): 237 | self.pose_mat = self.pose_bone.matrix 238 | self.pose_imat = self.pose_mat.inverted() 239 | 240 | def __repr__(self): 241 | if self.parent: 242 | return "[\"%s\" child on \"%s\"]\n" % (self.name, self.parent.name) 243 | else: 244 | return "[\"%s\" root bone]\n" % (self.name) 245 | 246 | bones_decorated = [DecoratedBone(bone_name) for bone_name in serialized_names] 247 | 248 | # Assign parents 249 | bones_decorated_dict = {dbone.name: dbone for dbone in bones_decorated} 250 | for dbone in bones_decorated: 251 | parent = dbone.rest_bone.parent 252 | if parent: 253 | dbone.parent = bones_decorated_dict[parent.name] 254 | del bones_decorated_dict 255 | # finish assigning parents 256 | 257 | scene = context.scene 258 | frame_current = scene.frame_current 259 | num_frames = frame_end - frame_start + 1 260 | if add_rest_pose_as_first_frame: 261 | num_frames += 1 262 | 263 | file.write("MOTION\n") 264 | file.write("Frames: %d\n" % (num_frames)) 265 | file.write("Frame Time: %.6f\n" % (1.0 / (scene.render.fps / scene.render.fps_base))) 266 | 267 | if add_rest_pose_as_first_frame: 268 | for dbone in bones_decorated: 269 | if not dbone.skip_position: 270 | file.write("%.6f %.6f %.6f " % (0, 0, 0)) 271 | 272 | file.write("%.6f %.6f %.6f " % (0, 0, 0)) 273 | 274 | file.write("\n") 275 | 276 | for frame in range(frame_start, frame_end + 1): 277 | scene.frame_set(frame) 278 | 279 | for dbone in bones_decorated: 280 | dbone.update_posedata() 281 | 282 | for dbone in bones_decorated: 283 | trans = Matrix.Translation(dbone.rest_bone.head_local) 284 | itrans = Matrix.Translation(-dbone.rest_bone.head_local) 285 | 286 | if dbone.parent: 287 | mat_final = dbone.parent.rest_arm_mat @ dbone.parent.pose_imat @ dbone.pose_mat @ dbone.rest_arm_imat 288 | mat_final = itrans @ mat_final @ trans 289 | loc = mat_final.to_translation() + (dbone.rest_bone.head_local - dbone.parent.rest_bone.head_local) 290 | else: 291 | mat_final = dbone.pose_mat @ dbone.rest_arm_imat 292 | mat_final = itrans @ mat_final @ trans 293 | loc = mat_final.to_translation() + dbone.rest_bone.head 294 | 295 | # global_matrix: from current Blender coordinates system to output coordinates system 296 | if global_matrix: 297 | loc = global_matrix_3x3 @ loc 298 | mat_final = global_matrix @ mat_final @ global_matrix_inv 299 | 300 | # keep eulers compatible, no jumping on interpolation. 301 | rot = mat_final.to_euler(dbone.rot_order_str_reverse, dbone.prev_euler) 302 | 303 | if not dbone.skip_position: 304 | file.write("%.6f %.6f %.6f " % (loc * global_scale)[:]) 305 | 306 | file.write("%.6f %.6f %.6f " % (degrees(rot[dbone.rot_order[0]]), degrees(rot[dbone.rot_order[1]]), degrees(rot[dbone.rot_order[2]]))) 307 | 308 | dbone.prev_euler = rot 309 | 310 | file.write("\n") 311 | 312 | file.close() 313 | 314 | scene.frame_set(frame_current) 315 | 316 | print("BVH Exported: %s frames:%d\n" % (filepath, frame_end - frame_start + 1)) 317 | 318 | 319 | def save( 320 | context, filepath="", 321 | frame_start=-1, 322 | frame_end=-1, 323 | global_scale=1.0, 324 | rotate_mode="NATIVE", 325 | root_transform_only=False, 326 | global_matrix=None, 327 | add_rest_pose_as_first_frame = False, 328 | ): 329 | # global_matrix: from current Blender coordinates system to output coordinates system 330 | write_armature( 331 | context, filepath, 332 | frame_start=frame_start, 333 | frame_end=frame_end, 334 | global_scale=global_scale, 335 | rotate_mode=rotate_mode, 336 | root_transform_only=root_transform_only, 337 | global_matrix=global_matrix, 338 | add_rest_pose_as_first_frame = add_rest_pose_as_first_frame, 339 | ) 340 | 341 | return {'FINISHED'} 342 | -------------------------------------------------------------------------------- /io_anim_bvh_enhanced/import_bvh_enhanced.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | # 20 | 21 | # By zhaoyafei0210@gmail.com (https://github.com/walkoncross) 22 | # Modified on blender's bvh addon (import_bvh.py) by Campbell Barton 23 | 24 | from math import radians, ceil 25 | 26 | import bpy 27 | from mathutils import Vector, Euler, Matrix 28 | import math 29 | 30 | class BVH_Node: 31 | __slots__ = ( 32 | # Bvh joint name. 33 | 'name', 34 | # BVH_Node type or None for no parent. 35 | 'parent', 36 | # A list of children of this type.. 37 | 'children', 38 | # Worldspace rest location for the head of this node. 39 | 'rest_head_world', 40 | # Localspace rest location for the head of this node. 41 | 'rest_head_local', 42 | # Worldspace rest location for the tail of this node. 43 | 'rest_tail_world', 44 | # Worldspace rest location for the tail of this node. 45 | 'rest_tail_local', 46 | # List of 6 ints, -1 for an unused channel, 47 | # otherwise an index for the BVH motion data lines, 48 | # loc triple then rot triple. 49 | 'channels', 50 | # A triple of indices as to the order rotation is applied. 51 | # [0,1,2] is x/y/z - [None, None, None] if no rotation.. 52 | 'rot_order', 53 | # Same as above but a string 'XYZ' format.. 54 | 'rot_order_str', 55 | # A list one tuple's one for each frame: (locx, locy, locz, rotx, roty, rotz), 56 | # euler rotation ALWAYS stored xyz order, even when native used. 57 | 'anim_data', 58 | # Convenience function, bool, same as: (channels[0] != -1 or channels[1] != -1 or channels[2] != -1). 59 | 'has_loc', 60 | # Convenience function, bool, same as: (channels[3] != -1 or channels[4] != -1 or channels[5] != -1). 61 | 'has_rot', 62 | # Index from the file, not strictly needed but nice to maintain order. 63 | 'index', 64 | # Use this for whatever you want. 65 | 'temp', 66 | ) 67 | 68 | _eul_order_lookup = { 69 | (None, None, None): 'XYZ', # XXX Dummy one, no rotation anyway! 70 | (0, 1, 2): 'XYZ', 71 | (0, 2, 1): 'XZY', 72 | (1, 0, 2): 'YXZ', 73 | (1, 2, 0): 'YZX', 74 | (2, 0, 1): 'ZXY', 75 | (2, 1, 0): 'ZYX', 76 | } 77 | 78 | def __init__(self, name, rest_head_world, rest_head_local, parent, channels, rot_order, index): 79 | self.name = name 80 | self.rest_head_world = rest_head_world 81 | self.rest_head_local = rest_head_local 82 | self.rest_tail_world = None 83 | self.rest_tail_local = None 84 | self.parent = parent 85 | self.channels = channels 86 | self.rot_order = tuple(rot_order) 87 | self.rot_order_str = BVH_Node._eul_order_lookup[self.rot_order] 88 | self.index = index 89 | 90 | # convenience functions 91 | self.has_loc = channels[0] != - \ 92 | 1 or channels[1] != -1 or channels[2] != -1 93 | self.has_rot = channels[3] != - \ 94 | 1 or channels[4] != -1 or channels[5] != -1 95 | 96 | self.children = [] 97 | 98 | # List of 6 length tuples: (lx, ly, lz, rx, ry, rz) 99 | # even if the channels aren't used they will just be zero. 100 | self.anim_data = [(0, 0, 0, 0, 0, 0)] 101 | 102 | def __repr__(self): 103 | return ( 104 | "BVH name: '%s', rest_loc:(%.3f,%.3f,%.3f), rest_tail:(%.3f,%.3f,%.3f)" % ( 105 | self.name, 106 | *self.rest_head_world, 107 | *self.rest_head_world, 108 | ) 109 | ) 110 | 111 | 112 | def sorted_nodes(bvh_nodes): 113 | bvh_nodes_list = list(bvh_nodes.values()) 114 | bvh_nodes_list.sort(key=lambda bvh_node: bvh_node.index) 115 | return bvh_nodes_list 116 | 117 | 118 | def read_bvh(context, file_path, rotate_mode='XYZ', global_scale=1.0): 119 | # File loading stuff 120 | # Open the file for importing 121 | file = open(file_path, 'rU') 122 | 123 | # Separate into a list of lists, each line a list of words. 124 | file_lines = file.readlines() 125 | # Non standard carrage returns? 126 | if len(file_lines) == 1: 127 | file_lines = file_lines[0].split('\r') 128 | 129 | # Split by whitespace. 130 | file_lines = [ll for ll in [l.split() for l in file_lines] if ll] 131 | 132 | # Create hierarchy as empties 133 | if file_lines[0][0].lower() == 'hierarchy': 134 | # print 'Importing the BVH Hierarchy for:', file_path 135 | pass 136 | else: 137 | raise Exception("This is not a BVH file") 138 | 139 | bvh_nodes = {None: None} 140 | bvh_nodes_serial = [None] 141 | bvh_frame_count = None 142 | bvh_frame_time = None 143 | 144 | channelIndex = -1 145 | 146 | lineIdx = 0 # An index for the file. 147 | while lineIdx < len(file_lines) - 1: 148 | if file_lines[lineIdx][0].lower() in {'root', 'joint'}: 149 | 150 | # Join spaces into 1 word with underscores joining it. 151 | if len(file_lines[lineIdx]) > 2: 152 | file_lines[lineIdx][1] = '_'.join(file_lines[lineIdx][1:]) 153 | file_lines[lineIdx] = file_lines[lineIdx][:2] 154 | 155 | # MAY NEED TO SUPPORT MULTIPLE ROOTS HERE! Still unsure weather multiple roots are possible? 156 | 157 | # Make sure the names are unique - Object names will match joint names exactly and both will be unique. 158 | name = file_lines[lineIdx][1] 159 | 160 | # print '%snode: %s, parent: %s' % (len(bvh_nodes_serial) * ' ', name, bvh_nodes_serial[-1]) 161 | 162 | lineIdx += 2 # Increment to the next line (Offset) 163 | rest_head_local = global_scale * Vector(( 164 | float(file_lines[lineIdx][1]), 165 | float(file_lines[lineIdx][2]), 166 | float(file_lines[lineIdx][3]), 167 | )) 168 | lineIdx += 1 # Increment to the next line (Channels) 169 | 170 | # newChannel[Xposition, Yposition, Zposition, Xrotation, Yrotation, Zrotation] 171 | # newChannel references indices to the motiondata, 172 | # if not assigned then -1 refers to the last value that will be added on loading at a value of zero, this is appended 173 | # We'll add a zero value onto the end of the MotionDATA so this always refers to a value. 174 | my_channel = [-1, -1, -1, -1, -1, -1] 175 | my_rot_order = [None, None, None] 176 | rot_count = 0 177 | for channel in file_lines[lineIdx][2:]: 178 | channel = channel.lower() 179 | channelIndex += 1 # So the index points to the right channel 180 | if channel == 'xposition': 181 | my_channel[0] = channelIndex 182 | elif channel == 'yposition': 183 | my_channel[1] = channelIndex 184 | elif channel == 'zposition': 185 | my_channel[2] = channelIndex 186 | 187 | elif channel == 'xrotation': 188 | my_channel[3] = channelIndex 189 | my_rot_order[rot_count] = 0 190 | rot_count += 1 191 | elif channel == 'yrotation': 192 | my_channel[4] = channelIndex 193 | my_rot_order[rot_count] = 1 194 | rot_count += 1 195 | elif channel == 'zrotation': 196 | my_channel[5] = channelIndex 197 | my_rot_order[rot_count] = 2 198 | rot_count += 1 199 | 200 | channels = file_lines[lineIdx][2:] 201 | 202 | my_parent = bvh_nodes_serial[-1] # account for none 203 | 204 | # Apply the parents offset accumulatively 205 | if my_parent is None: 206 | rest_head_world = Vector(rest_head_local) 207 | else: 208 | rest_head_world = my_parent.rest_head_world + rest_head_local 209 | 210 | bvh_node = bvh_nodes[name] = BVH_Node( 211 | name, 212 | rest_head_world, 213 | rest_head_local, 214 | my_parent, 215 | my_channel, 216 | my_rot_order, 217 | len(bvh_nodes) - 1, 218 | ) 219 | 220 | # If we have another child then we can call ourselves a parent, else 221 | bvh_nodes_serial.append(bvh_node) 222 | 223 | # Account for an end node. 224 | # There is sometimes a name after 'End Site' but we will ignore it. 225 | if file_lines[lineIdx][0].lower() == 'end' and file_lines[lineIdx][1].lower() == 'site': 226 | # Increment to the next line (Offset) 227 | lineIdx += 2 228 | rest_tail = global_scale * Vector(( 229 | float(file_lines[lineIdx][1]), 230 | float(file_lines[lineIdx][2]), 231 | float(file_lines[lineIdx][3]), 232 | )) 233 | 234 | bvh_nodes_serial[-1].rest_tail_world = bvh_nodes_serial[-1].rest_head_world + rest_tail 235 | bvh_nodes_serial[-1].rest_tail_local = bvh_nodes_serial[-1].rest_head_local + rest_tail 236 | 237 | # Just so we can remove the parents in a uniform way, 238 | # the end has kids so this is a placeholder. 239 | bvh_nodes_serial.append(None) 240 | 241 | if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0] == '}': # == ['}'] 242 | bvh_nodes_serial.pop() # Remove the last item 243 | 244 | # End of the hierarchy. Begin the animation section of the file with 245 | # the following header. 246 | # MOTION 247 | # Frames: n 248 | # Frame Time: dt 249 | if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0].lower() == 'motion': 250 | lineIdx += 1 # Read frame count. 251 | if ( 252 | len(file_lines[lineIdx]) == 2 and 253 | file_lines[lineIdx][0].lower() == 'frames:' 254 | ): 255 | bvh_frame_count = int(file_lines[lineIdx][1]) 256 | 257 | lineIdx += 1 # Read frame rate. 258 | if ( 259 | len(file_lines[lineIdx]) == 3 and 260 | file_lines[lineIdx][0].lower() == 'frame' and 261 | file_lines[lineIdx][1].lower() == 'time:' 262 | ): 263 | bvh_frame_time = float(file_lines[lineIdx][2]) 264 | 265 | lineIdx += 1 # Set the cursor to the first frame 266 | 267 | break 268 | 269 | lineIdx += 1 270 | 271 | # Remove the None value used for easy parent reference 272 | del bvh_nodes[None] 273 | # Don't use anymore 274 | del bvh_nodes_serial 275 | 276 | # importing world with any order but nicer to maintain order 277 | # second life expects it, which isn't to spec. 278 | bvh_nodes_list = sorted_nodes(bvh_nodes) 279 | 280 | while lineIdx < len(file_lines): 281 | line = file_lines[lineIdx] 282 | for bvh_node in bvh_nodes_list: 283 | # for bvh_node in bvh_nodes_serial: 284 | lx = ly = lz = rx = ry = rz = 0.0 285 | channels = bvh_node.channels 286 | anim_data = bvh_node.anim_data 287 | if channels[0] != -1: 288 | lx = global_scale * float(line[channels[0]]) 289 | 290 | if channels[1] != -1: 291 | ly = global_scale * float(line[channels[1]]) 292 | 293 | if channels[2] != -1: 294 | lz = global_scale * float(line[channels[2]]) 295 | 296 | if channels[3] != -1 or channels[4] != -1 or channels[5] != -1: 297 | 298 | rx = radians(float(line[channels[3]])) 299 | ry = radians(float(line[channels[4]])) 300 | rz = radians(float(line[channels[5]])) 301 | 302 | # Done importing motion data # 303 | anim_data.append((lx, ly, lz, rx, ry, rz)) 304 | lineIdx += 1 305 | 306 | # Assign children 307 | for bvh_node in bvh_nodes_list: 308 | bvh_node_parent = bvh_node.parent 309 | if bvh_node_parent: 310 | bvh_node_parent.children.append(bvh_node) 311 | 312 | # Now set the tip of each bvh_node 313 | for bvh_node in bvh_nodes_list: 314 | 315 | if not bvh_node.rest_tail_world: 316 | if len(bvh_node.children) == 0: 317 | # could just fail here, but rare BVH files have childless nodes 318 | bvh_node.rest_tail_world = Vector(bvh_node.rest_head_world) 319 | bvh_node.rest_tail_local = Vector(bvh_node.rest_head_local) 320 | elif len(bvh_node.children) == 1: 321 | bvh_node.rest_tail_world = Vector( 322 | bvh_node.children[0].rest_head_world) 323 | bvh_node.rest_tail_local = bvh_node.rest_head_local + \ 324 | bvh_node.children[0].rest_head_local 325 | else: 326 | # allow this, see above 327 | # if not bvh_node.children: 328 | # raise Exception("bvh node has no end and no children. bad file") 329 | 330 | # Removed temp for now 331 | rest_tail_world = Vector((0.0, 0.0, 0.0)) 332 | rest_tail_local = Vector((0.0, 0.0, 0.0)) 333 | for bvh_node_child in bvh_node.children: 334 | rest_tail_world += bvh_node_child.rest_head_world 335 | rest_tail_local += bvh_node_child.rest_head_local 336 | 337 | bvh_node.rest_tail_world = rest_tail_world * \ 338 | (1.0 / len(bvh_node.children)) 339 | bvh_node.rest_tail_local = rest_tail_local * \ 340 | (1.0 / len(bvh_node.children)) 341 | 342 | # Make sure tail isn't the same location as the head. 343 | if (bvh_node.rest_tail_local - bvh_node.rest_head_local).length <= 0.001 * global_scale: 344 | print("\tzero length node found:", bvh_node.name) 345 | bvh_node.rest_tail_local.y = bvh_node.rest_tail_local.y + global_scale / 10 346 | bvh_node.rest_tail_world.y = bvh_node.rest_tail_world.y + global_scale / 10 347 | 348 | return bvh_nodes, bvh_frame_time, bvh_frame_count 349 | 350 | 351 | def bvh_node_dict2objects( 352 | context, 353 | bvh_name, 354 | bvh_nodes, 355 | rotate_mode='NATIVE', 356 | frame_start=1, 357 | IMPORT_LOOP=False, 358 | global_matrix=None, 359 | translation_mode="", 360 | apply_axis_conversion=False, 361 | skip_frames = 0, 362 | add_rest_pose_as_first_frame = False, 363 | ): 364 | if apply_axis_conversion and global_matrix: 365 | global_matrix_3x3 = global_matrix.to_3x3() 366 | global_matrix_inv = global_matrix.inverted() 367 | 368 | if frame_start < 1: 369 | frame_start = 1 370 | 371 | if skip_frames < 0: 372 | skip_frames = 0 373 | 374 | if skip_frames >= len(bvh_nodes)-1: 375 | skip_frames = 0 376 | 377 | if not add_rest_pose_as_first_frame: 378 | skip_frames += 1 # skip the first data in bvh_node.anim_data = [(0, 0, 0, 0, 0, 0)] added by read_bvh() 379 | 380 | scene = context.scene 381 | for obj in scene.objects: 382 | obj.select_set(False) 383 | 384 | objects = [] 385 | 386 | def add_ob(name): 387 | obj = bpy.data.objects.new(name, None) 388 | context.collection.objects.link(obj) 389 | objects.append(obj) 390 | obj.select_set(True) 391 | 392 | # nicer drawing. 393 | obj.empty_display_type = 'CUBE' 394 | obj.empty_display_size = 0.1 395 | 396 | return obj 397 | 398 | bvh_nodes_list = sorted_nodes(bvh_nodes) 399 | root_bone_name = bvh_nodes_list[0].name 400 | 401 | # Add objects 402 | for name, bvh_node in bvh_nodes.items(): 403 | bvh_node.temp = add_ob(name) 404 | bvh_node.temp.rotation_mode = bvh_node.rot_order_str[::-1] 405 | 406 | # Parent the objects 407 | for bvh_node in bvh_nodes.values(): 408 | for bvh_node_child in bvh_node.children: 409 | bvh_node_child.temp.parent = bvh_node.temp 410 | 411 | if apply_axis_conversion and global_matrix: 412 | # Offset 413 | for bvh_node in bvh_nodes.values(): 414 | # Make relative to parents offset 415 | bvh_node.temp.location = global_matrix_3x3 @ bvh_node.rest_head_local 416 | 417 | # Add tail objects 418 | for name, bvh_node in bvh_nodes.items(): 419 | if not bvh_node.children: 420 | ob_end = add_ob(name + '_end') 421 | ob_end.parent = bvh_node.temp 422 | ob_end.location = global_matrix_3x3 @ (bvh_node.rest_tail_world - 423 | bvh_node.rest_head_world) 424 | 425 | for name, bvh_node in bvh_nodes.items(): 426 | obj = bvh_node.temp 427 | 428 | skip_loc = (translation_mode == 'TRANSLATION_FOR_NONE_BONE') or \ 429 | (translation_mode == 'TRANSLATION_FOR_ROOT_BONE' and bvh_node.name != root_bone_name) 430 | 431 | prev_euler = Euler((0.0, 0.0, 0.0)) 432 | 433 | for frame_current in range(len(bvh_node.anim_data) - skip_frames): 434 | # print('---> Frame: #', frame_current) 435 | lx, ly, lz, rx, ry, rz = bvh_node.anim_data[frame_current + skip_frames] 436 | 437 | if bvh_node.has_loc and not skip_loc: 438 | # if bvh_node.has_loc: 439 | obj.delta_location = global_matrix_3x3 @ (Vector((lx, ly, lz)) - bvh_node.rest_head_local) 440 | obj.keyframe_insert("delta_location", index=-1, frame=frame_start + frame_current) 441 | 442 | if bvh_node.has_rot: 443 | euler_orig = Euler((rx, ry, rz), obj.rotation_mode) 444 | rot_matrix = euler_orig.to_matrix().to_4x4() 445 | rot_matrix = global_matrix @ rot_matrix @ global_matrix_inv 446 | euler_new = rot_matrix.to_euler(obj.rotation_mode, prev_euler) 447 | prev_euler = euler_new 448 | 449 | obj.delta_rotation_euler = (euler_new[0], euler_new[1], euler_new[2]) 450 | obj.keyframe_insert("delta_rotation_euler", index=-1, frame=frame_start + frame_current) 451 | else: 452 | # Offset 453 | for bvh_node in bvh_nodes.values(): 454 | # Make relative to parents offset 455 | bvh_node.temp.location = bvh_node.rest_head_local 456 | 457 | # Add tail objects 458 | for name, bvh_node in bvh_nodes.items(): 459 | if not bvh_node.children: 460 | ob_end = add_ob(name + '_end') 461 | ob_end.parent = bvh_node.temp 462 | ob_end.location = bvh_node.rest_tail_world - bvh_node.rest_head_world 463 | 464 | for name, bvh_node in bvh_nodes.items(): 465 | obj = bvh_node.temp 466 | skip_loc = (translation_mode == 'TRANSLATION_FOR_NONE_BONE') or \ 467 | (translation_mode == 'TRANSLATION_FOR_ROOT_BONE' and bvh_node.name != root_bone_name) 468 | 469 | for frame_current in range(len(bvh_node.anim_data)-skip_frames): 470 | 471 | lx, ly, lz, rx, ry, rz = bvh_node.anim_data[frame_current + skip_frames] 472 | 473 | if bvh_node.has_loc and not skip_loc: 474 | # if bvh_node.has_loc: 475 | # print('--> name: ', name) 476 | # print('--> bvh_node.rest_head_world: ', bvh_node.rest_head_world) 477 | # print('--> bvh_node.rest_head_local: ', bvh_node.rest_head_local) 478 | obj.delta_location = Vector((lx, ly, lz)) - bvh_node.rest_head_local 479 | # print('--> obj.delta_location: ', obj.delta_location) 480 | obj.keyframe_insert("delta_location", index=-1, frame=frame_start + frame_current) 481 | 482 | if bvh_node.has_rot: 483 | obj.delta_rotation_euler = rx, ry, rz 484 | obj.keyframe_insert("delta_rotation_euler", index=-1, frame=frame_start + frame_current) 485 | 486 | return objects 487 | 488 | 489 | def bvh_node_dict2armature( 490 | context, 491 | bvh_name, 492 | bvh_nodes, 493 | bvh_frame_time, 494 | rotate_mode='XYZ', 495 | frame_start=1, 496 | IMPORT_LOOP=False, 497 | global_matrix=None, 498 | use_fps_scale=False, 499 | translation_mode='TRANSLATION_FOR_ALL_BONES', 500 | apply_axis_conversion=False, 501 | skip_frames = 0, 502 | add_rest_pose_as_first_frame = False, 503 | ): 504 | if frame_start < 1: 505 | frame_start = 1 506 | 507 | if skip_frames < 0: 508 | skip_frames = 0 509 | 510 | if skip_frames >= len(bvh_nodes)-1: 511 | skip_frames = 0 512 | 513 | if not add_rest_pose_as_first_frame: 514 | skip_frames += 1 # skip the first data in bvh_node.anim_data = [(0, 0, 0, 0, 0, 0)] added by read_bvh() 515 | 516 | # Add the NEW_ARMATURE, 517 | scene = context.scene 518 | for obj in scene.objects: 519 | obj.select_set(False) 520 | 521 | arm_data = bpy.data.armatures.new(bvh_name) 522 | arm_ob = bpy.data.objects.new(bvh_name, arm_data) 523 | 524 | context.collection.objects.link(arm_ob) 525 | 526 | arm_ob.select_set(True) 527 | context.view_layer.objects.active = arm_ob 528 | 529 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 530 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 531 | 532 | bvh_nodes_list = sorted_nodes(bvh_nodes) 533 | 534 | # Get the average bone length for zero length bones, we may not use this. 535 | average_bone_length = 0.0 536 | nonzero_count = 0 537 | for bvh_node in bvh_nodes_list: 538 | l = (bvh_node.rest_head_local - bvh_node.rest_tail_local).length 539 | if l: 540 | average_bone_length += l 541 | nonzero_count += 1 542 | 543 | # Very rare cases all bones could be zero length??? 544 | if not average_bone_length: 545 | average_bone_length = 0.1 546 | else: 547 | # Normal operation 548 | average_bone_length = average_bone_length / nonzero_count 549 | 550 | # XXX, annoying, remove bone. 551 | while arm_data.edit_bones: 552 | arm_ob.edit_bones.remove(arm_data.edit_bones[-1]) 553 | 554 | ZERO_AREA_BONES = [] 555 | for bvh_node in bvh_nodes_list: 556 | 557 | # New editbone 558 | bone = bvh_node.temp = arm_data.edit_bones.new(bvh_node.name) 559 | 560 | bone.head = bvh_node.rest_head_world 561 | bone.tail = bvh_node.rest_tail_world 562 | 563 | # Zero Length Bones! (an exceptional case) 564 | if (bone.head - bone.tail).length < 0.001: 565 | print("\tzero length bone found:", bone.name) 566 | if bvh_node.parent: 567 | ofs = bvh_node.parent.rest_head_local - bvh_node.parent.rest_tail_local 568 | if ofs.length: # is our parent zero length also?? unlikely 569 | bone.tail = bone.tail - ofs 570 | else: 571 | bone.tail.y = bone.tail.y + average_bone_length 572 | else: 573 | bone.tail.y = bone.tail.y + average_bone_length 574 | 575 | ZERO_AREA_BONES.append(bone.name) 576 | 577 | for bvh_node in bvh_nodes_list: 578 | if bvh_node.parent: 579 | # bvh_node.temp is the Editbone 580 | 581 | # Set the bone parent 582 | bvh_node.temp.parent = bvh_node.parent.temp 583 | 584 | # Set the connection state 585 | if( 586 | (not bvh_node.has_loc) and 587 | (bvh_node.parent.temp.name not in ZERO_AREA_BONES) and 588 | (bvh_node.parent.rest_tail_local == bvh_node.rest_head_local) 589 | ): 590 | bvh_node.temp.use_connect = True 591 | 592 | # Replace the editbone with the editbone name, 593 | # to avoid memory errors accessing the editbone outside editmode 594 | for bvh_node in bvh_nodes_list: 595 | bvh_node.temp = bvh_node.temp.name 596 | 597 | # Now Apply the animation to the armature 598 | 599 | # Get armature animation data 600 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 601 | 602 | pose = arm_ob.pose 603 | pose_bones = pose.bones 604 | 605 | root_bone_name = pose_bones[0].name 606 | 607 | if rotate_mode == 'NATIVE': 608 | for bvh_node in bvh_nodes_list: 609 | # may not be the same name as the bvh_node, could have been shortened. 610 | bone_name = bvh_node.temp 611 | pose_bone = pose_bones[bone_name] 612 | pose_bone.rotation_mode = bvh_node.rot_order_str 613 | 614 | elif rotate_mode != 'QUATERNION': 615 | for pose_bone in pose_bones: 616 | pose_bone.rotation_mode = rotate_mode 617 | else: 618 | # Quats default 619 | pass 620 | 621 | context.view_layer.update() 622 | 623 | arm_ob.animation_data_create() 624 | action = bpy.data.actions.new(name=bvh_name) 625 | arm_ob.animation_data.action = action 626 | 627 | # Replace the bvh_node.temp (currently an editbone) 628 | # With a tuple (pose_bone, armature_bone, bone_rest_matrix, bone_rest_matrix_inv) 629 | num_frame = 0 630 | for bvh_node in bvh_nodes_list: 631 | # may not be the same name as the bvh_node, could have been shortened. 632 | bone_name = bvh_node.temp 633 | pose_bone = pose_bones[bone_name] 634 | rest_bone = arm_data.bones[bone_name] 635 | bone_rest_matrix = rest_bone.matrix_local.to_3x3() 636 | 637 | bone_rest_matrix_inv = Matrix(bone_rest_matrix) 638 | bone_rest_matrix_inv.invert() 639 | 640 | bone_rest_matrix_inv.resize_4x4() 641 | bone_rest_matrix.resize_4x4() 642 | bvh_node.temp = (pose_bone, rest_bone, bone_rest_matrix, 643 | bone_rest_matrix_inv) 644 | 645 | if 0 == num_frame: 646 | num_frame = len(bvh_node.anim_data) 647 | 648 | # Choose to skip some frames at the beginning. Frame 0 is the rest pose 649 | # used internally by this importer. Frame 1, by convention, is also often 650 | # the rest pose of the skeleton exported by the motion capture system. 651 | # skip_frames = 1 652 | 653 | if num_frame > skip_frames: 654 | num_frame = num_frame - skip_frames 655 | 656 | # Create a shared time axis for all animation curves. 657 | time = [float(frame_start)] * num_frame 658 | if use_fps_scale: 659 | dt = scene.render.fps * bvh_frame_time 660 | for frame_i in range(1, num_frame): 661 | time[frame_i] += float(frame_i) * dt 662 | else: 663 | for frame_i in range(1, num_frame): 664 | time[frame_i] += float(frame_i) 665 | 666 | # print("bvh_frame_time = %f, dt = %f, num_frame = %d" 667 | # % (bvh_frame_time, dt, num_frame])) 668 | 669 | # print('\n'*3) 670 | # print('*'*64) 671 | for i, bvh_node in enumerate(bvh_nodes_list): 672 | pose_bone, rest_bone, bone_rest_matrix, bone_rest_matrix_inv = bvh_node.temp 673 | 674 | skip_loc = (translation_mode == 'TRANSLATION_FOR_NONE_BONE') or \ 675 | (translation_mode == 'TRANSLATION_FOR_ROOT_BONE' and pose_bone.name != root_bone_name) 676 | 677 | if bvh_node.has_loc and not skip_loc: 678 | # if bvh_node.has_loc: 679 | # Not sure if there is a way to query this or access it in the 680 | # PoseBone structure. 681 | data_path = 'pose.bones["%s"].location' % pose_bone.name 682 | 683 | location = [(0.0, 0.0, 0.0)] * num_frame 684 | for frame_i in range(num_frame): 685 | bvh_loc = bvh_node.anim_data[frame_i + skip_frames][:3] 686 | 687 | # print('---> bvh_loc before: ', bvh_loc) 688 | 689 | bone_translate_matrix = Matrix.Translation( 690 | Vector(bvh_loc) - bvh_node.rest_head_local) 691 | # print('---> bone_translate_matrix: ', bone_translate_matrix) 692 | # print('---> bone_rest_matrix_inv: ', bone_rest_matrix_inv) 693 | location[frame_i] = (bone_rest_matrix_inv @ 694 | bone_translate_matrix).to_translation() 695 | # print('---> location[frame_i]: ', location[frame_i]) 696 | 697 | # For each location x, y, z. 698 | for axis_i in range(3): 699 | curve = action.fcurves.new(data_path=data_path, index=axis_i) 700 | keyframe_points = curve.keyframe_points 701 | keyframe_points.add(num_frame) 702 | 703 | for frame_i in range(num_frame): 704 | keyframe_points[frame_i].co = ( 705 | time[frame_i], 706 | location[frame_i][axis_i], 707 | ) 708 | 709 | if bvh_node.has_rot: 710 | data_path = None 711 | rotate = None 712 | 713 | if 'QUATERNION' == rotate_mode: 714 | rotate = [(1.0, 0.0, 0.0, 0.0)] * num_frame 715 | data_path = ('pose.bones["%s"].rotation_quaternion' 716 | % pose_bone.name) 717 | else: 718 | rotate = [(0.0, 0.0, 0.0)] * num_frame 719 | data_path = ('pose.bones["%s"].rotation_euler' % 720 | pose_bone.name) 721 | 722 | prev_euler = Euler((0.0, 0.0, 0.0)) 723 | for frame_i in range(num_frame): 724 | bvh_rot = bvh_node.anim_data[frame_i + skip_frames][3:] 725 | 726 | # print('='*32) 727 | # print('---> bvh_node.rot_order_str: ', bvh_node.rot_order_str) 728 | # print('---> bvh_node.rot_order_str[::-1]:', 729 | # bvh_node.rot_order_str[::-1]) 730 | 731 | # apply rotation order and convert to XYZ 732 | # note that the rot_order_str is reversed. 733 | euler = Euler(bvh_rot, bvh_node.rot_order_str[::-1]) 734 | # print('---> euler: ', euler) 735 | # print("---> %.2f, %.2f, %.2f" % tuple(math.degrees(a) for a in euler)) 736 | 737 | bone_rotation_matrix = euler.to_matrix().to_4x4() 738 | bone_rotation_matrix = ( 739 | bone_rest_matrix_inv @ 740 | bone_rotation_matrix @ 741 | bone_rest_matrix 742 | ) 743 | 744 | # print('---> len(rotate[frame_i]): ', len(rotate[frame_i])) 745 | 746 | if len(rotate[frame_i]) == 4: 747 | rotate[frame_i] = bone_rotation_matrix.to_quaternion() 748 | else: 749 | rotate[frame_i] = bone_rotation_matrix.to_euler( 750 | pose_bone.rotation_mode, prev_euler) 751 | prev_euler = rotate[frame_i] 752 | # print('---> converted euler: ', rotate[frame_i]) 753 | # print("---> %.2f, %.2f, %.2f" % tuple(math.degrees(a) for a in rotate[frame_i])) 754 | 755 | # For each euler angle x, y, z (or quaternion w, x, y, z). 756 | for axis_i in range(len(rotate[0])): 757 | curve = action.fcurves.new(data_path=data_path, index=axis_i) 758 | keyframe_points = curve.keyframe_points 759 | keyframe_points.add(num_frame) 760 | 761 | for frame_i in range(num_frame): 762 | keyframe_points[frame_i].co = ( 763 | time[frame_i], 764 | rotate[frame_i][axis_i], 765 | ) 766 | 767 | for cu in action.fcurves: 768 | if IMPORT_LOOP: 769 | pass # 2.5 doenst have cyclic now? 770 | 771 | for bez in cu.keyframe_points: 772 | bez.interpolation = 'LINEAR' 773 | 774 | if apply_axis_conversion and global_matrix: 775 | # print('===> finally apply global_matrix: \n', global_matrix) 776 | # print('\n'*3) 777 | 778 | # finally apply matrix 779 | arm_ob.matrix_world = global_matrix 780 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 781 | 782 | return arm_ob 783 | 784 | 785 | def bvh_node_dict2existing_armature( 786 | context, 787 | armature_name, 788 | bvh_name, 789 | bvh_nodes, 790 | bvh_frame_time, 791 | rotate_mode='XYZ', 792 | frame_start=1, 793 | IMPORT_LOOP=False, 794 | global_matrix=None, 795 | use_fps_scale=False, 796 | translation_mode='TRANSLATION_FOR_ALL_BONES', 797 | apply_axis_conversion=False, 798 | skip_frames = 0, 799 | add_rest_pose_as_first_frame = False, 800 | ): 801 | # global_matrix = None 802 | 803 | if apply_axis_conversion and global_matrix: 804 | global_matrix_3x3 = global_matrix.to_3x3() 805 | global_matrix_inv = global_matrix.inverted() 806 | 807 | if frame_start < 1: 808 | frame_start = 1 809 | 810 | if skip_frames < 0: 811 | skip_frames = 0 812 | 813 | if skip_frames >= len(bvh_nodes)-1: 814 | skip_frames = 0 815 | 816 | if not add_rest_pose_as_first_frame: 817 | skip_frames += 1 # skip the first data in bvh_node.anim_data = [(0, 0, 0, 0, 0, 0)] added by read_bvh() 818 | 819 | # Add the NEW_ARMATURE, 820 | scene = context.scene 821 | for obj in scene.objects: 822 | obj.select_set(False) 823 | 824 | # arm_data = bpy.data.armatures.new(bvh_name) 825 | # arm_ob = bpy.data.objects.new(bvh_name, arm_data) 826 | arm_ob = bpy.data.objects[armature_name] 827 | arm_data = arm_ob.data 828 | 829 | # context.collection.objects.link(arm_ob) 830 | 831 | arm_ob.select_set(True) 832 | context.view_layer.objects.active = arm_ob 833 | 834 | # bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 835 | # bpy.ops.object.mode_set(mode='EDIT', toggle=False) 836 | 837 | bvh_nodes_list = sorted_nodes(bvh_nodes) 838 | 839 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 840 | 841 | pose = arm_ob.pose 842 | pose_bones = pose.bones 843 | 844 | root_bone_name = pose_bones[0].name 845 | 846 | # print('---> rotate_mode: ', rotate_mode) 847 | 848 | if rotate_mode == 'NATIVE': 849 | # for bvh_node in bvh_nodes_list: 850 | for bone_name, bvh_node in bvh_nodes.items(): 851 | # bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened. 852 | pose_bone = pose_bones[bone_name] 853 | pose_bone.rotation_mode = bvh_node.rot_order_str 854 | 855 | elif rotate_mode != 'QUATERNION': 856 | for pose_bone in pose_bones: 857 | pose_bone.rotation_mode = rotate_mode 858 | else: 859 | # Quats default 860 | pass 861 | 862 | context.view_layer.update() 863 | 864 | arm_ob.animation_data_create() 865 | action = bpy.data.actions.new(name=bvh_name) 866 | arm_ob.animation_data.action = action 867 | 868 | # Replace the bvh_node.temp (currently an editbone) 869 | # With a tuple (pose_bone, armature_bone, bone_rest_matrix, bone_rest_matrix_inv) 870 | num_frame = 0 871 | # for bvh_node in bvh_nodes_list: 872 | for bone_name, bvh_node in bvh_nodes.items(): 873 | # bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened. 874 | pose_bone = pose_bones[bone_name] 875 | rest_bone = arm_data.bones[bone_name] 876 | bone_rest_matrix = rest_bone.matrix_local.to_3x3() 877 | 878 | bone_rest_matrix_inv = Matrix(bone_rest_matrix) 879 | bone_rest_matrix_inv.invert() 880 | 881 | bone_rest_matrix_inv.resize_4x4() 882 | bone_rest_matrix.resize_4x4() 883 | bvh_node.temp = (pose_bone, rest_bone, 884 | bone_rest_matrix, bone_rest_matrix_inv) 885 | 886 | if 0 == num_frame: 887 | num_frame = len(bvh_node.anim_data) 888 | 889 | # Choose to skip some frames at the beginning. Frame 0 is the rest pose 890 | # used internally by this importer. Frame 1, by convention, is also often 891 | # the rest pose of the skeleton exported by the motion capture system. 892 | # skip_frames = 1 893 | if skip_frames < 0: 894 | skip_frames = 0 # don't skip any frames, keep the same frames as in the .bvh file 895 | if num_frame > skip_frames: 896 | num_frame = num_frame - skip_frames 897 | 898 | # Create a shared time axis for all animation curves. 899 | time = [float(frame_start)] * num_frame 900 | if use_fps_scale: 901 | dt = scene.render.fps * bvh_frame_time 902 | for frame_i in range(1, num_frame): 903 | time[frame_i] += float(frame_i) * dt 904 | else: 905 | for frame_i in range(1, num_frame): 906 | time[frame_i] += float(frame_i) 907 | 908 | # print("bvh_frame_time = %f, dt = %f, num_frame = %d" 909 | # % (bvh_frame_time, dt, num_frame])) 910 | 911 | print('\n'*3) 912 | print('*'*64) 913 | for i, bvh_node in enumerate(bvh_nodes_list): 914 | pose_bone, rest_bone, bone_rest_matrix, bone_rest_matrix_inv = bvh_node.temp 915 | 916 | skip_loc = (translation_mode == 'TRANSLATION_FOR_NONE_BONE') or \ 917 | (translation_mode == 'TRANSLATION_FOR_ROOT_BONE' and pose_bone.name != root_bone_name) 918 | 919 | if bvh_node.has_loc and not skip_loc: 920 | # Not sure if there is a way to query this or access it in the 921 | # PoseBone structure. 922 | data_path = 'pose.bones["%s"].location' % pose_bone.name 923 | # print('---> ', data_path) 924 | 925 | location = [(0.0, 0.0, 0.0)] * num_frame 926 | for frame_i in range(num_frame): 927 | bvh_loc = bvh_node.anim_data[frame_i + skip_frames][:3] 928 | # print('---> bvh_loc before: \n', bvh_loc) 929 | 930 | bone_translate_matrix = Matrix.Translation( 931 | Vector(bvh_loc) - bvh_node.rest_head_local) 932 | # print('---> bone_translate_matrix before: \n', bone_translate_matrix) 933 | 934 | if apply_axis_conversion and global_matrix: 935 | # convert translation data into blender's coordinate systems (Z-up/Y-forward/X-right) 936 | bone_translate_matrix = global_matrix @ bone_translate_matrix 937 | # print('---> bone_translate_matrix after: \n', bone_translate_matrix) 938 | 939 | # print('---> bone_rest_matrix_inv: \n', bone_rest_matrix_inv) 940 | location[frame_i] = (bone_rest_matrix_inv @ 941 | bone_translate_matrix).to_translation() 942 | # print('---> location[frame_i]: \n', location[frame_i]) 943 | 944 | # For each location x, y, z. 945 | for axis_i in range(3): 946 | curve = action.fcurves.new(data_path=data_path, index=axis_i) 947 | keyframe_points = curve.keyframe_points 948 | keyframe_points.add(num_frame) 949 | 950 | for frame_i in range(num_frame): 951 | keyframe_points[frame_i].co = ( 952 | time[frame_i], 953 | location[frame_i][axis_i], 954 | ) 955 | 956 | if bvh_node.has_rot: 957 | data_path = None 958 | rotate = None 959 | 960 | if 'QUATERNION' == rotate_mode: 961 | rotate = [(1.0, 0.0, 0.0, 0.0)] * num_frame 962 | data_path = ('pose.bones["%s"].rotation_quaternion' 963 | % pose_bone.name) 964 | else: 965 | rotate = [(0.0, 0.0, 0.0)] * num_frame 966 | data_path = ('pose.bones["%s"].rotation_euler' % 967 | pose_bone.name) 968 | 969 | prev_euler = Euler((0.0, 0.0, 0.0)) 970 | 971 | if apply_axis_conversion and global_matrix: 972 | # backup 973 | bone_rest_matrix_bk = bone_rest_matrix.copy() 974 | bone_rest_matrix_inv_bk = bone_rest_matrix_inv.copy() 975 | 976 | # [comment-fusion] 977 | # compensate for the global_matrix to convert rotation data into blender's coordinate systems (Z-up/Y-forward/X-right) 978 | bone_rest_matrix_inv = bone_rest_matrix_inv @ global_matrix 979 | bone_rest_matrix = global_matrix_inv @ bone_rest_matrix 980 | 981 | for frame_i in range(num_frame): 982 | bvh_rot = bvh_node.anim_data[frame_i + skip_frames][3:] 983 | 984 | # apply rotation order and convert to XYZ 985 | # note that the rot_order_str is reversed. 986 | # print('='*32) 987 | # print('---> bvh_node.rot_order_str: ', bvh_node.rot_order_str) 988 | # print('---> bvh_node.rot_order_str[::-1]:', 989 | # bvh_node.rot_order_str[::-1]) 990 | 991 | euler = Euler(bvh_rot, bvh_node.rot_order_str[::-1]) 992 | 993 | # print('---> euler: ', euler) 994 | # print("---> %.2f, %.2f, %.2f" % tuple(math.degrees(a) for a in euler)) 995 | 996 | bone_rotation_matrix = euler.to_matrix().to_4x4() 997 | 998 | # convert rotation data into blender's coordinate systems (Z-up/Y-forward/X-right), 999 | # fused into bone_rest_matrix and bone_rest_matrix_inv, see the above: [comment-fusion] 1000 | # bone_rotation_matrix = global_matrix @ bone_rotation_matrix @ global_matrix_inv 1001 | 1002 | bone_rotation_matrix = ( 1003 | bone_rest_matrix_inv @ 1004 | bone_rotation_matrix @ 1005 | bone_rest_matrix 1006 | ) 1007 | 1008 | # print('---> len(rotate[frame_i]): ', len(rotate[frame_i])) 1009 | 1010 | if len(rotate[frame_i]) == 4: 1011 | rotate[frame_i] = bone_rotation_matrix.to_quaternion() 1012 | else: 1013 | rotate[frame_i] = bone_rotation_matrix.to_euler( 1014 | pose_bone.rotation_mode, prev_euler) 1015 | prev_euler = rotate[frame_i] 1016 | 1017 | # print('---> converted euler: ', rotate[frame_i]) 1018 | # print("%.2f, %.2f, %.2f" % tuple(math.degrees(a) for a in rotate[frame_i])) 1019 | 1020 | 1021 | # For each euler angle x, y, z (or quaternion w, x, y, z). 1022 | for axis_i in range(len(rotate[0])): 1023 | curve = action.fcurves.new(data_path=data_path, index=axis_i) 1024 | keyframe_points = curve.keyframe_points 1025 | keyframe_points.add(num_frame) 1026 | 1027 | for frame_i in range(num_frame): 1028 | keyframe_points[frame_i].co = ( 1029 | time[frame_i], 1030 | rotate[frame_i][axis_i], 1031 | ) 1032 | 1033 | if apply_axis_conversion and global_matrix: 1034 | bone_rest_matrix = bone_rest_matrix_bk 1035 | bone_rest_matrix_inv = bone_rest_matrix_inv_bk 1036 | 1037 | for cu in action.fcurves: 1038 | if IMPORT_LOOP: 1039 | pass # 2.5 doenst have cyclic now? 1040 | 1041 | for bez in cu.keyframe_points: 1042 | bez.interpolation = 'LINEAR' 1043 | 1044 | # finally apply matrix 1045 | # arm_ob.matrix_world = global_matrix 1046 | # print('===> global_matrix: ', global_matrix) 1047 | # bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 1048 | 1049 | return arm_ob 1050 | 1051 | 1052 | def load( 1053 | context, 1054 | filepath, 1055 | *, 1056 | target='NEW_ARMATURE', 1057 | rotate_mode='NATIVE', 1058 | global_scale=1.0, 1059 | use_cyclic=False, 1060 | frame_start=1, 1061 | global_matrix=None, 1062 | use_fps_scale=False, 1063 | update_scene_fps=False, 1064 | update_scene_duration=False, 1065 | report=print, 1066 | translation_mode='TRANSLATION_FOR_ALL_BONES', 1067 | apply_axis_conversion=False, 1068 | skip_frames=0, 1069 | add_rest_pose_as_first_frame = False, 1070 | ): 1071 | import time 1072 | t1 = time.time() 1073 | print("\tparsing bvh %r..." % filepath, end="") 1074 | 1075 | bvh_nodes, bvh_frame_time, bvh_frame_count = read_bvh( 1076 | context, filepath, 1077 | rotate_mode=rotate_mode, 1078 | global_scale=global_scale, 1079 | ) 1080 | 1081 | print("%.4f" % (time.time() - t1)) 1082 | 1083 | scene = context.scene 1084 | frame_orig = scene.frame_current 1085 | 1086 | # Broken BVH handling: guess frame rate when it is not contained in the file. 1087 | if bvh_frame_time is None: 1088 | report( 1089 | {'WARNING'}, 1090 | "The BVH file does not contain frame duration in its MOTION " 1091 | "section, assuming the BVH and Blender scene have the same " 1092 | "frame rate" 1093 | ) 1094 | bvh_frame_time = scene.render.fps_base / scene.render.fps 1095 | # No need to scale the frame rate, as they're equal now anyway. 1096 | use_fps_scale = False 1097 | 1098 | if update_scene_fps: 1099 | _update_scene_fps(context, report, bvh_frame_time) 1100 | 1101 | # Now that we have a 1-to-1 mapping of Blender frames and BVH frames, there is no need 1102 | # to scale the FPS any more. It's even better not to, to prevent roundoff errors. 1103 | use_fps_scale = False 1104 | 1105 | if update_scene_duration: 1106 | _update_scene_duration( 1107 | context, report, bvh_frame_count, bvh_frame_time, frame_start, use_fps_scale) 1108 | 1109 | t1 = time.time() 1110 | print("\timporting to blender...", end="") 1111 | 1112 | bvh_name = bpy.path.display_name_from_filepath(filepath) 1113 | 1114 | if target == 'NEW_ARMATURE': 1115 | bvh_node_dict2armature( 1116 | context, bvh_name, bvh_nodes, bvh_frame_time, 1117 | rotate_mode=rotate_mode, 1118 | frame_start=frame_start, 1119 | IMPORT_LOOP=use_cyclic, 1120 | global_matrix=global_matrix, 1121 | use_fps_scale=use_fps_scale, 1122 | translation_mode=translation_mode, 1123 | apply_axis_conversion=apply_axis_conversion, 1124 | skip_frames=skip_frames, 1125 | add_rest_pose_as_first_frame = add_rest_pose_as_first_frame, 1126 | ) 1127 | elif target == 'NEW_PARENTED_OBJECTS': 1128 | bvh_node_dict2objects( 1129 | context, bvh_name, bvh_nodes, 1130 | rotate_mode=rotate_mode, 1131 | frame_start=frame_start, 1132 | IMPORT_LOOP=use_cyclic, 1133 | global_matrix=global_matrix, 1134 | translation_mode=translation_mode, 1135 | apply_axis_conversion=apply_axis_conversion, 1136 | skip_frames=skip_frames, 1137 | add_rest_pose_as_first_frame = add_rest_pose_as_first_frame, 1138 | ) 1139 | elif target in bpy.data.objects.keys(): 1140 | bvh_node_dict2existing_armature( 1141 | context, target, bvh_name, bvh_nodes, bvh_frame_time, 1142 | rotate_mode=rotate_mode, 1143 | frame_start=frame_start, 1144 | IMPORT_LOOP=use_cyclic, 1145 | global_matrix=global_matrix, 1146 | use_fps_scale=use_fps_scale, 1147 | translation_mode=translation_mode, 1148 | apply_axis_conversion=apply_axis_conversion, 1149 | skip_frames=skip_frames, 1150 | add_rest_pose_as_first_frame = add_rest_pose_as_first_frame, 1151 | ) 1152 | else: 1153 | report({'ERROR'}, "Invalid target %r (must be 'NEW_ARMATURE' or name for existing ARMATURE Objects)" % target) 1154 | return {'CANCELLED'} 1155 | 1156 | print('Done in %.4f\n' % (time.time() - t1)) 1157 | 1158 | context.scene.frame_set(frame_orig) 1159 | 1160 | return {'FINISHED'} 1161 | 1162 | 1163 | def _update_scene_fps(context, report, bvh_frame_time): 1164 | """Update the scene's FPS settings from the BVH, but only if the BVH contains enough info.""" 1165 | 1166 | # Broken BVH handling: prevent division by zero. 1167 | if bvh_frame_time == 0.0: 1168 | report( 1169 | {'WARNING'}, 1170 | "Unable to update scene frame rate, as the BVH file " 1171 | "contains a zero frame duration in its MOTION section", 1172 | ) 1173 | return 1174 | 1175 | scene = context.scene 1176 | scene_fps = scene.render.fps / scene.render.fps_base 1177 | new_fps = 1.0 / bvh_frame_time 1178 | 1179 | if scene.render.fps != new_fps or scene.render.fps_base != 1.0: 1180 | print("\tupdating scene FPS (was %f) to BVH FPS (%f)" % 1181 | (scene_fps, new_fps)) 1182 | scene.render.fps = new_fps 1183 | scene.render.fps_base = 1.0 1184 | 1185 | 1186 | def _update_scene_duration( 1187 | context, report, bvh_frame_count, bvh_frame_time, frame_start, 1188 | use_fps_scale): 1189 | """Extend the scene's duration so that the BVH file fits in its entirety.""" 1190 | 1191 | if bvh_frame_count is None: 1192 | report( 1193 | {'WARNING'}, 1194 | "Unable to extend the scene duration, as the BVH file does not " 1195 | "contain the number of frames in its MOTION section", 1196 | ) 1197 | return 1198 | 1199 | # Not likely, but it can happen when a BVH is just used to store an armature. 1200 | if bvh_frame_count == 0: 1201 | return 1202 | 1203 | if use_fps_scale: 1204 | scene_fps = context.scene.render.fps / context.scene.render.fps_base 1205 | scaled_frame_count = int( 1206 | ceil(bvh_frame_count * bvh_frame_time * scene_fps)) 1207 | bvh_last_frame = frame_start + scaled_frame_count 1208 | else: 1209 | bvh_last_frame = frame_start + bvh_frame_count 1210 | 1211 | # Only extend the scene, never shorten it. 1212 | if context.scene.frame_end < bvh_last_frame: 1213 | context.scene.frame_end = bvh_last_frame 1214 | --------------------------------------------------------------------------------