Skip to content

Commit

Permalink
- Fixed an issue where the script would always iterate over material …
Browse files Browse the repository at this point in the history
…tracks.

- Added an option to choose between types of tracks before updating the track list or baking an object.  This greatly speeds up execution, especially if you only need to bake specific types of keys.

- Known issue where some tracks can be baked, but produce odd results.  Need to look into this more.
  • Loading branch information
TicTac-93 committed Jan 9, 2019
1 parent 32cd4c5 commit ebd627c
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 45 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,17 @@ A script for 3ds Max to bake any animated track, with options for every-nth-fram
Install by copying the bakeAllAnim folder to your 3ds Max scripts directory.
Run in 3ds Max using with the MaxScript snippet:
`python.ExecuteFile @"C:\path\to\bakeAllAnim\bakeAllAnim.py"`


-----

To use, run the script and select the objects you want to bake.

Set your frame range with the spinners at the top of the UI, optionally setting the "Nth Frame" value.

Select which types of tracks you want to bake from the "Track Options" menu.

Click "Get Tracks From Selection" and *uncheck* any tracks that you do *not* want to be baked.
Note that any tracks that are baked will lose animation outside of the frame range!

To actually bake the selected tracks, click "Get Baked". This may take a while if you have a large number of objects selected, so please be patient. The loading bar should update, but if the UI freezes there's not much I can do.
136 changes: 97 additions & 39 deletions bakeAllAnim.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __init__(self, ui_file, pymxs, parent=MaxPlus.GetQMaxMainWindow()):

# Titling

self._window_title = "Bake All Anim v01"
self._window_title = "Bake All Anim v1.1.0"
self.setWindowTitle(self._window_title)

# ---------------------------------------------------
Expand All @@ -92,6 +92,10 @@ def __init__(self, ui_file, pymxs, parent=MaxPlus.GetQMaxMainWindow()):
# Track Selection
self._box_tracks = self.findChild(QtW.QWidget, 'box_tracks')
self._btn_tracks = self.findChild(QtW.QPushButton, 'btn_updateTracks')
self._chk_transforms = self.findChild(QtW.QCheckBox, 'chk_transforms')
self._chk_modifiers = self.findChild(QtW.QCheckBox, 'chk_modifiers')
self._chk_visibility = self.findChild(QtW.QCheckBox, 'chk_visibility')
self._chk_materials = self.findChild(QtW.QCheckBox, 'chk_materials')

# Bake
self._btn_bake = self.findChild(QtW.QPushButton, 'btn_getBaked')
Expand Down Expand Up @@ -120,6 +124,10 @@ def __init__(self, ui_file, pymxs, parent=MaxPlus.GetQMaxMainWindow()):
'end': 100,
'nth': 1,
'pad': False,
'transforms': True,
'modifiers': True,
'visibility': False,
'materials': False,
'tracks': []}

# Label color vars
Expand All @@ -145,6 +153,44 @@ def _update_range(self):
# Update status label
self._lbl_status.setText("<font %s>Updated:</font> Frame Range" % self._grn)

def _get_settings(self, tracks=None, validate=None):
"""
Get settings from UI and update self._options. Optionally populate _options.tracks[] and validate settings.
:param tracks: Bool, populate .tracks[] if True
:param validate: Bool, validate settings if True
:return: Bool indicating success or failure
"""
# Update options from GUI
self._options['start'] = self._spn_start.value()
self._options['end'] = self._spn_end.value()
self._options['nth'] = self._spn_nth.value()
self._options['pad'] = self._chk_pad.isChecked()
self._options['transforms'] = self._chk_transforms.isChecked()
self._options['modifiers'] = self._chk_modifiers.isChecked()
self._options['visibility'] = self._chk_visibility.isChecked()
self._options['materials'] = self._chk_materials.isChecked()
self._options['tracks'] = []

# Pad frame range
if self._options['pad'] and ((self._options['end'] - self._options['start']) % self._options['nth']) > 0:
self._options['end'] += ((self._options['end'] - self._options['start']) % self._options['nth'])

# Get selected tracks
if tracks is True:
layout = self._box_tracks.layout()
for i in range(layout.count()):
widget = layout.itemAt(i).widget()
if widget and widget.isChecked():
self._options['tracks'].append(str(widget.text()))

# Validate options
if validate is True:
if self._options['start'] >= self._options['end']:
self._lbl_status.setText("<font %s>ERROR:</font> Start frame is after End!" % self._err)
return False

return True

def _update_tracks(self):
"""
Updates the Track Selection box with a list of unique track names.
Expand All @@ -153,10 +199,10 @@ def _update_tracks(self):
update_tracks_start = time.time()

rt = self._pymxs.runtime
tracks = []
layout = self._box_tracks.layout()

selection = rt.getCurrentSelection()
tracks = []
self._get_settings()
self._bar_progress.setMaximum(len(selection))
self._lbl_status.setText("Finding animated tracks...")

Expand All @@ -178,18 +224,24 @@ def _update_tracks(self):
widget.deleteLater()

index = 0
for track in tracks:
layout.addWidget(QtW.QCheckBox(track))
widget = layout.itemAt(index).widget()
widget.setChecked(True)
self._bar_progress.setValue(self._bar_progress.value()+1)
index += 1
if len(tracks) == 0:
self._lbl_status.setText("<font %s>Warning:</font> No animated tracks found!" % self._wrn)
self._bar_progress.setMaximum(1)
self._bar_progress.setValue(1)

else:
for track in tracks:
layout.addWidget(QtW.QCheckBox(track))
widget = layout.itemAt(index).widget()
widget.setChecked(True)
self._bar_progress.setValue(self._bar_progress.value()+1)
index += 1

layout.addStretch()
layout.addStretch()

self._lbl_status.setText("<font %s>Found:</font> %d Tracks in %d Objects" % (self._grn,
len(tracks),
len(rt.getCurrentSelection())))
self._lbl_status.setText("<font %s>Found:</font> %d Tracks in %d Objects" % (self._grn,
len(tracks),
len(rt.getCurrentSelection())))

# DEBUG TIMER
update_tracks_end = time.time()
Expand All @@ -206,28 +258,9 @@ def _bake(self):
at = self._pymxs.attime
animate = self._pymxs.animate

# Update options from GUI
self._options['start'] = self._spn_start.value()
self._options['end'] = self._spn_end.value()
self._options['nth'] = self._spn_nth.value()
self._options['pad'] = self._chk_pad.isChecked()
self._options['tracks'] = []
# Get selected tracks
layout = self._box_tracks.layout()
for i in range(layout.count()):
widget = layout.itemAt(i).widget()
if widget and widget.isChecked():
self._options['tracks'].append(str(widget.text()))

# Validate options
if self._options['start'] >= self._options['end']:
self._lbl_status.setText("<font %s>ERROR:</font> Start frame is after End!" % self._err)
if not self._get_settings(tracks=True, validate=True):
return

# Apply padding if needed
if self._options['pad'] and ((self._options['end'] - self._options['start']) % self._options['nth']) > 0:
self._options['end'] += ((self._options['end'] - self._options['start']) % self._options['nth'])

# Bake selected objects / tracks
with self._pymxs.undo(True, 'Bake Selection'), self._pymxs.redraw(False):
selection = rt.getCurrentSelection()
Expand All @@ -238,7 +271,6 @@ def _bake(self):
self._bar_progress.setMaximum(len(selection))

for obj in selection:

tracks = self._get_keyed_subtracks(obj)
if len(tracks) == 0:
self._bar_progress.setValue(self._bar_progress.value()+1)
Expand Down Expand Up @@ -277,35 +309,61 @@ def _bake(self):
# print "- Main Bake loop: %sms" % round((bake_end - bake_settings)*1000, 3)
print "Bake took %sms" % round((bake_end - bake_start)*1000, 3)

def _get_keyed_subtracks(self, track, list=None, namesOnly=None):
def _get_keyed_subtracks(self, track, list=None, namesOnly=None, firstCall=None):
# TODO: Add option to ignore material tracks, since they're not usually animated
"""
Crawl through track heirarchy, building a list of animated tracks. Exclude Position and Rotation parent tracks.
:param track: The track to start crawling from - also used when called recursively
:param list: The list of animated tracks, should start blank.
:param namesOnly: If true, only consider the track names for uniqueness.
:param firstCall: Set internally, if unset we'll call ourself recursively in the chosen tracks.
:return: A list of all (unique) animated track objects below the initial track.
"""
# Default values
if list is None:
list = []
if namesOnly is None:
namesOnly = False
if firstCall is None:
firstCall = True

rt = self._pymxs.runtime
ignore = ['Transform', 'Position', 'Rotation']

# DEBUG
# track_properties = rt.getPropNames(track)
# if track_properties is not None:
# print "Track: %s" % track.name
# print " -Parent: %s" % track.parent
# print " -Props: %s" % track_properties
# /DEBUG

# Only add this track to the list if it's a SubAnim, has a controller, and has been animated
if rt.iskindof(track, rt.SubAnim) and rt.iscontroller(track.controller) and track.isanimated:
if not namesOnly and track not in list and track.name not in ignore:
list.append(track)
elif namesOnly and track.name not in list and track.name not in ignore:
list.append(track.name)

# Always call self recursively on all children of this track
# Call recursively on selected tracks
# Track 1 is object visibility
# Track 3 is object transforms
# Track 4 is object modifiers
# Track 5 is object material
if firstCall:
if self._options['visibility'] and rt.getSubAnim(track, 1) is not None:
list = self._get_keyed_subtracks(rt.getSubAnim(track, 1), list, namesOnly, firstCall=False)
if self._options['transforms']:
list = self._get_keyed_subtracks(rt.getSubAnim(track, 3), list, namesOnly, firstCall=False)
if self._options['modifiers']:
list = self._get_keyed_subtracks(rt.getSubAnim(track, 4), list, namesOnly, firstCall=False)
if self._options['materials'] and rt.getSubAnim(track, 5) is not None:
list = self._get_keyed_subtracks(rt.getSubAnim(track, 5), list, namesOnly, firstCall=False)

# Note: SubAnim list is 1-indexed. Thanks, Autodesk.
for i in range(1, track.numsubs + 1):
list = self._get_keyed_subtracks(rt.getSubAnim(track, i), list, namesOnly)
else:
for i in range(1, track.numSubs + 1):
list = self._get_keyed_subtracks(rt.getSubAnim(track, i), list, namesOnly, firstCall=False)

return list

Expand All @@ -323,4 +381,4 @@ def _get_keyed_subtracks(self, track, list=None, namesOnly=None):
ui.show()

# DEBUG
# print "\rTest Version 22"
print "\rTest Version 49"
Loading

0 comments on commit ebd627c

Please sign in to comment.