""" This module provides Python wrappers around animation code. classes: Key -- A single key in an animation curve. Curve -- A collection of keys that can be evaluated at a certain time. ChildEvent -- A single event in the event template. ChildEventGroup -- A group of child events, from which only one can spawn. EventTemplate -- The collection of child event groups which produces the Take. Animation -- A collection of curves and an event template. A wrapper around a FaceFx Studio animation. PreviewAnimationSettings - The preview animation settings for the currently selected animation in FaceFX Studio. Owner: Jamie Redmond Copyright (c) 2002-2011 OC3 Entertainment, Inc. """ from FxStudio import getEventTemplate, getEventTake, getAnimationProperties,\ getCurveNames, getAnimationNames, getKeys, isCurveOwnedByAnalysis,\ issueCommand, getPreviewAnimationSettings, FaceFXError from FxPhonemes import PhonemeWordList class Key(object): """ A wrapper around a single key. instance variables: time -- The time of the key, in seconds. value -- The value of the key slopeIn -- The slope of the curve as it comes into the key slopeOut -- The slope of the curve as it leaves the key """ def __init__(self, keyTupleFromStudio): """ Initializes the key from a tuple sent back from FaceFx Studio. parameters: keyTupleFromStudio -- A tuple (time, value, slopeIn, slopeOut) """ self.time = keyTupleFromStudio[0] self.value = keyTupleFromStudio[1] self.slopeIn = keyTupleFromStudio[2] self.slopeOut = keyTupleFromStudio[3] def __str__(self): """ Returns the string representation of the key. """ return 'Key: time={0}, value={1}, slopeIn={2}, slopeOut={3}'.format( self.time, self.value, self.slopeIn, self.slopeOut) def __repr__(self): """ Returns the Python represenation of the key. """ return 'Key(({0}, {1}, {2}, {3}))'.format(self.time, self.value, self.slopeIn, self.slopeOut) class HermiteKeyInterpolator(object): """ A class that performs a modified Hermite interpolation between two keys. """ def interpolate(self, firstKey, secondKey, time): """ Perform a modified Hermite interpolation between two keys. Returns the curve value at the requested time between the keys. keyword arguments: firstKey -- the key with a time less than "time" secondKey -- the key with a time greater than "time" time -- the time for which to evaluate. returns: float """ time1 = firstKey.time time2 = secondKey.time deltaTime = time2 - time1 parametricTime = (time - time1) / deltaTime p0 = firstKey.value p1 = secondKey.value m0 = firstKey.slopeOut * deltaTime m1 = secondKey.slopeIn * deltaTime return parametricTime * (parametricTime * (parametricTime * (2.0*p0 - 2.0*p1 + m0 + m1) + (-3.0*p0 + 3.0*p1 - 2.0*m0 - m1)) + m0) + p0 class Curve(object): """ A collection of keys that can be evaluated at a given time. instance variables: animation -- a reference back to the animation containing this curve name -- the name of the curve interpolator -- an object that can interpolate(firstKey, secondKey, time) keys -- a list of the keys in the animation isOwnedByAnalysis -- boolean; True if the curve is owned by analysis, meaning changes cannot be brought back into FaceFX Studio """ def __init__(self, name, curveTupleFromStudio, animation): """ Initializes the animation with the tuple from studio. """ self.animation = animation self.name = name self.interpolator = HermiteKeyInterpolator() self.keys = [Key(key) for key in curveTupleFromStudio] self.isOwnedByAnalysis = isCurveOwnedByAnalysis( self.animation.groupName, self.animation.name, self.name) def __str__(self): """ Returns the string representation of the curve. """ return 'Curve: "{0}" [{1} keys, owned by {2}]'.format( self.name, len(self), 'Analysis' if self.isOwnedByAnalysis else 'User') def __repr__(self): """ Hackish repr to make printing lists of curves pretty. """ return self.__str__() def __len__(self): """ Returns the number of keys in the curve. """ return len(self.keys) def __getitem__(self, item): """ Returns the key at item. """ return self.keys[item] def getInterpolator(self): """ Returns the interpolator object in use by the curve. """ return self.interpolator def setInterpolator(self, interpolator): """ Sets the interpolator object that the curve will use to evaluate """ self.interpolator = interpolator def getNumKeys(self): """ Returns the number of keys in the curve """ return len(self) def getStartTime(self): """ Returns the time of the first key. """ try: return self[0].time except IndexError: return 0.0 def getEndTime(self): """ Returns the time of the last key. """ try: return self[-1].time except IndexError: return 0.0 def evaluateAt(self, time): """ Evaluates the curve at the given time. keyword arguments: time -- the time in seconds to evaluate at """ value = 0.0 numKeys = self.getNumKeys() if( numKeys > 0 ): numKeysM1 = numKeys - 1 # Check for out-of-range time and clamp to end points of curve. if time <= self.keys[0].time: value = self.keys[0].value elif time >= self.keys[numKeysM1].time: value = self.keys[numKeysM1].value else: # The time is in range. if 1 == numKeys: value = self.keys[0].value else: # Find the bounding keys. firstKey = 0 secondKey = 0 pos = 0 for i in range(numKeysM1): if self.keys[i].time <= time and time < self.keys[i+1].time: pos = i break if pos != numKeysM1: firstKey = pos secondKey = pos+1 else: firstKey = pos-1 secondKey = pos # Interpolate. value = self.interpolator.interpolate(self.keys[firstKey], self.keys[secondKey], time) return value class ChildEvent(object): """ A wrapper around a child event in the event template. instance variables: animGroupName -- the name of the animation group the event points to animName -- the name of the animation the event points to startTimeRange -- a tuple defining the range when the event can start magnitudeRange -- a tuple defining the range of the event's magnitude scale durationRange -- a tuple defining the range of the event's duration scale blendInRange -- a tuple defining the range of the event's blend in time blendOutRange -- a tuple defining the range of the event's blend out time customPayload -- a string that will be sent back to the game engine at the event's ingress when the event is played in game eventID -- a read-only internal identifier isDurationScaledByParent -- If this is true then when the take is created this event's duration will be scaled by the duration scale of the event that spawned it into the take. If this is false or this event was not spawned by another event this event's duration will fall within its own durationRange. isMagnitudeScaledByParent -- If this is true then when the take is created this event's magnitude will be scaled by the magnitude scale of the event that spawned it into the take. If this is false or this event was not spawned by another event this event's magnitude will fall within its own magnitudeRange. isBlendUnscaled -- True if the event's blend times are unscaled by the event's duration useParentBlendTimes -- True if the event's blend times where inherited from the event that spawned it into the take. shouldPersistValue -- True if the event's values will "stick" on the character at the event's egress spawnConditionProbability -- The probability that the event will be spawned into the take. A value of None or 1.0 means it is always spawned. spawnConditionDurationScale -- If the duration scale of the event that could possibly spawn this event into the take falls into this range then this event will be spawned. A value of None means it is unbounded to that side. spawnConditionMagnitudeScale -- If the magnitude scale of the event that could possibly spawn this event into the take falls into this range then this event will be spawned. A value of None means it is unbounded to that side. spawnConditionStartTimeOffset -- If the actual start time of the event that could possibly spawn this event into the take falls into this range this event will be spawned. A value of None means it is unbounded to that side. weight -- The weight assigned to this event in a group, used for picking one event from a group of child events """ def __init__(self, childEventTupleFromStudio): """ Initializes the child event with the tuple from Studio. """ self.animGroupName = childEventTupleFromStudio[0] self.animName = childEventTupleFromStudio[1] self.startTimeRange = childEventTupleFromStudio[2] self.magnitudeRange = childEventTupleFromStudio[3] self.durationRange = childEventTupleFromStudio[4] self.blendInRange = childEventTupleFromStudio[5] self.blendOutRange = childEventTupleFromStudio[6] self.customPayload = childEventTupleFromStudio[7] self.eventID = childEventTupleFromStudio[8] self.isDurationScaledByParent = childEventTupleFromStudio[9] self.isMagnitudeScaledByParent = childEventTupleFromStudio[10] self.isBlendUnscaled = childEventTupleFromStudio[11] self.useParentBlendTimes = childEventTupleFromStudio[12] self.shouldPersistValues = childEventTupleFromStudio[13] self.spawnConditionProbability = childEventTupleFromStudio[14] self.spawnConditionDurationScale = childEventTupleFromStudio[15] self.spawnConditionMagnitudeScale = childEventTupleFromStudio[16] self.spawnConditionStartTimeOffset = childEventTupleFromStudio[17] self.weight = childEventTupleFromStudio[18] def __str__(self): """ Returns the string representation of the child event. """ r = "animGroupName: " + str(self.animGroupName) + "\n" r += "animName: " + str(self.animName) + "\n" r += "startTimeRange: (" + str(self.startTimeRange[0]) + ", " + str(self.startTimeRange[1]) + ")" + "\n" r += "magnitudeRange: (" + str(self.magnitudeRange[0]) + ", " + str(self.magnitudeRange[1]) + ")" + "\n" r += "durationRange: (" + str(self.durationRange[0]) + ", " + str(self.durationRange[1]) + ")" + "\n" r += "blendInRange: (" + str(self.blendInRange[0]) + ", " + str(self.blendInRange[1]) + ")" + "\n" r += "blendOutRange: (" + str(self.blendOutRange[0]) + ", " + str(self.blendOutRange[1]) + ")" + "\n" r += "customPayload: " + str(self.customPayload) + "\n" r += "eventID: " + str(self.eventID) + "\n" r += "isDurationScaledByParent: " + str(self.isDurationScaledByParent) + "\n" r += "isMagnitudeScaledByParent: " + str(self.isMagnitudeScaledByParent) + "\n" r += "isBlendUnscaled: " + str(self.isBlendUnscaled) + "\n" r += "useParentBlendTimes: " + str(self.useParentBlendTimes) + "\n" r += "shouldPersistValues: " + str(self.shouldPersistValues) + "\n" r += "spawnConditionProbability: " + str(self.spawnConditionProbability) + "\n" r += "spawnConditionDurationScale: (" + str(self.spawnConditionDurationScale[0]) + ", " + str(self.spawnConditionDurationScale[1]) + ")" + "\n" r += "spawnConditionMagnitudeScale: (" + str(self.spawnConditionMagnitudeScale[0]) + ", " + str(self.spawnConditionMagnitudeScale[1]) + ")" + "\n" r += "spawnConditionStartTimeOffset: (" + str(self.spawnConditionStartTimeOffset[0]) + ", " + str(self.spawnConditionStartTimeOffset[1]) + ")" + "\n" r += "weight: " + str(self.weight) + "\n" return r class ChildEventGroup(object): """ A group of child events. instance variables: childEvents -- a list of the child events in the group """ def __init__(self, childEventGroupTupleFromStudio): """ Initializes the child event group with the tuple from Studio. """ self.childEvents = [ChildEvent(e) for e in childEventGroupTupleFromStudio] def __len__(self): """ Returns the number of child events in the group. """ return len(self.childEvents) def __getitem__(self, item): """ Returns the child event at item """ return self.childEvents[item] def getNumChildEvents(self): """ Returns the number of child events in the group. """ return len(self) def __str__(self): """ Returns the string representation of the child event group. """ r = str(self.getNumChildEvents()) + " childEvents:\n" childEventIndex = 0 for childEvent in self.childEvents: r += "childEvent " + str(childEventIndex) + ":\n" r += str(childEvent) childEventIndex += 1 return r class EventTemplate(object): """ An event template defines the events that might be spawned in a take. instance variables: templateRevisionID -- a read-only internal identifier childEventGroups -- a list of the groups of child events in the template """ def __init__(self, animGroupName, animName): """ Requests the event template for the given animation from Studio. """ eventTemplateTuple = getEventTemplate(animGroupName, animName) self.templateRevisionID = -1 self.childEventGroups = [] if len(eventTemplateTuple) >= 2: self.templateRevisionID = eventTemplateTuple[0] for childEventGroup in eventTemplateTuple[1]: self.childEventGroups.append(ChildEventGroup(childEventGroup)) def __len__(self): """ Returns the number of child event groups in the template. """ return len(self.childEventGroups) def __getitem__(self, item): """ Returns the child event group at item. """ return self.childEventGroups[item] def getNumChildEventGroups(self): """ Returns the number of child event groups in the template. """ return len(self.childEventGroups) def __str__(self): """ Returns the string representation of the event template. """ r = "templateRevisionID: " + str(self.templateRevisionID) + "\n" r += str(self.getNumChildEventGroups()) + " childEventGroups:\n" childEventGroupIndex = 0 for childEventGroup in self.childEventGroups: r += "childEventGroup " + str(childEventGroupIndex) + ":\n" r += str(childEventGroup) childEventGroupIndex += 1 return r class Event(object): """ An event contained in a take. instance variables: animGroupName -- the name of the animation group the event points to animName -- the name of the animation the event points to startTime -- the start time of the event (in seconds) duration -- the duration of the event (in seconds) durationScale -- the duration scale of the event magnitudeScale -- the magnitude scale of the event blendInTime -- the blend in time of the event (in seconds) blendOutTime -- the blend out time of the event (in seconds) shouldPersistValue -- True if the event's values will "stick" on the character at the event's egress customPayload -- a string that will be sent back to the game engine at the event's ingress when the event is played in game eventID -- a read-only internal identifier """ def __init__(self, eventTupleFromStudio): """ Initializes the event with the tuple from Studio. """ self.animGroupName = eventTupleFromStudio[0] self.animName = eventTupleFromStudio[1] self.startTime = eventTupleFromStudio[2] self.duration = eventTupleFromStudio[3] self.durationScale = eventTupleFromStudio[4] self.magnitudeScale = eventTupleFromStudio[5] self.blendInTime = eventTupleFromStudio[6] self.blendOutTime = eventTupleFromStudio[7] self.shouldPersistValues = eventTupleFromStudio[8] self.customPayload = eventTupleFromStudio[9] self.eventID = eventTupleFromStudio[10] def __str__(self): """ Returns the string representation of the event. """ r = "animGroupName: " + str(self.animGroupName) + "\n" r += "animName: " + str(self.animName) + "\n" r += "startTime: " + str(self.startTime) + "\n" r += "duration: " + str(self.duration) + "\n" r += "durationScale: " + str(self.durationScale) + "\n" r += "magnitudeScale: " + str(self.magnitudeScale) + "\n" r += "blendInTime: " + str(self.blendInTime) + "\n" r += "blendOutTime: " + str(self.blendOutTime) + "\n" r += "shouldPersistValues: " + str(self.shouldPersistValues) + "\n" r += "customPayload: " + str(self.customPayload) + "\n" r += "eventID: " + str(self.eventID) + "\n" return r class EventTake(object): """ An event take defines the events that are actually in the take. instance variables: events -- a list of the events contained in the take """ def __init__(self, animGroupName, animName): """ Requests the event take for the given animation from Studio. """ eventTakeTuple = getEventTake(animGroupName, animName) self.events = [] for event in eventTakeTuple: self.events.append(Event(event)) def __len__(self): """ Returns the number of events in the take. """ return len(self.events) def __getitem__(self, item): """ Returns the event at item. """ return self.events[item] def getNumEvents(self): """ Returns the number of events in the take. """ return len(self.events) def __str__(self): """ Returns the string representation of the event take. """ r = str(self.getNumEvents()) + " events:\n" eventIndex = 0 for event in self.events: r += "event " + str(eventIndex) + ":\n" r += str(event) eventIndex += 1 return r class Animation(object): """ A collection of curves, an event template, an event take, and other animation properties. instance variables: groupName -- the name of the group containing the animation name -- the name of the animation startTime -- the start time of the animation, either events or curves endTime -- the end time of the animation, either events or curves curvesStartTime -- the start time of the curves curvesEndTime -- the end time of the curves frameRate -- the frame rate of the animation absoluteAudioAssetPath -- the absolute path to the audio asset audioAssetPath -- the relative path to the audio asset language -- the language the animation was analyzed in analysisActor -- the name of the analysis actor used to analyze the anim analysisText -- the text used to analyze the anim phonemeWordList -- a PhonemeWordList object containing the phonemes and words that were output by the analysis curves -- a list of Curve objects containing the curves in the animation eventTemplate -- an EventTemplate object containing the child event groups eventTake -- an EventTake object containing the events """ def __init__(self, animGroupName, animName): """ Initializes the animation by pulling the data from Studio. """ self.groupName = animGroupName self.name = animName self.path = animGroupName + "/" + animName try: animationProperties = getAnimationProperties(animGroupName, animName) self.startTime = animationProperties[0] self.endTime = animationProperties[1] self.curvesStartTime = animationProperties[2] self.curvesEndTime = animationProperties[3] self.frameRate = animationProperties[4] self.absoluteAudioAssetPath = animationProperties[5] self.audioAssetPath = animationProperties[6] self.language = animationProperties[7] self.analysisActor = animationProperties[8] self.analysisText = animationProperties[9] self.phonemeWordList = PhonemeWordList(animGroupName, animName) curveNames = getCurveNames(animGroupName, animName) self.curves = [Curve(c, getKeys(animGroupName, animName, c), self) for c in curveNames] self.eventTemplate = EventTemplate(animGroupName, animName) self.eventTake = EventTake(animGroupName, animName) except Exception, e: raise FaceFXError('{0}'.format(e)) def getNumCurves(self): """ Returns the number of curves in the animation. """ return len(self.curves) def findCurve(self, curveName): """ Returns the curve with the requested name, or None. """ for curve in self.curves: if curve.name == curveName: return curve return None def __str__(self): """ Returns the string representation of the Animation. """ r = str(self.path) + ":\n" r += " startTime: " + str(self.startTime) + "\n" r += " endTime: " + str(self.endTime) + "\n" r += " curvesStartTime: " + str(self.curvesStartTime) + "\n" r += " curvesEndTime: " + str(self.curvesEndTime) + "\n" r += " frameRate: " + str(self.frameRate) + "\n" r += " absoluteAudioAssetPath: " + self.absoluteAudioAssetPath + "\n" r += " audioAssetPath: " + self.audioAssetPath + "\n" r += " language: " + self.language + "\n" r += " analysisActor: " + self.analysisActor + "\n" r += " analysisText: " + self.analysisText + "\n" r += " " + str(self.getNumCurves()) + " curves:\n" curveIndex = 0 for curve in self.curves: r += " [" + str(curveIndex) + "] " + str(curve) + "\n" curveIndex += 1 return r class PreviewAnimationSettings(object): """ The preview animation settings for the currently selected animation in FaceFX Studio. instance variables: blendMode -- the current preview animation blend mode animationName -- the name of the skeletal animation being used for preview purposes length -- the length of the preview animation (in seconds) startTime -- the time at which the preview animation starts in the FaceFX Studio timeline (in seconds) blendInTime -- the time (in seconds) over which the preview animation blends in blendOutTime -- the time (in seconds) over which the preview animation blends out loop - inidcates whether or not the preview animation is looping Notes: blendMode is always valid (a string), but the other instance variables will be set to None if there is no preview animation selected in FaceFX Studio """ def __init__(self): """ Initializes the preview animation settings with the tuple from Studio. """ previewAnimationSettingsTupleFromStudio = getPreviewAnimationSettings() self.blendMode = previewAnimationSettingsTupleFromStudio[0] if "" == previewAnimationSettingsTupleFromStudio[1]: self.animationName = None self.length = None self.startTime = None self.blendInTime = None self.blendOutTime = None self.loop = None else: self.animationName = previewAnimationSettingsTupleFromStudio[1] self.length = previewAnimationSettingsTupleFromStudio[2] self.startTime = previewAnimationSettingsTupleFromStudio[3] self.blendInTime = previewAnimationSettingsTupleFromStudio[4] self.blendOutTime = previewAnimationSettingsTupleFromStudio[5] self.loop = previewAnimationSettingsTupleFromStudio[6] def __str__(self): """ Returns the string representation of the preview animation settings. """ r = "Preview Animation Settings:\n" r += " blendMode: " + str(self.blendMode) + "\n" r += " animationName: " + str(self.animationName) + "\n" r += " length: " + str(self.length) + "\n" r += " startTime: " + str(self.startTime) + "\n" r += " blendInTime: " + str(self.blendInTime) + "\n" r += " blendOutTime: " + str(self.blendOutTime) + "\n" r += " loop: " + str(self.loop) + "\n" return r