# Create a camera and look through it in the viewport
camera = FBCamera('Camera')
camera.Show = True
FBSystem().Renderer.CurrentCamera = camera
camera.Selected = True
# Animate the Roll property, thereby creating an FCurve
camera.Roll.SetAnimated(True)
# Keep a local reference to the roll FCurve
node = camera.Roll.GetAnimationNode()
fcurve = node.FCurve
fcurve.KeyAdd(FBTime(0, 0, 0, 0), 0.0)
fcurve.KeyAdd(FBTime(0, 0, 0, 100), 100.0)
fcurve.KeyAdd(FBTime(0, 0, 0, 50), 25.0)
fcurve.KeyAdd(FBTime(0, 0, 0, 50), 10.0)
# At this point, we have three new keys:
# 0.0 at frame 0, 10.0 at frame 50, and 100.0 at frame 100
index = fcurve.KeyAdd(FBTime(0, 0, 0, 75), 50.0)
key = fcurve.Keys[index]
print key # <FBFCurveKey object>
for i in range(0, len(fcurve.Keys)):
key = fcurve.Keys[i]
print '[%d] Frame %3d: %.2f' % (
i,
key.Time.GetFrame(True),
key.Value
)
# "for key in fcurve.Keys" works equally well if you don't need the index
import math
for i in range(0, 150):
fcurve.KeyAdd(FBTime(0, 0, 0, i), math.sin(i * 0.1) * 10)
# Specify a range. To delete a single key, use the same index for both values.
start = 50
stop = 100
# MotionBuilder will crash if the index range is out of bounds!
assert start >= 0 and start < len(fcurve.Keys)
assert stop >= 0 and stop < len(fcurve.Keys)
fcurve.KeyDeleteByIndexRange(start, stop)
# Bad! Untouched keys have their indices shifted around as we iterate forward.
for i in range(0, len(fcurve.Keys), 2):
fcurve.KeyDeleteByIndexRange(i, i)
# Works as intended!
for i in reversed(range(0, len(fcurve.Keys), 2)):
fcurve.KeyDeleteByIndexRange(i, i)
# Delete all keys between 2 seconds and the end of time.
# Since pInclusive is False, the key at the 2-second mark will be left intact.
fcurve.KeyDeleteByTimeRange(FBTime(0, 0, 2), FBTime.Infinity, False)
fcurve.EditClear()
# Effectively equivalent to:
# fcurve.KeyDeleteByTimeRange(FBTime.MinusInfinity, FBTime.Infinity)
def SerializeCurve(fcurve):
'''
Returns a list of dictionaries representing each of the keys in the given
FCurve.
'''
keyDataList = []
for key in fcurve.Keys:
keyData = {
'time': key.Time.Get(),
'value': key.Value,
'interpolation': int(key.Interpolation),
'tangent-mode': int(key.TangentMode),
'constant-mode': int(key.TangentConstantMode),
'left-derivative': key.LeftDerivative,
'right-derivative': key.RightDerivative,
'left-weight': key.LeftTangentWeight,
'right-weight': key.RightTangentWeight
}
keyDataList.append(keyData)
return keyDataList
def TangentWeightIsDefault(tangentWeight):
'''
Returns whether the given tangent weight is equal to the default value of
1/3, taking floating-point precision into account.
'''
return tangentWeight > 0.3333 and tangentWeight < 0.3334
def DeserializeCurve(fcurve, keyDataList):
'''
Populates the given FCurve based on keyframe data listed in serialized
form. Expects key data to be ordered by time. Any existing keys will be
removed from the curve.
'''
# Ensure a blank slate
fcurve.EditClear()
# Loop 1: Add keys and set non-numeric properties
for keyData in keyDataList:
keyIndex = fcurve.KeyAdd(FBTime(keyData['time']), keyData['value'])
key = fcurve.Keys[keyIndex]
key.Interpolation = FBInterpolation.values[keyData['interpolation']]
key.TangentMode = FBTangentMode.values[keyData['tangent-mode']]
if key.TangentMode == FBTangentMode.kFBTangentModeTCB:
key.TangentMode = FBTangentMode.kFBTangentModeBreak
key.TangentConstantMode = \
FBTangentConstantMode.values[keyData['constant-mode']]
# Loop 2: With all keys in place, set tangent properties
for i in range(0, len(keyDataList)):
keyData = keyDataList[i]
key = fcurve.Keys[i]
key.LeftDerivative = keyData['left-derivative']
key.RightDerivative = keyData['right-derivative']
if not TangentWeightIsDefault(keyData['left-weight']):
key.LeftTangentWeight = keyData['left-weight']
if not TangentWeightIsDefault(keyData['right-weight']):
key.RightTangentWeight = keyData['right-weight']
There are a few less-important properties of FBFCurveKey that I didn't explicitly mention. In brief:
In the FCurve editor UI, there's a little button which enables custom tangent weight values for the selected key:
With the Wt button disabled, the tangent weights stay at their default value of 0.3333. With Wt enabled, the user can input a different value (between 0.0 and 1.0). Regardless of the value, the editor displays the tangents differently depending on whether the Wt button is enabled. When the user drags the tangent with custom weighting enabled, he's modifying the weight as well as the angle.
Unfortunately, there's no way to access this setting from Python. Initially, it's disabled, and as soon as either TangentWeight property is changed via the API, it's irrevocably enabled. That's the rationale for including that final step in the DeserializeCurve function: we don't modify the tangent weight if it was at the default value in the first place. Otherwise, the user would find that every single tangent handle affected the weight value as well as the angle.
You'll notice that when I save the values of each key's enumeration properties, I convert them to raw integers first. By using the values dictionary of each enumeration type, I can map those integers back into the correct value.
print FBInterpolation.values
# { 0: kFBInterpolationConstant,
# 1: kFBInterpolationLinear,
# 2: kFBInterpolationCubic }
# Convert from FBInterpolation to integer and back again
intValue = int(FBInterpolation.kFBInterpolationLinear)
interpolation = FBInterpolation.values[intValue]
print intValue, interpolation
# 1, kFBInterpolationLinear
Instead of using integers, you could just as well store the enumeration value names as strings. This would protect your data in case enumerations were added or shifted around in subsequent versions, and it would make the values a bit easier to deal with if you intended to use the data outside of MotionBuilder. It'd only take a bit more work:
# Convert from FBInterpolation to string and back again
strValue = str(FBInterpolation.kFBInterpolationLinear)
interpolation = getattr(FBInterpolation, strValue)
Similarly, I store the time of each keyframe as the internal FBTime value — that really big integer that only MotionBuilder knows how to interpret. To make things a bit more agnostic, you could use the floating-point GetSecondDouble() value instead. Using Get() just keeps things simple and avoids precision issues.
Around 6:53, I briefly demonstrate how to use the json module to write the serialized data for a curve to an ASCII file. Converting between simple objects and JSON files is quite painless:
import json
filepath = 'C:\\fcurve.json'
# Write a curve to a JSON file
with open(filepath, 'w') as fp:
keyDataList = SerializeCurve(fcurve)
fp.write(json.dumps(keyDataList, indent = 4))
# Read a curve from a JSON file
with open(filepath, 'r') as fp:
keyDataList = json.loads(fp.read())
DeserializeCurve(fcurve, keyDataList)
JSON is a convenient format to use since it maps so cleanly to Python lists and dictionaries, but of course you could use any format you like. Another equally convenient option from the Python Standard Library is the pickle module:
import cPickle as pickle
filepath = 'C:\\fcurve_file'
# Pickle a curve to disk in binary format
with open(filepath, 'wb') as fp:
keyDataList = SerializeCurve(fcurve)
pickle.dump(keyDataList, fp, 2)
# Unpickle binary data back to the curve
with open(filepath, 'rb') as fp:
keyDataList = pickle.load(fp)
DeserializeCurve(fcurve, keyDataList)
If you see any errors that you'd like to point out, feel free to email me at awforsythe@gmail.com.