Created
June 15, 2024 04:08
-
-
Save nathanielanozie/7349c22a815b957f1522d5267e39c28e to your computer and use it in GitHub Desktop.
animation import export class in blender 2.79
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ArmatureAnimationIO.py | |
#tested in blender 2.79 | |
#please modify/use at your own risk | |
import bpy | |
###for animation io | |
import os | |
import re | |
import json | |
import logging | |
from collections import namedtuple | |
logger = logging.getLogger(__name__) | |
logging.basicConfig(level=logging.DEBUG) #without this info logs wouldnt show in console | |
### | |
""" | |
import imp | |
testTool = imp.load_source("tool","/Users/Nathaniel/Documents/src_blender/python/animationTools/ArmatureAnimationIO.py") #change to path to python file | |
animIO = testTool.ArmatureAnimationIO() | |
#writing animation | |
animIO.toJson("Armature","/Users/Nathaniel/Documents/src_blender/python/snippets/tmp/anim_test.json") | |
#importing animation | |
#animIO.fromJson("Armature","/Users/Nathaniel/Documents/src_blender/python/snippets/tmp/anim_test.json") | |
""" | |
class ArmatureAnimationIO(object): | |
"""handles armature animation import and export | |
example usage: | |
animIO = ArmatureAnimationIO() | |
#writing animation | |
animIO.toJson("Armature","/Users/Nathaniel/Documents/src_blender/python/snippets/tmp/anim_test.json") | |
#importing animation | |
#animIO.fromJson("Armature","/Users/Nathaniel/Documents/src_blender/python/snippets/tmp/anim_test.json") | |
""" | |
def __init__(self): | |
self._animatedKey = "animated" #what key of dictionary holds animation data | |
self._staticKey = "static" | |
def toJson(self, armature, filePath): | |
"""export animation to json | |
#Armature{ | |
# animated{ | |
# 5:{ "bone1":channel:array_index:{bone data}, | |
# "bone10":channel:array_index:{bone data} | |
# } | |
# 10:{ "bone3":channel:array_index:{bone data} | |
# } | |
# | |
# }, | |
# static{ | |
# 1:{"bone50":{bone data}, #all static can be on first frame | |
# "bone30":{bone data} | |
# } | |
# } | |
#} | |
todo: support export of a specific action | |
@param armature (str) armature name | |
@param filePath (str) output file path - directory must exist | |
""" | |
if not os.path.isdir(os.path.dirname(filePath)): | |
logger.warning("requires directory to exist for {}".format(filePath)) | |
return False | |
if not armature in bpy.data.objects: | |
logger.warning("requires armature name to exist for {}".format(armature)) | |
return False | |
result = {} | |
animatedKey = self._animatedKey#"animated" | |
staticKey = self._staticKey#"static" | |
result[armature] = {animatedKey:{}, staticKey:{}} | |
animatedBones = [] | |
allFcurves = bpy.data.objects[armature].animation_data.action.fcurves | |
decimals = 5 #round number decimals | |
for fcurve in allFcurves: | |
boneDataList = [] | |
keyframePoints = fcurve.keyframe_points | |
for keyframePoint in keyframePoints: | |
frame = keyframePoint.co[0] | |
value = keyframePoint.co[1] | |
value = round(value, decimals) | |
interpolation = keyframePoint.interpolation | |
handle_left = tuple(keyframePoint.handle_left) #need to convert Vector to tuple | |
handle_left = [round(x, decimals) for x in handle_left] | |
handle_left_type = keyframePoint.handle_left_type | |
handle_right = tuple(keyframePoint.handle_right) | |
handle_right = [round(x, decimals) for x in handle_right] | |
handle_right_type = keyframePoint.handle_right_type | |
array_index = fcurve.array_index | |
###it should handle bendybone attributes if they exist. the channel tells which channel to animate | |
### | |
logger.info("frame {frame} value {value} interpolation {interpolation} handle_left {handle_left} handle_right {handle_right} handle_left_type {handle_left_type} handle_right_type {handle_right_type}".format(frame=frame, | |
value=value, | |
interpolation=interpolation, | |
handle_left=handle_left, | |
handle_left_type=handle_left_type, | |
handle_right=handle_right, | |
handle_right_type=handle_right_type | |
)) | |
#get bone name for fcurve | |
matchGroups = re.match(r'pose.bones\["(.*)"\].(.*)', fcurve.data_path) | |
bone = '' | |
channel = '' | |
if matchGroups: | |
bone = matchGroups.groups(0)[0] | |
channel = matchGroups.groups(0)[1] | |
animatedBones.append(bone) #keep track of animated bones for handling static bones later | |
boneData = dict(value=value, | |
channel=channel, | |
array_index=array_index, #ex: 0 with channel location meats location in X axis | |
interpolation=interpolation, | |
handle_left=handle_left, | |
handle_left_type=handle_left_type, | |
handle_right=handle_right, | |
handle_right_type=handle_right_type) | |
#doing the saving of dictionary | |
#result[armature][animatedKey].setdefault(frame, {})[bone]=boneData #using setdefault so can create an empty dictionary if no key added yet | |
result[armature][animatedKey].setdefault(frame, {}).setdefault(bone, {}).setdefault(channel, {}).setdefault(array_index,{}) | |
result[armature][animatedKey][frame][bone][channel][array_index] = boneData | |
#end keyframe points loop | |
#end fcurve loop | |
animatedBones = list(set(animatedBones)) #remove duplicates | |
staticBones = [b.name for b in bpy.data.objects[armature].pose.bones if b.name not in animatedBones] | |
logger.info("static bones ") | |
logger.info(staticBones) | |
for staticBone in staticBones: | |
boneData = {} | |
location = bpy.data.objects[armature].pose.bones[staticBone].location | |
location = [round(x, decimals) for x in location] | |
scale = bpy.data.objects[armature].pose.bones[staticBone].scale | |
scale = [round(x, decimals) for x in scale] | |
rotationMode = bpy.data.objects[armature].pose.bones[staticBone].rotation_mode | |
if rotationMode != 'QUATERNION': | |
#save euler | |
rotation = bpy.data.objects[armature].pose.bones[staticBone].rotation_euler | |
rotation = tuple(rotation) | |
rotation = [round(x, decimals) for x in rotation] | |
else: | |
#save quaternion rotations | |
rotation = bpy.data.objects[armature].pose.bones[staticBone].rotation_quaternion | |
rotation = tuple(rotation) | |
rotation = [round(x, decimals) for x in rotation] | |
boneData = dict(location=location, | |
rotationMode=rotationMode, | |
rotation=rotation, | |
scale=scale) | |
#add additional data for bendy static bone | |
if self._isBendyBone(armature, staticBone): | |
bendyData = self._getStaticBendyBoneDict(armature, staticBone) | |
boneData.update(bendyData) #like joining to list but for dictionaries | |
#save the static bones | |
result[armature][staticKey].setdefault('1', {})[staticBone]=boneData | |
#print(result) | |
##write to json | |
with open(filePath, 'w') as outf: | |
json.dump(result, outf, indent=4) | |
## | |
return True | |
def fromJson(self, armature, filePath, importActionName="naImportAction"): | |
"""import animation from json. currently doesnt do any resetting of bones before importing animation | |
todo: when applying animation > ?set all channels of bone to defaults before importing animation. | |
todo: support import animation on selected bones only | |
""" | |
if (not os.path.isfile(filePath)) or not armature: | |
logger.warning("no file path or armature found to do import of animation. doing nothing") | |
return False | |
data = {} | |
with open(filePath, 'r') as infile: | |
data = json.load(infile) | |
armatureData = data.get(armature) | |
if not armatureData: | |
logger.warning("no armature data found. doing nothing") | |
return False | |
animationData = armatureData.get(self._animatedKey) | |
actionName = importActionName | |
if animationData: | |
#make an action if it doesnt exist | |
action = self._createAction(armature, actionName) | |
for frame, bonesDataDict in animationData.items(): | |
for boneName, boneData in bonesDataDict.items(): | |
for channel, channelDict in boneData.items(): | |
for arrayIndex, primaryInfo in channelDict.items(): | |
#add a fcurve if doesnt exist | |
#add a keypoint | |
self._createKeyFramePoint(armature, action, boneName=boneName, boneData=primaryInfo, frame=float(frame)) | |
#import static data | |
staticData = armatureData.get(self._staticKey).get("1") #first frame for static data | |
if not staticData: | |
#no static data were done | |
return True | |
for boneName, boneStaticData in staticData.items(): | |
self._setStaticPose(armature, boneName=boneName, boneData=boneStaticData) | |
return True | |
def _setStaticPose(self, armature, boneName=None, boneData={}): | |
"""sets pose for static bone. | |
ex: boneData | |
"Bone": { | |
"rotationMode": "QUATERNION", | |
"location": [ | |
0.0, | |
0.0, | |
0.0 | |
], | |
"rotation": [ | |
1.0, | |
0.0, | |
0.0, | |
0.0 | |
], | |
"scale": [ | |
1.0, | |
1.0, | |
1.0 | |
] | |
}, | |
""" | |
if (not armature) or (not boneName) or (not boneData): | |
logger.warning("missing inputs for creating static pose. doing nothing") | |
return False | |
skipItems = ["rotationMode"] #things in boneData that are not channels that will be set | |
for channel, value in boneData.items(): | |
if channel in skipItems: | |
continue | |
#handle rotations | |
if (channel == "rotation"): | |
if (boneData.get("rotationMode") == "QUATERNION"): | |
#set quaternion rotation | |
setattr(bpy.data.objects[armature].pose.bones[boneName], "rotation_quaternion", value) | |
else: | |
#set euler rotation | |
setattr(bpy.data.objects[armature].pose.bones[boneName], "rotation_euler", value) | |
#were done with this channel move to next | |
continue | |
#set the pose | |
setattr(bpy.data.objects[armature].pose.bones[boneName], channel, value) | |
def _createKeyFramePoint(self, armature, action, boneName=None, boneData={}, frame=None): | |
"""create a key frame point using given data. add to appropriate fcurve. add an fcurve if doesnt exist. add keyframe point. | |
todo also modify tangents appropriately | |
@param armature (str) name of armature | |
@param action (str) name of action | |
ex: boneData | |
{ | |
"location": { | |
"0": { | |
"array_index": 0, | |
"handle_right_type": "AUTO_CLAMPED", | |
"handle_left": [ | |
11.31506, | |
0.0 | |
], | |
"value": 0.0, | |
"handle_right": [ | |
20.68494, | |
0.0 | |
], | |
"interpolation": "BEZIER", | |
"channel": "location", | |
"handle_left_type": "AUTO_CLAMPED" | |
}, | |
"1": { | |
"array_index": 1, | |
"handle_right_type": "AUTO_CLAMPED", | |
"handle_left": [ | |
11.31506, | |
0.0 | |
], | |
"value": 0.0, | |
"handle_right": [ | |
20.68494, | |
0.0 | |
], | |
"interpolation": "BEZIER", | |
"channel": "location", | |
"handle_left_type": "AUTO_CLAMPED" | |
} | |
} | |
} | |
""" | |
if (not armature) or (not action) or (not boneData) or (not frame): | |
logger.warning("missing inputs for creating keyframe point. doing nothing") | |
return False | |
boneAttribute = boneData.get("channel") #todo switch out to namedtuple | |
attributeIndex = boneData.get("array_index") | |
#todo asserts | |
#todo make an fcurve if it doesnt exist. if it exists use existing fcurve | |
fcurveIndex = self._getFcurveIndex(armature, boneName=boneName, boneAttribute=boneAttribute, attributeIndex=attributeIndex) | |
if fcurveIndex is None: | |
logger.warning("couldnt determine an fcurve. doing nothing") | |
return False | |
#insert a keyframe point | |
value = boneData.get("value") | |
logger.info("number fcurve>>>") | |
logger.info(len(bpy.data.objects[armature].animation_data.action.fcurves)) | |
logger.info("fcrv index {0} frame {1} value {2}".format(fcurveIndex, frame, value)) | |
fcurve = bpy.data.objects[armature].animation_data.action.fcurves[fcurveIndex] | |
keyframePoint = fcurve.keyframe_points.insert(frame, value) | |
#set tangents and interpolation of keyframe | |
extraAttrs = ["handle_right_type", | |
"handle_left_type", | |
"handle_left", | |
"handle_right", | |
"interpolation"] | |
for extraAttr in extraAttrs: | |
val = boneData.get(extraAttr) | |
if val: | |
setattr(keyframePoint, extraAttr, val) | |
##### | |
def _createAction(self, armature, actionName): | |
""" | |
@return (str) name of created action | |
""" | |
if (not armature in bpy.data.objects) or (not actionName): | |
logger.warning("could not find inputs for creating action. doing nothing") | |
return '' | |
if actionName not in bpy.data.actions: | |
actionObj = bpy.data.actions.new(actionName) | |
bpy.data.objects[armature].animation_data.action = actionObj | |
return actionName | |
def _getFcurveIndex(self, armature, boneName='', boneAttribute='', attributeIndex=None): | |
"""todo: cleans this up > for creating an fcurve | |
@return (int) the index of the fcurve for this bone and attribute | |
""" | |
#todo check inputs | |
dataPath = 'pose.bones["{bone}"].{channel}'.format(bone=boneName, channel=boneAttribute) | |
#is fcurve already created | |
isCurveExist = False | |
allCreatedFcurves = bpy.data.objects[armature].animation_data.action.fcurves or [] | |
if not allCreatedFcurves: | |
fcurve = bpy.data.objects[armature].animation_data.action.fcurves.new(dataPath, attributeIndex) | |
return 0 #since no fcurves have been created use 0 index | |
foundIndex = None | |
for i in range(0,len(allCreatedFcurves)): | |
fcurve = allCreatedFcurves[i] | |
if (fcurve.data_path == dataPath) and (fcurve.array_index == attributeIndex): | |
#found existing curve | |
isCurveExist = True | |
foundIndex = i | |
break | |
if isCurveExist: | |
return foundIndex | |
logger.warning("testing >>>>") | |
logger.warning(attributeIndex) | |
logger.warning(dataPath) | |
fcurve = bpy.data.objects[armature].animation_data.action.fcurves.new(dataPath, attributeIndex) | |
return len(allCreatedFcurves)-1 #assuming newest fcurve is after last index. todo confirm this | |
def _getStaticBendyBoneDict(self, armature, bone): | |
"""returns empty dictionary if not a bendy bone | |
""" | |
isBendyBone = self._isBendyBone(armature, bone) | |
if not isBendyBone: | |
return {} | |
result = {} | |
channels=[ "bbone_curveinx", | |
"bbone_curveiny", | |
"bbone_curveoutx", | |
"bbone_curveouty", | |
"bbone_rollin", | |
"bbone_rollout", | |
"bbone_scalein", | |
"bbone_scaleout" | |
] | |
for channel in channels: | |
result[channel] = eval('bpy.data.objects["{armat}"].pose.bones["{bone}"].{chan}'.format(armat=armature, bone=bone, chan=channel)) | |
return result | |
def _isBendyBone(self, armature, bone): | |
#todo assert bone in armature | |
if bpy.data.objects[armature].data.bones[bone].bbone_segments > 1: | |
return True | |
return False | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment