Files
gtav-src/tools_ng/techart/dcc/motionbuilder2014/python/RS/Tools/AnimationNoise.py
T
2025-09-29 00:52:08 +02:00

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 )