444 lines
18 KiB
Python
Executable File
444 lines
18 KiB
Python
Executable File
from pyfbsdk import *
|
|
|
|
import random
|
|
import operator
|
|
|
|
|
|
## Defaults ##
|
|
|
|
DEFAULT_AMPLITUDE_MIN = -0.05
|
|
DEFAULT_AMPLITUDE_MAX = 0.05
|
|
DEFAULT_HOLD = False
|
|
DEFAULT_HOLD_PERCENTAGE = 50
|
|
DEFAULT_HOLD_DURATION_MIN = 5
|
|
DEFAULT_HOLD_DURATION_MAX = 20
|
|
DEFAULT_FRAME_STEP = 2
|
|
DEFAULT_ANIMATION_LAYER_NAME = 'Animation Noise 1'
|
|
|
|
# Modulate the overall animation with a curve.
|
|
CURVE_TYPE_NONE = None
|
|
CURVE_TYPE_LINEAR = 1
|
|
CURVE_TYPE_LINEAR_INVERTED = 2
|
|
CURVE_TYPE_EXPONENTIAL = 3
|
|
CURVE_TYPE_EXPONENTIAL_INVERTED = 4
|
|
|
|
CURVE_TYPES = { 'None': CURVE_TYPE_NONE,
|
|
'Linear': CURVE_TYPE_LINEAR,
|
|
'Linear Inverted': CURVE_TYPE_LINEAR_INVERTED }
|
|
|
|
|
|
## Classes ##
|
|
|
|
class AnimationNoiseTrackOptions( object ):
|
|
'''
|
|
Represents options for an individual transform component, such as the X, Y or Z.
|
|
'''
|
|
def __init__( self ):
|
|
self.enabled = False
|
|
self.animationNode = None
|
|
|
|
self.reset()
|
|
|
|
def reset( self ):
|
|
self.curveType = CURVE_TYPE_NONE
|
|
|
|
self.amplitudeMin = DEFAULT_AMPLITUDE_MIN
|
|
self.amplitudeMax = DEFAULT_AMPLITUDE_MAX
|
|
|
|
self.hold = DEFAULT_HOLD
|
|
self.holdPercentage = DEFAULT_HOLD_PERCENTAGE
|
|
self.holdDurationMin = DEFAULT_HOLD_DURATION_MIN
|
|
self.holdDurationMax = DEFAULT_HOLD_DURATION_MAX
|
|
|
|
self.frameStep = DEFAULT_FRAME_STEP
|
|
|
|
def save( self, fstream ):
|
|
fstream.write( '\t\t\t\t<amplitudeMin>{0}</amplitudeMin>\n'.format( self.amplitudeMin ) )
|
|
fstream.write( '\t\t\t\t<amplitudeMax>{0}</amplitudeMax>\n'.format( self.amplitudeMax ) )
|
|
fstream.write( '\t\t\t\t<hold>{0}</hold>\n'.format( self.hold ) )
|
|
fstream.write( '\t\t\t\t<holdPercentage>{0}</holdPercentage>\n'.format( self.holdPercentage ) )
|
|
fstream.write( '\t\t\t\t<holdDurationMin>{0}</holdDurationMin>\n'.format( self.holdDurationMin ) )
|
|
fstream.write( '\t\t\t\t<holdDurationMax>{0}</holdDurationMax>\n'.format( self.holdDurationMax ) )
|
|
fstream.write( '\t\t\t\t<frameStep>{0}</frameStep>\n'.format( self.frameStep ) )
|
|
|
|
class AnimationNoiseTracks( object ):
|
|
'''
|
|
Container for a set of transform tracks.
|
|
'''
|
|
def __init__( self ):
|
|
self.__x = AnimationNoiseTrackOptions()
|
|
self.__y = AnimationNoiseTrackOptions()
|
|
self.__z = AnimationNoiseTrackOptions()
|
|
|
|
@property
|
|
def x( self ):
|
|
return self.__x
|
|
|
|
@property
|
|
def y( self ):
|
|
return self.__y
|
|
|
|
@property
|
|
def z( self ):
|
|
return self.__z
|
|
|
|
@property
|
|
def enabled( self ):
|
|
return self.x.enabled or self.y.enabled or self.z.enabled
|
|
|
|
def save( self, fstream ):
|
|
fstream.write( '\t\t\t<x enabled="{0}">\n'.format( self.__x.enabled ) )
|
|
self.__x.save( fstream )
|
|
fstream.write( '\t\t\t</x>\n' )
|
|
|
|
fstream.write( '\t\t\t<y enabled="{0}">\n'.format( self.__y.enabled ) )
|
|
self.__y.save( fstream )
|
|
fstream.write( '\t\t\t</y>\n' )
|
|
|
|
fstream.write( '\t\t\t<z enabled="{0}">\n'.format( self.__z.enabled ) )
|
|
self.__z.save( fstream )
|
|
fstream.write( '\t\t\t</z>\n' )
|
|
|
|
class AnimationNoiseOptions( object ):
|
|
'''
|
|
Options for the animation noise.
|
|
|
|
Author:
|
|
Jason Hayes <jason.hayes@rockstarsandiego.com>
|
|
'''
|
|
def __init__( self, name = 'Animation Noise Options' ):
|
|
self.name = name
|
|
|
|
self.createAnimationLayer = False
|
|
self.animationLayerName = DEFAULT_ANIMATION_LAYER_NAME
|
|
|
|
self.translation = AnimationNoiseTracks()
|
|
self.rotation = AnimationNoiseTracks()
|
|
self.scale = AnimationNoiseTracks()
|
|
|
|
self.useActiveFrameRange = True
|
|
|
|
self.frameStart = 0
|
|
self.frameEnd = 0
|
|
|
|
def save( self, fstream ):
|
|
fstream.write( '\t<preset name="{0}">\n'.format( self.name ) )
|
|
|
|
transformNames = [ 'translation', 'rotation', 'scale' ]
|
|
|
|
for transformName in transformNames:
|
|
|
|
fstream.write( '\t\t<{0}>\n'.format( transformName ) )
|
|
|
|
if transformName == 'translation':
|
|
self.translation.save( fstream )
|
|
|
|
elif transformName == 'rotation':
|
|
self.rotation.save( fstream )
|
|
|
|
elif transformName == 'scale':
|
|
self.scale.save( fstream )
|
|
|
|
fstream.write( '\t\t</{0}>\n'.format( transformName ) )
|
|
|
|
fstream.write( '\t</preset>\n' )
|
|
|
|
class AnimationNoisePresets( object ):
|
|
def __init__( self, presetsFilename ):
|
|
self.__presetsFilename = presetsFilename
|
|
|
|
# Dictionary of AnimationNoiseOptions objects. Key is the preset name.
|
|
self.__presets = {}
|
|
|
|
def getPreset( self, presetName ):
|
|
if presetName in self.__presets:
|
|
return self.__presets[ presetName ]
|
|
|
|
return None
|
|
|
|
def load( self ):
|
|
pass
|
|
|
|
def save( self ):
|
|
fstream = open( self.__presetsFilename, 'wt' )
|
|
|
|
fstream.write( '<?xml version="1.0" encoding="UTF-8"?>\n' )
|
|
fstream.write( '<presets>\n' )
|
|
|
|
for presetName, options in self.__presets.iteritems():
|
|
options.save( fstream )
|
|
|
|
fstream.write( '</presets>' )
|
|
fstream.close()
|
|
|
|
def add( self, options ):
|
|
self.__presets[ options.name ] = options
|
|
|
|
def remove( self, presetName ):
|
|
pass
|
|
|
|
|
|
## Functions ##
|
|
|
|
def addNoise( modelObject, options ):
|
|
'''
|
|
Adds animated noise to a model object, using the supplied options.
|
|
|
|
Author:
|
|
Jason Hayes <jason.hayes@rockstarsandiego.com>
|
|
'''
|
|
|
|
def applyNoiseAnimation( frameStart, frameEnd, animationNoiseTrack ):
|
|
|
|
def addKey( fcurve, frameNum, value ):
|
|
keyIdx = fcurve.KeyAdd( FBTime( 0, 0, 0, frameNum ), value )
|
|
key = fcurve.Keys[ keyIdx ]
|
|
key.Interpolation = FBInterpolation.kFBInterpolationLinear
|
|
|
|
return keyIdx
|
|
|
|
# Valid frame ranges to apply the animation. If the track is supposed to apply a hold, then this
|
|
# will get split up into more buckets. By default, only one bucket is available.
|
|
buckets = [ [ frameStart, frameEnd ] ]
|
|
|
|
if animationNoiseTrack.hold:
|
|
'''
|
|
Okay, so this whole block of code is run if the track is supposed to apply holds during parts of the
|
|
animated noise. It does this by taking the incoming frame range, and then splitting it up into
|
|
buckets of valid frame ranges, removing the parts where a hold would occur. So the resulting list
|
|
would look something like below, where each item in the list is a valid frame range, and the missing
|
|
frames are where a hold occurs:
|
|
|
|
buckets = [ [ 1, 10 ], [ 20, 55 ], [ 65, 100 ] ]
|
|
'''
|
|
|
|
# Get a random hold duration.
|
|
holdDuration = random.randint( animationNoiseTrack.holdDurationMin, animationNoiseTrack.holdDurationMax )
|
|
|
|
# Create a valid frame range to work with for determining where the holds will occur. Start by reducing our
|
|
# frame range by the hold duration amount, to avoid going outside of the frame range.
|
|
adjustedFrameEnd = frameEnd - holdDuration
|
|
loopDuration = adjustedFrameEnd - frameStart
|
|
|
|
# Calculate the number of holds we will attempt to create based on a hold percentage over the valid frame range.
|
|
numHolds = int( ( ( loopDuration / holdDuration ) * animationNoiseTrack.holdPercentage ) / 100.0 )
|
|
|
|
# Start breaking the frame range up into buckets, leaving only ranges we want and removing hold frames.
|
|
buckets = [ [ frameStart, adjustedFrameEnd ] ]
|
|
|
|
for holdNum in range( numHolds ):
|
|
currentBucket = None
|
|
|
|
# Shuffle the buckets around randomly.
|
|
random.shuffle( buckets, random.random )
|
|
|
|
# Find a bucket large enough that we can split.
|
|
for bucket in buckets:
|
|
adjustedBucketDuration = ( bucket[ 1 ] - bucket[ 0 ] ) - holdDuration
|
|
|
|
if adjustedBucketDuration >= holdDuration:
|
|
currentBucket = bucket
|
|
break
|
|
|
|
# If we have a bucket we can split, do it.
|
|
if currentBucket:
|
|
start, end = currentBucket
|
|
adjustedEnd = end - holdDuration
|
|
|
|
# Randomly choose a start frame from inside of the current bucket.
|
|
holdStartFrame = random.randint( start, adjustedEnd )
|
|
holdEndFrame = holdStartFrame + holdDuration
|
|
|
|
# Split the bucket.
|
|
firstBucket = None
|
|
secondBucket = None
|
|
|
|
firstBucketDuration = ( holdStartFrame - 1 ) - start
|
|
|
|
if firstBucketDuration > 1:
|
|
firstBucket = [ start, holdStartFrame - 1 ]
|
|
|
|
secondBucketDuration = end - ( holdEndFrame + 1 )
|
|
|
|
if secondBucketDuration > 1:
|
|
secondBucket = [ holdEndFrame + 1, end ]
|
|
|
|
# If we have a new bucket, pop the current bucket from the list.
|
|
if firstBucket or secondBucket:
|
|
buckets.remove( currentBucket )
|
|
|
|
# Add the new split buckets.
|
|
if firstBucket:
|
|
buckets.append( firstBucket )
|
|
|
|
if secondBucket:
|
|
buckets.append( secondBucket )
|
|
|
|
# Put the buckets back in order.
|
|
buckets.sort( key = operator.itemgetter( 0 ) )
|
|
|
|
|
|
# Go through each bucket and apply the noise animation.
|
|
|
|
# TODO: Zero out first and last keyframes.
|
|
|
|
fcurve = animationNoiseTrack.animationNode.FCurve
|
|
fcurve.EditClear()
|
|
fcurve.EditBegin()
|
|
|
|
lastAmplitude = None
|
|
prevBucket = None
|
|
|
|
# Add initial key frame
|
|
addKey( fcurve, frameStart, 0 )
|
|
|
|
for bucket in buckets:
|
|
bucketFrameStart, bucketFrameEnd = bucket
|
|
|
|
lastFrameNum = 0
|
|
|
|
# Iterate over the current bucket's frame range and add keys for the noise animation.
|
|
for frameNum in range( bucketFrameStart, bucketFrameEnd, animationNoiseTrack.frameStep ):
|
|
|
|
# Skip first and last frames of the overall range.
|
|
if frameNum != frameStart and frameNum != frameEnd:
|
|
|
|
# Get a random amplitude value.
|
|
amplitude = random.uniform( animationNoiseTrack.amplitudeMin, animationNoiseTrack.amplitudeMax )
|
|
|
|
# Modulate the amplitude by a curve.
|
|
if animationNoiseTrack.curveType:
|
|
|
|
curveModulation = 0.0
|
|
|
|
# Normalize current frame number down to between 0 - 1 of the current range.
|
|
currentTime = ( float( frameNum ) - float( frameStart ) ) / ( float( frameEnd ) - float( frameStart ) )
|
|
|
|
# Linear
|
|
if animationNoiseTrack.curveType == CURVE_TYPE_LINEAR:
|
|
curveModulation = animationNoiseTrack.amplitudeMin * ( 1 - currentTime ) + animationNoiseTrack.amplitudeMax * currentTime
|
|
|
|
elif animationNoiseTrack.curveType == CURVE_TYPE_LINEAR_INVERTED:
|
|
curveModulation = animationNoiseTrack.amplitudeMax * ( 1 - currentTime ) + animationNoiseTrack.amplitudeMin * currentTime
|
|
|
|
# Exponential
|
|
elif animationNoiseTrack.curveType == CURVE_TYPE_EXPONENTIAL:
|
|
pass
|
|
|
|
amplitude *= curveModulation
|
|
|
|
|
|
# If we have a previous bucket, then set the amplitude to the last one used. Theoretically,
|
|
# we should only hit this case on the first frame of the next bucket.
|
|
if prevBucket:
|
|
amplitude = lastAmplitude
|
|
prevBucket = None
|
|
|
|
addKey( fcurve, frameNum, amplitude )
|
|
|
|
lastFrameNum = frameNum
|
|
lastAmplitude = amplitude
|
|
|
|
# Add a key on the last frame of the bucket if the frame step caused us to miss it.
|
|
#if lastFrameNum != prevBucket[ -1 ]:
|
|
# addKey( fcurve, bucketFrameEnd, 0.0 )
|
|
|
|
fcurve.EditEnd()
|
|
|
|
prevBucket = bucket
|
|
|
|
# Add final key frame.
|
|
addKey( fcurve, frameEnd, 0 )
|
|
|
|
# Setup frame range.
|
|
frameStart = 0
|
|
frameEnd = 0
|
|
|
|
if options.useActiveFrameRange:
|
|
frameStart = FBSystem().CurrentTake.LocalTimeSpan.GetStart().GetFrame( True )
|
|
frameEnd = FBSystem().CurrentTake.LocalTimeSpan.GetStop().GetFrame( True )
|
|
|
|
else:
|
|
frameStart = options.frameStart
|
|
frameEnd = options.frameEnd
|
|
|
|
if frameStart == frameEnd:
|
|
FBMessageBox( 'Rockstar', 'The frame range start and end cannot be the same!', 'OK' )
|
|
|
|
return False
|
|
|
|
# Create a new animation layer.
|
|
if options.createAnimationLayer:
|
|
currentTake = FBSystem().CurrentTake
|
|
|
|
currentTake.CreateNewLayer()
|
|
layerCount = currentTake.GetLayerCount()
|
|
currentTake.GetLayer( layerCount - 1 ).Name = options.animationLayerName
|
|
currentTake.SetCurrentLayer( layerCount - 1 )
|
|
|
|
# Apply to an existing animation layer.
|
|
else:
|
|
currentTake = FBSystem().CurrentTake
|
|
layerCount = currentTake.GetLayerCount()
|
|
|
|
for layerId in range( layerCount ):
|
|
layer = currentTake.GetLayer( layerId )
|
|
|
|
if layer.Name == options.animationLayerName:
|
|
currentTake.SetCurrentLayer( layerId )
|
|
|
|
# For each track, setup the animation node and apply noise animation.
|
|
if options.translation.enabled:
|
|
lclTranslationNode = modelObject.PropertyList.Find( 'Lcl Translation' )
|
|
lclTranslationNode.SetAnimated( True )
|
|
|
|
lclAnimNode = lclTranslationNode.GetAnimationNode()
|
|
|
|
if options.translation.x.enabled:
|
|
options.translation.x.animationNode = lclAnimNode.Nodes[ 0 ]
|
|
applyNoiseAnimation( frameStart, frameEnd, options.translation.x )
|
|
|
|
if options.translation.y.enabled:
|
|
options.translation.y.animationNode = lclAnimNode.Nodes[ 1 ]
|
|
applyNoiseAnimation( frameStart, frameEnd, options.translation.y )
|
|
|
|
if options.translation.z.enabled:
|
|
options.translation.z.animationNode = lclAnimNode.Nodes[ 2 ]
|
|
applyNoiseAnimation( frameStart, frameEnd, options.translation.z )
|
|
|
|
if options.rotation.enabled:
|
|
lclRotationNode = modelObject.PropertyList.Find( 'Lcl Rotation' )
|
|
lclRotationNode.SetAnimated( True )
|
|
|
|
lclAnimNode = lclRotationNode.GetAnimationNode()
|
|
|
|
if options.rotation.x.enabled:
|
|
options.rotation.x.animationNode = lclAnimNode.Nodes[ 0 ]
|
|
applyNoiseAnimation( frameStart, frameEnd, options.rotation.x )
|
|
|
|
if options.rotation.y.enabled:
|
|
options.rotation.y.animationNode = lclAnimNode.Nodes[ 1 ]
|
|
applyNoiseAnimation( frameStart, frameEnd, options.rotation.y )
|
|
|
|
if options.rotation.z.enabled:
|
|
options.rotation.z.animationNode = lclAnimNode.Nodes[ 2 ]
|
|
applyNoiseAnimation( frameStart, frameEnd, options.rotation.z )
|
|
|
|
if options.scale.enabled:
|
|
lclScaleNode = modelObject.PropertyList.Find( 'Lcl Scale' )
|
|
lclScaleNode.SetAnimated( True )
|
|
|
|
lclAnimNode = lclScaleNode.GetAnimationNode()
|
|
|
|
if options.scale.x.enabled:
|
|
options.scale.x.animationNode = lclAnimNode.Nodes[ 0 ]
|
|
applyNoiseAnimation( frameStart, frameEnd, options.scale.x )
|
|
|
|
if options.scale.y.enabled:
|
|
options.scale.y.animationNode = lclAnimNode.Nodes[ 1 ]
|
|
applyNoiseAnimation( frameStart, frameEnd, options.scale.y )
|
|
|
|
if options.scale.z.enabled:
|
|
options.scale.z.animationNode = lclAnimNode.Nodes[ 2 ]
|
|
applyNoiseAnimation( frameStart, frameEnd, options.scale.z )
|