def write(self) -> str: """ Writes collected Blender and XPlane data as a \\n seperated string of OBJ directives. """ if self.export_animation_only: return "" debug = getDebug() o = "" xplaneFile = self.xplaneBone.xplaneFile commands = xplaneFile.commands if debug: indent = self.xplaneBone.getIndent() o += f"{indent}# {self.type}: {self.name}\tweight: {self.weight}\n" o += commands.writeReseters(self) for attr in self.attributes: o += commands.writeAttribute(self.attributes[attr], self) # if the file is a cockpit file write all cockpit attributes if xplaneFile.options.export_type == EXPORT_TYPE_COCKPIT: for attr in self.cockpitAttributes: o += commands.writeAttribute(self.cockpitAttributes[attr], self) return o
def _writeStaticTranslation(self, bakeMatrix): debug = getDebug() indent = self.getIndent() o = '' bakeMatrix = bakeMatrix translation = bakeMatrix.to_translation() translation[0] = round(translation[0],5) translation[1] = round(translation[1],5) translation[2] = round(translation[2],5) # ignore noop translations if translation[0] == 0 and translation[1] == 0 and translation[2] == 0: return o if debug: o += indent + '# static translation\n' o += indent + 'ANIM_trans\t%s\t%s\t%s\t%s\t%s\t%s\n' % ( floatToStr(translation[0]), floatToStr(translation[2]), floatToStr(-translation[1]), floatToStr(translation[0]), floatToStr(translation[2]), floatToStr(-translation[1]) ) return o
def writeAnimationPrefix(self): debug = getDebug() indent = self.getIndent() o = '' if debug: o += indent + '# ' + self.getName() + '\n' ''' if self.blenderBone: poseBone = self.blenderObject.pose.bones[self.blenderBone.name] if poseBone != None: o += "# Armature\n" + str(self.blenderObject.matrix_world) + "\n" if self.blenderBone.parent: poseParent = self.blenderObject.pose.bones[self.blenderBone.parent.name] if poseParent: o += "# parent matrix local rest\n" + str(self.blenderBone.parent.matrix_local) + "\n" o += "# parent matrix local pose\n" + str(poseParent.matrix) + "\n" o += "# delta r2r\n" + str(self.blenderBone.parent.matrix_local.inverted_safe() * self.blenderBone.matrix_local) + "\n" o += "# delta p2p\n" + str(poseParent.matrix.inverted_safe() * poseBone.matrix) + "\n" o += "# matrix local rest\n" + str(self.blenderBone.matrix_local) + "\n" o += "# matrix local pose\n" + str(poseBone.matrix) + "\n" o += "# pose delta\n" + str(self.blenderBone.matrix_local.inverted_safe() * poseBone.matrix) + "\n" elif self.blenderObject != None: o += "# Data block\n" + str(self.blenderObject.matrix_world) + "\n" # Debug code - this dumps the pre/post/bake matrix for every single xplane bone into the file. p = self while p != None: o += indent + '# ' + p.getName() + '\n' o += str(p.getPreAnimationMatrix()) + '\n' o += str(p.getPostAnimationMatrix()) + '\n' o += str(p.getBakeMatrixForMyAnimations()) + '\n' p = None ''' isAnimated = self.isAnimated() hasAnimationAttributes = (self.xplaneObject != None and len(self.xplaneObject.animAttributes) > 0) if not isAnimated and not hasAnimationAttributes: return o # and postMatrix is not preMatrix if (isAnimated) or \ hasAnimationAttributes: o += indent + 'ANIM_begin\n' if isAnimated:# and postMatrix is not preMatrix: # write out static translations of bake bakeMatrix = self.getBakeMatrixForMyAnimations() o += self._writeStaticTranslation(bakeMatrix) o += self._writeStaticRotation(bakeMatrix) for dataref in sorted(list(self.animations.keys())): o += self._writeTranslationKeyframes(dataref) for dataref in sorted(list(self.animations.keys())): o += self._writeRotationKeyframes(dataref) o += self._writeAnimAttributes() return o
def useLogger(self): debug = getDebug() logLevels = ['error', 'warning'] if debug: logLevels.append('info') logLevels.append('success') logger.clear() logger.addTransport(XPlaneLogger.ConsoleTransport(), logLevels)
def useLogger(self): debug = getDebug() logLevels = ['error', 'warning'] if debug: logLevels.append('info') logLevels.append('success') logger.clear() logger.addTransport(XPlaneLogger.ConsoleTransport(), logLevels)
def _writeStaticRotation(self, bakeMatrix): debug = getDebug() indent = self.getIndent() o = '' bakeMatrix = bakeMatrix rotation = bakeMatrix.to_euler('XYZ') rotation[0] = round(rotation[0],5) rotation[1] = round(rotation[1],5) rotation[2] = round(rotation[2],5) # ignore noop rotations if rotation[0] == 0 and rotation[1] == 0 and rotation[2] == 0: return o if debug: o += indent + '# static rotation\n' # Ben says: this is SLIGHTLY counter-intuitive...Blender axes are # globally applied in a Euler, so in our XYZ, X is affected -by- Y # and both are affected by Z. # # Since X-Plane works opposite this, we are going to apply the # animations exactly BACKWARD! ZYX. The order here must # be opposite the decomposition order above. # # Note that since our axis naming is ALSO different this will # appear in the OBJ file as Y -Z X. # # see also: http://hacksoflife.blogspot.com/2015/11/blender-notepad-eulers.html axes = (2, 1, 0) eulerAxes = [(0.0,0.0,1.0),(0.0,1.0,0.0),(1.0,0.0,0.0)] i = 0 for axis in eulerAxes: deg = math.degrees(rotation[axes[i]]) # ignore zero rotation if not deg == 0: o += indent + 'ANIM_rotate\t%s\t%s\t%s\t%s\t%s\n' % ( floatToStr(axis[0]), floatToStr(axis[2]), floatToStr(-axis[1]), floatToStr(deg), floatToStr(deg) ) i += 1 return o
def write(self): debug = xplane_config.getDebug() indent = self.xplaneBone.getIndent() o = super().write() special_empty_props = self.blenderObject.xplane.special_empty_props if (int(bpy.context.scene.xplane.version) >= 1130 and (special_empty_props.special_type == EMPTY_USAGE_EMITTER_PARTICLE or special_empty_props.special_type == EMPTY_USAGE_EMITTER_SOUND)): if not self.xplaneBone.xplaneFile.options.particle_system_file.endswith( ".pss"): logger.error( "Particle emitter {} is used, despite no .pss file being set" .format(self.blenderObject.name)) return '' elif special_empty_props.emitter_props.name.strip() == "": logger.error( "Particle name for emitter {} can't be blank".format( self.blenderObject.name)) return '' bake_matrix = self.xplaneBone.getBakeMatrixForAttached() em_location = xplane_helpers.vec_b_to_x( bake_matrix.to_translation()) #yaw,pitch,roll theta, psi, phi = tuple( map(math.degrees, bake_matrix.to_euler()[:])) floatToStr = xplane_helpers.floatToStr o += '{indent}EMITTER {name} {x} {y} {z} {phi} {theta} {psi}'.format( indent=indent, name=special_empty_props.emitter_props.name, x=floatToStr(em_location.x), y=floatToStr(em_location.y), z=floatToStr(em_location.z), phi=floatToStr(-phi), #yaw right theta=floatToStr(theta), #pitch up psi=floatToStr(psi)) #roll right if (special_empty_props.emitter_props.index_enabled and special_empty_props.emitter_props.index >= 0): o += ' {}'.format(special_empty_props.emitter_props.index) o += '\n' return o
def _writeStaticRotation(self, bakeMatrix: mathutils.Matrix) -> str: debug = getDebug() indent = self.getIndent() o = '' bakeMatrix = bakeMatrix rotation = list( map(lambda c: round(c, xplane_constants.PRECISION_KEYFRAME), bakeMatrix.to_euler('XYZ'))) # ignore noop rotations if rotation == (0, 0, 0): return o if debug: o += indent + '# static rotation\n' # Ben says: this is SLIGHTLY counter-intuitive...Blender axes are # globally applied in a Euler, so in our XYZ, X is affected -by- Y # and both are affected by Z. # # Since X-Plane works opposite this, we are going to apply the # animations exactly BACKWARD! ZYX. The order here must # be opposite the decomposition order above. # # Note that since our axis naming is ALSO different this will # appear in the OBJ file as Y -Z X. # # see also: http://hacksoflife.blogspot.com/2015/11/blender-notepad-eulers.html axes = (2, 1, 0) eulerAxes = [(0, 0, 1), (0, 1, 0), (1, 0, 0)] for i, axis in enumerate(eulerAxes): deg = math.degrees(rotation[axes[i]]) # ignore zero rotation if not round(deg, xplane_constants.PRECISION_KEYFRAME) == 0: tab = "\t" o += (f"{indent}ANIM_rotate" f"\t{tab.join(map(floatToStr,vec_b_to_x(axis)))}" f"\t{tab.join(map(floatToStr, [deg, deg]))}\n") return o
def _writeEulerRotationKeyframes(self, dataref, keyframes): debug = getDebug() o = '' indent = self.getIndent() axes, final_rotation_mode = keyframes.getReferenceAxes() totalRot = 0 for axis,order in zip(axes,XPlaneKeyframeCollection.EULER_AXIS_ORDERING[final_rotation_mode]): ao = '' totalAxisRot = 0 ao += "%sANIM_rotate_begin\t%s\t%s\t%s\t%s\n" % ( indent, floatToStr(axis[0]), floatToStr(axis[2]), floatToStr(-axis[1]), dataref ) for keyframe in keyframes: deg = math.degrees(keyframe.rotation[order]) totalRot += abs(deg) totalAxisRot += abs(deg) ao += "%sANIM_rotate_key\t%s\t%s\n" % ( indent, floatToStr(keyframe.value), floatToStr(deg) ) ao += self._writeKeyframesLoop(dataref) ao += "%sANIM_rotate_end\n" % indent # do not write non-animated axis if round(totalAxisRot, FLOAT_PRECISION) > 0: o += ao # do not write zero rotations if round(totalRot, FLOAT_PRECISION) == 0: return '' return o
def write(self): debug = xplane_config.getDebug() indent = self.xplaneBone.getIndent() o = super().write() special_empty_props = self.blenderObject.xplane.special_empty_props if (int(bpy.context.scene.xplane.version) >= 1130 and (special_empty_props.special_type == EMPTY_USAGE_EMITTER_PARTICLE or special_empty_props.special_type == EMPTY_USAGE_EMITTER_SOUND)): if not self.xplaneBone.xplaneFile.options.particle_system_file.endswith(".pss"): logger.error("Particle emitter {} is used, despite no .pss file being set" .format(self.blenderObject.name)) return '' elif special_empty_props.emitter_props.name.strip() == "": logger.error("Particle name for emitter {} can't be blank" .format(self.blenderObject.name)) return '' bake_matrix = self.xplaneBone.getBakeMatrixForAttached() em_location = xplane_helpers.vec_b_to_x(bake_matrix.to_translation()) #yaw,pitch,roll theta, psi, phi = tuple(map(math.degrees,bake_matrix.to_euler()[:])) floatToStr = xplane_helpers.floatToStr o += '{indent}EMITTER {name} {x} {y} {z} {phi} {theta} {psi}'.format( indent=indent, name=special_empty_props.emitter_props.name, x=floatToStr(em_location.x), y=floatToStr(em_location.y), z=floatToStr(em_location.z), phi=floatToStr(-phi), #yaw right theta=floatToStr(theta), #pitch up psi=floatToStr(psi)) #roll right if (special_empty_props.emitter_props.index_enabled and special_empty_props.emitter_props.index >= 0): o += ' {}'.format(special_empty_props.emitter_props.index) o += '\n' return o
def _writeRotationKeyframes(self, dataref) -> str: debug = getDebug() keyframes = self.animations[dataref] o = '' if not self.isDataRefAnimatedForRotation(): return o if debug: o += f"{self.getIndent()}# rotation keyframes\n" rotationMode = keyframes[0].rotationMode if rotationMode == 'AXIS_ANGLE': o += self._writeAxisAngleRotationKeyframes(dataref, keyframes) elif rotationMode == 'QUATERNION': o += self._writeQuaternionRotationKeyframes(dataref, keyframes) else: o += self._writeEulerRotationKeyframes(dataref, keyframes) return o
def write(self): debug = getDebug() indent = self.xplaneBone.getIndent() o = '' xplaneFile = self.xplaneBone.xplaneFile commands = xplaneFile.commands if debug: o += "%s# %s: %s\tweight: %d\n" % (indent, self.type, self.name, self.weight) o += commands.writeReseters(self) for attr in self.attributes: o += commands.writeAttribute(self.attributes[attr], self) # if the file is a cockpit file write all cockpit attributes if xplaneFile.options.export_type == EXPORT_TYPE_COCKPIT: for attr in self.cockpitAttributes: o += commands.writeAttribute(self.cockpitAttributes[attr], self) return o
def _writeTranslationKeyframes(self, dataref): debug = getDebug() keyframes = self.animations[dataref] o = '' if not self.isDataRefAnimatedForTranslation(): return o # Apply scaling to translations pre_loc, pre_rot, pre_scale = self.getPreAnimationMatrix().decompose() totalTrans = 0 indent = self.getIndent() if debug: o += indent + '# translation keyframes\n' o += "%sANIM_trans_begin\t%s\n" % (indent, dataref) for keyframe in keyframes: totalTrans += abs(keyframe.location[0]) + abs(keyframe.location[1]) + abs(keyframe.location[2]) o += "%sANIM_trans_key\t%s\t%s\t%s\t%s\n" % ( indent, floatToStr(keyframe.value), floatToStr(keyframe.location[0] * pre_scale[0]), floatToStr(keyframe.location[2] * pre_scale[2]), floatToStr(-keyframe.location[1] * pre_scale[1]) ) o += self._writeKeyframesLoop(dataref) o += "%sANIM_trans_end\n" % indent # do not write zero translations if totalTrans == 0: return '' return o
def _writeTranslationKeyframes(self, dataref: str) -> str: debug = getDebug() keyframes = self.animations[dataref] o = '' if not self.isDataRefAnimatedForTranslation(): return o # Apply scaling to translations pre_loc, pre_rot, pre_scale = self.getPreAnimationMatrix().decompose() totalTrans = 0 indent = self.getIndent() if debug: o += f"{indent}# translation keyframes\n" o += f"{indent}ANIM_trans_begin\t{dataref}\n" for keyframe in keyframes: totalTrans += sum(map(abs, keyframe.location)) o += (f"{indent}ANIM_trans_key" f"\t{floatToStr(keyframe.dataref_value)}" f"\t{floatToStr(keyframe.location[0] * pre_scale[0])}" f"\t{floatToStr(keyframe.location[2] * pre_scale[2])}" f"\t{floatToStr(-keyframe.location[1] * pre_scale[1])}" f"\n") o += self._writeKeyframesLoop(dataref) o += f"{indent}ANIM_trans_end\n" # do not write zero translations if totalTrans == 0: return '' return o
def _writeRotationKeyframes(self, dataref): debug = getDebug() keyframes = self.animations[dataref] o = '' if not self.isDataRefAnimatedForRotation(): return o indent = self.getIndent() if debug: o += indent + '# rotation keyframes\n' rotationMode = keyframes[0].rotationMode if rotationMode == 'AXIS_ANGLE': o += self._writeAxisAngleRotationKeyframes(dataref,keyframes) elif rotationMode == 'QUATERNION': o += self._writeQuaternionRotationKeyframes(dataref,keyframes) else: o += self._writeEulerRotationKeyframes(dataref,keyframes) return o
def _writeEulerRotationKeyframes(self, dataref, keyframes) -> str: debug = getDebug() o = '' indent = self.getIndent() axes, final_rotation_mode = keyframes.getReferenceAxes() totalRot = 0 for axis, order in zip( axes, XPlaneKeyframeCollection. EULER_AXIS_ORDERING[final_rotation_mode]): ao = '' totalAxisRot = 0 tab = "\t" ao += (f"{indent}ANIM_rotate_begin" f"\t{tab.join(map(floatToStr, vec_b_to_x(axis)))}" f"\t{dataref}\n") for keyframe in keyframes: deg = math.degrees(keyframe.rotation[order]) totalRot += abs(deg) totalAxisRot += abs(deg) ao += f"{indent}ANIM_rotate_key\t{floatToStr(keyframe.dataref_value)}\t{floatToStr(deg)}\n" ao += self._writeKeyframesLoop(dataref) ao += f"{indent}ANIM_rotate_end\n" # do not write non-animated axis if round(totalAxisRot, xplane_constants.PRECISION_KEYFRAME) > 0: o += ao # do not write zero rotations if round(totalRot, xplane_constants.PRECISION_KEYFRAME) == 0: return '' return o
def collectAnimations(self) -> None: """ Collects animation_data from blenderObject, and pairs it with xplane datarefs """ if not self.parent: return None debug = getDebug() bone = self.blenderBone blenderObject = self.blenderObject #check for animation #if bone: #print("\t\t checking animations of %s:%s" % (blenderObject.name, bone.name)) #else: #print("\t\t checking animations of %s" % blenderObject.name) try: if bone: # bone animation data resides in the armature objects .data block fcurves = [ f for f in blenderObject.data.animation_data.action.fcurves if f.data_path.startswith( f'bones["{bone.name}"].xplane.datarefs') ] else: fcurves = [ f for f in blenderObject.animation_data.action.fcurves if f.data_path.startswith(f"xplane.datarefs") ] except AttributeError: pass else: for fcurve in fcurves: if bone: index = int( fcurve. data_path[len(f'bones["{bone.name}"].xplane.datarefs[' ):-len("].value")]) else: index = int(fcurve.data_path[len("xplane.datarefs[" ):-len("].value")]) try: if bone: dataref = bone.xplane.datarefs[index].path else: dataref = blenderObject.xplane.datarefs[index].path except IndexError: # Due to a long standing bug in (I think in BONE_OT_remove_xplane_dataref.execute) # sometimes a Bone's fcurve is not properly removed. Any further indexes will also # be wrong. # # TODO: Fix whatever is causing this, but, we'll still need this for old .blend files # - Ted, 6/24/2020 return else: if len(fcurve.keyframe_points) > 1: if bone: self.datarefs[dataref] = bone.xplane.datarefs[ index] else: self.datarefs[ dataref] = blenderObject.xplane.datarefs[index] self.animations[dataref] = XPlaneKeyframeCollection([ XPlaneKeyframe(kf, i, dataref, self) for i, kf in enumerate(fcurve.keyframe_points) ])
def collectAnimations(self): if not self.parent: return None debug = getDebug() bone = self.blenderBone blenderObject = self.blenderObject # if bone: # groupName = "XPlane Datarefs " + bone.name # else: # groupName = "XPlane Datarefs" #check for animation if bone: logger.info("\t\t checking animations of %s:%s" % (blenderObject.name, bone.name)) else: logger.info("\t\t checking animations of %s" % blenderObject.name) animationData = blenderObject.animation_data # bone animation data resides in the armature objects .data block if bone: animationData = blenderObject.data.animation_data if (animationData != None and animationData.action != None and len(animationData.action.fcurves) > 0): logger.info("\t\t animation found") #check for dataref animation by getting fcurves with the dataref group for fcurve in animationData.action.fcurves: logger.info("\t\t checking FCurve %s Group: %s" % (fcurve.data_path, fcurve.group)) # Ben says: I'm not sure if this is the right way to do this -- when we iterate the fcurve data for this # armature, EVERY bone is included in a big pile. So we parse the data_path and if it's clearly (1) for a bone and # (2) NOT for us, we skip it. Without this, the key frames from differing bones get cross-contaminated in a multi- # bone case. if fcurve.data_path.startswith("bones[\"") and bone != None: path_we_want = "bones[\"%s\"]" % bone.name if not fcurve.data_path.startswith(path_we_want): continue #if (fcurve.group != None and fcurve.group.name == groupName): # since 2.61 group names are not set so we have to check the datapath if ('xplane.datarefs' in fcurve.data_path): # get dataref name pos = fcurve.data_path.find('xplane.datarefs[') if pos!=-1: index = fcurve.data_path[pos+len('xplane.datarefs[') : -len('].value')] else: return # old style datarefs with wrong datapaths can cause errors so we just skip them try: index = int(index) except: return # FIXME: removed datarefs keep fcurves, so we have to check if dataref is still there. # FCurves have to be deleted correctly. if bone: if index < len(bone.xplane.datarefs): dataref = bone.xplane.datarefs[index].path else: return else: if index < len(blenderObject.xplane.datarefs): dataref = blenderObject.xplane.datarefs[index].path else: return logger.info("\t\t adding dataref animation: %s" % dataref) if len(fcurve.keyframe_points) > 1: # time to add dataref to animations if bone: self.datarefs[dataref] = bone.xplane.datarefs[index] else: self.datarefs[dataref] = blenderObject.xplane.datarefs[index] # store keyframes temporary, so we can resort them keyframes = [] for i,keyframe in enumerate(fcurve.keyframe_points): logger.info("\t\t adding keyframe: %6.3f" % keyframe.co[0]) keyframes.append(XPlaneKeyframe(keyframe,i,dataref,self)) # sort keyframes by frame number keyframesSorted = sorted(keyframes, key = lambda keyframe: keyframe.index) self.animations[dataref] = XPlaneKeyframeCollection(keyframesSorted)