def commKeyframeManipulator(): '''Code for manipulating keyframed parms of a selected node. Version 0.9 alpha. For the Houdini community. Authors: SWest (v0.9 alpha). https://creativecommons.org/licenses/by/4.0/''' # Code for some initial variables actions = ['offset'] inputs_validated = True log_dict_before = {} log_dict_after = {} keyframes_json_list = [] # Code for getting initerial user inputserial user inputs action = actions[0] # hard coded for now # Code for getting all the user's desired nodes to adjust def getSelectedNodes(): selected_nodes = hou.selectedNodes() inputs_validated = False if selected_nodes == (): message='Please select a node with keyframes and try again. ' hou.ui.displayMessage(message) else: inputs_validated = True return inputs_validated, selected_nodes # Code for getting user action def getSelectedAction(): '''A function used to present the user with a set of tools related to manipulating keyframes''' mesh_source_choice = hou.ui.displayMessage('Please choose desired action:', buttons=('Offset','Cancel'), \ default_choice=0, close_choice=-1) if mesh_source_choice == -1: action = 'cancel' if mesh_source_choice == 0: action = 'offset' if mesh_source_choice == 1: action = 'cancel' return action # Code for validating user inputs for intended data types def getUserOffset(): message = 'Please type a positive or negative number for frame offset. Example -10 or 15.5' offset = hou.ui.readInput(message, buttons=('OK',), severity=hou.severityType.Message, default_choice=0, close_choice=-1, help=None, title=None, initial_contents=None) inputs_validated = True try: offset = float(offset[1]) except: message='There was an error with the input number. ' hou.ui.displayMessage(message) inputs_validated = False return inputs_validated, offset # Code for getting all parms with keyframes from the user's selection def getParmsFromSelection(selected_nodes): '''Return a dict with the parm path and keyframes as a list using json''' parms_keyframes_before_dict = {} for node in selected_nodes: for parm in node.allParms(): parm_is_scoped = parm.isScoped() parm_keyframes_length = 0 try: parm_keyframes = parm.keyframes() except: break else: parm_keyframes_length=len(parm_keyframes) if(parm_keyframes_length > 0 and parm_is_scoped): keyframes_json_list = [] parm_keyframes_before = [] #parm_keyframes_after = [] for i in range(0,parm_keyframes_length): keyframe_dict=parm.keyframes()[i].asJSON(save_keys_in_frames=True) parm_keyframes_before.append(keyframe_dict.copy()) # store this parm with the list of keyframes parms_keyframes_before_dict["'"+parm.path()+"'"] = parm_keyframes_before return parms_keyframes_before_dict def getParmsKeyframesChanges(action_dict, parms_keyframes_before_dict): '''A function used to calculate the intended changes to keyframes''' parms_keyframes_after_dict = {} keyframes_after_list = [] action = action_dict['action'] if action == 'offset': offset = action_dict['offset'] for parm, keyframes in parms_keyframes_before_dict.items(): for keyframe in keyframes: keyframe_dict = keyframe.copy() if action == 'offset': frame_in=keyframe_dict['frame'] frame_out=frame_in+offset keyframe_dict['frame'] = frame_out keyframes_after_list.append(keyframe_dict.copy()) parms_keyframes_after_dict[parm] = keyframes_after_list return parms_keyframes_after_dict # Code for presenting the intended changes prior to performing them def showIntendedTasks(action_dict={}, parms_keyframes_before_dict={}, parms_keyframes_after_dict={}): parm_count = 1 report_str = '' action = action_dict['action'] if action == 'offset': offset = action_dict['offset'] report_str += f'action: {action}\n\n' report_str += f'offset: {offset}\n\n\n' for i, (parm, keyframes) in enumerate(parms_keyframes_before_dict.items()): keyframe_count = 1 report_str += (f'parm {parm_count}: {parm}\n\n') for k in range(0,len(keyframes)): report_str += f'keyframe {keyframe_count} before: {keyframes[k]}\n\n' report_str += f'keyframe {keyframe_count} after: {parms_keyframes_after_dict[parm][k]}\n\n' keyframe_count += 1 parm_count += 1 report_str += f'\n\n' hou.ui.displayMessage(report_str, title='Change log') # Code for performing the accepted changes after the user accepted them # Code for deleting a specified keyframe using frame def deleteKeyframe(frame): parm.deleteKeyframeAtFrame(frame) # Code for setting a new keyframe using a keyframe definition from a dict def setKeyframeFromDict(keyframe_dict): # a new keyframe from template keyframe_out = hou.Keyframe() # configure the new keyframe from the dict keyframe_out.fromJSON(keyframe_dict) # set a new keyframe parm.setKeyframe(keyframe_out) # main: # get user inputs inputs_validated, selected_nodes = getSelectedNodes() # get user action action_dict = {} action = getSelectedAction() if inputs_validated: action_dict['action'] = action if inputs_validated and action == 'offset': inputs_validated, offset = getUserOffset() action_dict['offset'] = offset # get parms and keyframes if inputs_validated: parms_keyframes_before_dict = getParmsFromSelection(selected_nodes) # calculate intended changes into parms_keyframes_after_dict if inputs_validated: parms_keyframes_after_dict = getParmsKeyframesChanges(action_dict, parms_keyframes_before_dict) # present intended changes (ok, cancel) if inputs_validated: showIntendedTasks(action_dict, parms_keyframes_before_dict, parms_keyframes_after_dict) # todo: ok, cancel buttons # perform changes commKeyframeManipulator()
Keyframe manipulator (community edition)
2376 6 3- SWest
- Member
- 311 posts
- Joined: Oct. 2016
- Offline
Please test this tool on your system. At the moment it does not do anything. The first milestone will be an offset tool. However, it should be possible to extend it to do various other tasks. Bug reports, RFE:s as well as contributions are welcome in return for credits. Have fun and cheers!
Interested in character concepts, modeling, rigging, and animation. Related tool dev with Py and VEX.
- CYTE
- Member
- 679 posts
- Joined: Feb. 2017
- Offline
- SWest
- Member
- 311 posts
- Joined: Oct. 2016
- Offline
CYTE
Hey SWest,
Great idea to develop a tool to deal with keyframes! On my whishlist would also be a button to clear channels with zero value change.
Cheers
CYTE
Hi there. Yes indeed. There exist several methods for things like deleting or setting keyframes in the Houdini API (HOM) already. However, reusing them for tedious repetitive tasks could probably be made quicker and easier, for example applying something on all parms in one go. Most of the code is just for adding some error handling and user friendliness.
def commKeyframeManipulator(): """Code for manipulating keyframed parms of a selected node. It utilizes the Houdini Object Model (HOM) included in SideFX Houdini. It is made for the Houdini community. Current version: 0.9.2 alpha. Authors: SWest (v0.9.2 alpha). License: https://creativecommons.org/licenses/by/4.0""" # Code for some initial variables actions = ('offset','clear','cancel') inputs_valid = True def getNodes(): """A function for getting all the user's desired nodes to adjust.""" selected_nodes = hou.selectedNodes() inputs_valid = False if selected_nodes == (): message = 'Please select a node with keyframes and try again. ' hou.ui.displayMessage(message) else: inputs_valid = True return inputs_valid, selected_nodes def getAction(actions): """A function used to present the user with a set of tools related to manipulating keyframes. The user's choice is returned.""" choice = hou.ui.displayMessage('Please choose desired action:', buttons=actions, default_choice=0, close_choice=0) inputs_valid = True action = actions[choice] if action == 'cancel': inputs_valid = False return inputs_valid, action def getRange(): """A function used to let the user set a selection range. It accepts text input such as 0-99 and returns a tuple like (0,99). Both values are inclusive so that they will be used.""" range_tuple = () choice = 0 message = """Please type a frame range. \n\n Example: 0-99""" choice, range_str = hou.ui.readInput(message, buttons=('OK','Cancel'), severity=hou.severityType.Message, default_choice=0, close_choice=1, help=None, title=None, initial_contents='0-10000') inputs_valid = True try: range_list = range_str.split('-') range_tuple = (float(range_list[0]),float(range_list[1])) except: message='There was an error with the input range text. ' hou.ui.displayMessage(message) inputs_valid = False if choice == 1: # User clicked Cancel inputs_valid = False return inputs_valid, range_tuple def getOffset(): """A function used getting a users choice and validating inputs for intended data types.""" message = """Please type a positive or negative number for frame offset.\n\n Example: -10 or 15.5""" offset = hou.ui.readInput(message, buttons=('OK',), severity=hou.severityType.Message, default_choice=0, close_choice=-1, help=None, title='offset', initial_contents=None) inputs_valid = True try: offset = float(offset[1]) except: message='There was an error with the input number. ' hou.ui.displayMessage(message) inputs_valid = False return inputs_valid, offset def getParmsFromRange(action_dict, selected_nodes): """A function used for getting all parms with keyframes from the user's selection. It will return a dict with the parm path and keyframes as a list using json""" parms_input_dict = {} range_start, range_end = action_dict['selection_range'] for node in selected_nodes: for parm in node.allParms(): parm_is_scoped = parm.isScoped() parm_keyframes_length = 0 try: parm_keyframes = parm.keyframes() except: break else: parm_keyframes_length=len(parm_keyframes) if(parm_keyframes_length > 0 and parm_is_scoped): keyframes_input = [] for i in range(0,parm_keyframes_length): keyframe_dict=parm.keyframes()[i].asJSON(save_keys_in_frames=True) # check if the frame is in the selction range frame_in=keyframe_dict['frame'] if ( frame_in >= range_start) and (frame_in <= range_end): keyframes_input.append(keyframe_dict.copy()) if len(keyframes_input) > 0: parms_input_dict[parm.path()] = keyframes_input return parms_input_dict def getChanges(action_dict, parms_input_dict): """A function used to calculate the intended changes to keyframes. It support different actions (tools). """ parms_output_dict = {} keyframes_output_list = [] action = action_dict['action'] if action == 'offset': offset = action_dict['offset'] range_start, range_end = action_dict['selection_range'] for parm, keyframes in parms_input_dict.items(): keyframes_output_list = [] for keyframe in keyframes: keyframe_dict = keyframe.copy() frame_in=keyframe_dict['frame'] if ( frame_in >= range_start) and (frame_in <= range_end): if action == 'offset': frame_out=frame_in+offset keyframe_dict['frame'] = frame_out keyframes_output_list.append(keyframe_dict.copy()) if action == 'clear': if keyframe_dict not in keyframes_output_list: keyframes_output_list.append(keyframe_dict.copy()) parms_output_dict[parm] = keyframes_output_list return parms_output_dict def showIntentions(action_dict={}, parms_input_dict={}, parms_output_dict={}): """A function used to show the proposed changes based on the user input. The user may proceed with OK or cancel the action. """ action = action_dict['action'] range_start, range_end = action_dict['selection_range'] inputs_valid = True report_str = '' report_str += f'action: {action}\n\n' report_str += f'selection range: {range_start}-{range_end}\n\n' if len(parms_input_dict.items()) > 0: is_keyframes = True else: is_keyframes = False # distinguish report type if is_keyframes: parm_count = 1 if action == 'offset': offset = action_dict['offset'] report_str += f'offset: {offset}\n\n' report_str += f'Parms and keyframes: \n\n\n' for i, (parm, keyframes) in enumerate(parms_input_dict.items()): keyframe_count = 1 report_str += (f'parm {parm_count}: {parm}\n\n') for k in range(0,len(keyframes)): report_str += f'keyframe {keyframe_count}\n\n' report_str += f'from: {keyframes[k]}\n\n' report_str += f'to: {parms_output_dict[parm][k]}\n\n' keyframe_count += 1 parm_count += 1 report_str += f'\n\n' if action == 'clear': report_str += f'Keyframes to be deleted: \n\n\n' for i, (parm, keyframes) in enumerate(parms_input_dict.items()): keyframe_count = 1 report_str += (f'parm {parm_count}: {parm}\n\n') #if len(keyframes) > 1: for k in range(0,len(keyframes)): report_str += f'keyframe {keyframe_count}\n' report_str += f'{parms_output_dict[parm][k]}\n\n' keyframe_count += 1 parm_count += 1 report_str += f'\n\n' choice = hou.ui.displayMessage(report_str, title='Proposed changes', buttons=('OK','Cancel')) if choice == 1: inputs_valid = False # no keyframes else: report_str += f'No keyframes are found in the selection. \n\n\n' inputs_valid = False choice = hou.ui.displayMessage(report_str, title='Proposed changes', buttons=('OK','Cancel')) if choice == 1: inputs_valid = False return inputs_valid def setSingleKeyframe(parm_str, keyframe_dict): """A function used to set a new keyframe using a definition from a dict.""" # a new keyframe from template keyframe_out = hou.Keyframe() # configure the new keyframe from the dict keyframe_out.fromJSON(keyframe_dict) # set a new keyframe hou.parm(parm_str).setKeyframe(keyframe_out) def deleteKeyframes(input_dict): """A function used to delete keyframes from a dict.""" for parm_str, keyframes in input_dict.items(): for keyframe_dict in keyframes: frame_out = keyframe_dict['frame'] hou.parm(parm_str).deleteKeyframeAtFrame(frame_out) def setKeyframes(input_dict): """A function used to set keyframes from a dict.""" for parm_str, keyframes in input_dict.items(): for keyframe_dict in keyframes: setSingleKeyframe(parm_str, keyframe_dict) def makeChanges(action_dict={}, parms_input_dict={}, parms_output_dict={}): """A function used to make actual changes to keyframes specified in the input dictionary. All inputs should already have been validated. The proposed changes should be accepted by the user before this stage.""" action = action_dict['action'] range_start, range_end = action_dict['selection_range'] if action == 'offset': offset = action_dict['offset'] deleteKeyframes(parms_input_dict) setKeyframes(parms_output_dict) if action == 'clear': deleteKeyframes(parms_output_dict) # main: # get user inputs inputs_valid, selected_nodes = getNodes() # get user action action_dict = {} inputs_valid, action = getAction(actions) if inputs_valid: action_dict['action'] = action # get user selection range if inputs_valid: inputs_valid, selection_range = getRange() action_dict['selection_range'] = selection_range # get various inputs based on selected action if inputs_valid and action == 'offset': inputs_valid, offset = getOffset() action_dict['offset'] = offset elif inputs_valid and action == 'clear': pass else: inputs_valid = False # get parms and keyframes from user selection if inputs_valid: parms_input_dict = getParmsFromRange(action_dict, selected_nodes) # calculate intended changes into parms_output_dict if inputs_valid: parms_output_dict = getChanges(action_dict, parms_input_dict) # present intended changes if inputs_valid: inputs_valid = showIntentions(action_dict, parms_input_dict, parms_output_dict) # perform changes if inputs_valid: makeChanges(action_dict, parms_input_dict, parms_output_dict) commKeyframeManipulator()
Edit: Improving the readability and a minor bug fix
Edited by SWest - April 2, 2023 14:14:41
Interested in character concepts, modeling, rigging, and animation. Related tool dev with Py and VEX.
- SWest
- Member
- 311 posts
- Joined: Oct. 2016
- Offline
Changes:
1. Investigating a "refit" method and another type of UI window. Here only the user interaction is tested. The feature itself is not yet implemented.
1. Investigating a "refit" method and another type of UI window. Here only the user interaction is tested. The feature itself is not yet implemented.
def commKeyframeManipulator(): """Code for manipulating keyframed parms of a selected node. It utilizes the Houdini Object Model (HOM) included in SideFX Houdini. It is made for the Houdini community. Current version: 0.9.3 alpha. Authors: SWest (v0.9.3 alpha). License: https://creativecommons.org/licenses/by/4.0""" # Code for some initial variables actions = ('offset','clear','refit','cancel') inputs_valid = True def getNodes(): """A function for getting all the user's desired nodes to adjust.""" selected_nodes = hou.selectedNodes() inputs_valid = False if selected_nodes == (): message = 'Please select a node with keyframes and try again. ' hou.ui.displayMessage(message) else: inputs_valid = True return inputs_valid, selected_nodes def getAction(actions): """A function used to present the user with a set of tools related to manipulating keyframes. The user's choice is returned.""" choice = hou.ui.displayMessage('Please choose desired action:', buttons=actions, default_choice=0, close_choice=0) inputs_valid = True action = actions[choice] if action == 'cancel': inputs_valid = False return inputs_valid, action def getRange(): """A function used to let the user set a selection range. It accepts text input such as 0-99 and returns a tuple like (0,99). Both values are inclusive so that they will be used.""" range_tuple = () choice = 0 message = """Please type a frame range. \n\n Example: 0-99""" choice, range_str = hou.ui.readInput(message, buttons=('OK','Cancel'), severity=hou.severityType.Message, default_choice=0, close_choice=1, help=None, title=None, initial_contents='0-10000') inputs_valid = True try: range_list = range_str.split('-') range_tuple = (float(range_list[0]),float(range_list[1])) except: message='There was an error with the input range text. ' hou.ui.displayMessage(message) inputs_valid = False if choice == 1: # User clicked Cancel inputs_valid = False return inputs_valid, range_tuple def getOffset(): """A function used getting a users choice and validating inputs for intended data types.""" message = """Please type a positive or negative number for frame offset.\n\n Example: -10 or 15.5""" offset = hou.ui.readInput(message, buttons=('OK',), severity=hou.severityType.Message, default_choice=0, close_choice=-1, help=None, title='offset', initial_contents=None) inputs_valid = True try: offset = float(offset[1]) except: message='There was an error with the input number. ' hou.ui.displayMessage(message) inputs_valid = False return inputs_valid, offset def getParmsFromRange(action_dict, selected_nodes): """A function used for getting all parms with keyframes from the user's selection. It will return a dict with the parm path and keyframes as a list using json""" parms_input_dict = {} range_start, range_end = action_dict['selection_range'] for node in selected_nodes: for parm in node.allParms(): parm_is_scoped = parm.isScoped() parm_keyframes_length = 0 try: parm_keyframes = parm.keyframes() except: break else: parm_keyframes_length=len(parm_keyframes) if(parm_keyframes_length > 0 and parm_is_scoped): keyframes_input = [] for i in range(0,parm_keyframes_length): keyframe_dict=parm.keyframes()[i].asJSON(save_keys_in_frames=True) # check if the frame is in the selction range frame_in=keyframe_dict['frame'] if ( frame_in >= range_start) and (frame_in <= range_end): keyframes_input.append(keyframe_dict.copy()) if len(keyframes_input) > 0: parms_input_dict[parm.path()] = keyframes_input return parms_input_dict def getRefit(action_dict): """A function used to get the necessary inputs for the refit action. Todo: extract snippets to reusable functions. """ title = 'Refit settings' message = 'Refit settings' input_labels = ('refit','refit_tol','refit_preserve_extrema', 'refit_bezier','resample','resample_rate', 'resample_tol','range','range_start','range_end', 'bake_chop') initial_contents = ('True','1.0','False', 'True','False','1.0', '1.0','True','1','100', '1') help = """ refit True: cubic refitting False: only resampling or range but no refitting refit_tol: refit tolerance in absolute value refit_preserve_extrema: preserves keys that are local minima or maxima refit_bezier True: new keyframes use bezier() False: new keyframes use cubic() resample True: Resampling is done before refitting False: No resampling resample_rate: 1.0 adds a keyframe for each frame. resample_tol: 1.0 meaning no resampling for subframes. range True: use range_start and range_end False: use first and last keyframe range_start: Start frame range_end: End frame bake_chop: an enumeration value """ buttons = ('OK','Cancel') severity = hou.severityType.Message choice,inputs = hou.ui.readMultiInput(message=message, input_labels=input_labels, password_input_indices=(), buttons=buttons, severity=severity, default_choice=0, close_choice=-1, help=help, title=title, initial_contents=initial_contents) print(f'choice: {choice} inputs: {inputs}') inputs_valid = False # temp if not choice == 0: inputs_valid = False # todo: # add input_labels to action_dict keys # take the output string and add this to the action_dict choice = hou.ui.displayMessage('refit is not yet fully implemented', title='Notification', buttons=('OK',)) return inputs_valid, action_dict def getChanges(action_dict, parms_input_dict): """A function used to calculate the intended changes to keyframes. It support different actions (tools). """ parms_output_dict = {} keyframes_output_list = [] action = action_dict['action'] if action == 'offset': offset = action_dict['offset'] range_start, range_end = action_dict['selection_range'] for parm, keyframes in parms_input_dict.items(): keyframes_output_list = [] for keyframe in keyframes: keyframe_dict = keyframe.copy() frame_in=keyframe_dict['frame'] if ( frame_in >= range_start) and (frame_in <= range_end): if action == 'offset': frame_out=frame_in+offset keyframe_dict['frame'] = frame_out keyframes_output_list.append(keyframe_dict.copy()) if action == 'clear': if keyframe_dict not in keyframes_output_list: keyframes_output_list.append(keyframe_dict.copy()) parms_output_dict[parm] = keyframes_output_list return parms_output_dict def showIntentions(action_dict={}, parms_input_dict={}, parms_output_dict={}): """A function used to show the proposed changes based on the user input. The user may proceed with OK or cancel the action. """ action = action_dict['action'] range_start, range_end = action_dict['selection_range'] inputs_valid = True report_str = '' report_str += f'action: {action}\n\n' report_str += f'selection range: {range_start}-{range_end}\n\n' if len(parms_input_dict.items()) > 0: is_keyframes = True else: is_keyframes = False # distinguish report type if is_keyframes: parm_count = 1 if action == 'offset': offset = action_dict['offset'] report_str += f'offset: {offset}\n\n' report_str += f'Parms and keyframes: \n\n\n' for i, (parm, keyframes) in enumerate(parms_input_dict.items()): keyframe_count = 1 report_str += (f'parm {parm_count}: {parm}\n\n') for k in range(0,len(keyframes)): report_str += f'keyframe {keyframe_count}\n\n' report_str += f'from: {keyframes[k]}\n\n' report_str += f'to: {parms_output_dict[parm][k]}\n\n' keyframe_count += 1 parm_count += 1 report_str += f'\n\n' if action == 'clear': report_str += f'Keyframes to be deleted: \n\n\n' for i, (parm, keyframes) in enumerate(parms_input_dict.items()): keyframe_count = 1 report_str += (f'parm {parm_count}: {parm}\n\n') #if len(keyframes) > 1: for k in range(0,len(keyframes)): report_str += f'keyframe {keyframe_count}\n' report_str += f'{parms_output_dict[parm][k]}\n\n' keyframe_count += 1 parm_count += 1 report_str += f'\n\n' choice = hou.ui.displayMessage(report_str, title='Proposed changes', buttons=('OK','Cancel')) if choice == 1: inputs_valid = False # no keyframes else: report_str += f'No keyframes are found in the selection. \n\n\n' inputs_valid = False choice = hou.ui.displayMessage(report_str, title='Proposed changes', buttons=('OK','Cancel')) if choice == 1: inputs_valid = False return inputs_valid def setSingleKeyframe(parm_str, keyframe_dict): """A function used to set a new keyframe using a definition from a dict.""" # a new keyframe from template keyframe_out = hou.Keyframe() # configure the new keyframe from the dict keyframe_out.fromJSON(keyframe_dict) # set a new keyframe hou.parm(parm_str).setKeyframe(keyframe_out) def deleteKeyframes(input_dict): """A function used to delete keyframes from a dict.""" for parm_str, keyframes in input_dict.items(): for keyframe_dict in keyframes: frame_out = keyframe_dict['frame'] hou.parm(parm_str).deleteKeyframeAtFrame(frame_out) def setKeyframes(input_dict): """A function used to set keyframes from a dict.""" for parm_str, keyframes in input_dict.items(): for keyframe_dict in keyframes: setSingleKeyframe(parm_str, keyframe_dict) def makeChanges(action_dict, parms_input_dict, parms_output_dict): """A function used to make actual changes to keyframes specified in the input dictionary. All inputs should already have been validated. The proposed changes should be accepted by the user before this stage.""" action = action_dict['action'] range_start, range_end = action_dict['selection_range'] if action == 'offset': offset = action_dict['offset'] deleteKeyframes(parms_input_dict) setKeyframes(parms_output_dict) if action == 'clear': deleteKeyframes(parms_output_dict) if action == 'refit': pass #deleteKeyframes(parms_input_dict) #setKeyframes(parms_output_dict) # main: # get user inputs inputs_valid, selected_nodes = getNodes() # get user action action_dict = {} inputs_valid, action = getAction(actions) if inputs_valid: action_dict['action'] = action # get various inputs based on selected action if inputs_valid and action == 'offset': inputs_valid, offset = getOffset() action_dict['offset'] = offset elif inputs_valid and action == 'clear': pass elif inputs_valid and action == 'refit': inputs_valid, action_dict = getRefit(action_dict) else: inputs_valid = False # get user selection range if inputs_valid: inputs_valid, selection_range = getRange() action_dict['selection_range'] = selection_range # get parms and keyframes from user selection if inputs_valid: parms_input_dict = getParmsFromRange(action_dict, selected_nodes) # calculate intended changes into parms_output_dict if inputs_valid: parms_output_dict = getChanges(action_dict, parms_input_dict) # present intended changes if inputs_valid: inputs_valid = showIntentions(action_dict, parms_input_dict, parms_output_dict) # perform changes if inputs_valid: makeChanges(action_dict, parms_input_dict, parms_output_dict) commKeyframeManipulator()
Edited by SWest - April 13, 2023 07:22:47
Interested in character concepts, modeling, rigging, and animation. Related tool dev with Py and VEX.
- SWest
- Member
- 311 posts
- Joined: Oct. 2016
- Offline
Changes:
1. A small update to the UI so fewer input windows are used.
2. The "clear" action will now reset scoped parms to default. To avoid unexpected changes to parms there must be a method to identify which parms are used for animation. For that hou.parm(parm_path).isScoped() can be used. Then it is possible to easily control what parms or set of parms are cleared.
Extra: If you merely want to clear channels without limiting the range you can simply use shift+ctrl+LMB and ctrl+MMB on a group label for parms. At least one group is needed because the tool need to know what parms you are dealing with. A video is added to show this.
1. A small update to the UI so fewer input windows are used.
2. The "clear" action will now reset scoped parms to default. To avoid unexpected changes to parms there must be a method to identify which parms are used for animation. For that hou.parm(parm_path).isScoped() can be used. Then it is possible to easily control what parms or set of parms are cleared.
Extra: If you merely want to clear channels without limiting the range you can simply use shift+ctrl+LMB and ctrl+MMB on a group label for parms. At least one group is needed because the tool need to know what parms you are dealing with. A video is added to show this.
def commKeyframeManipulator(): """Code for manipulating keyframed parms of one or many selected node(s). Keyframes are expected to have been promoted to the selected node. That is why this function only look for keyframes on the selected node(s) parms. It utilizes the Houdini Object Model (HOM) included in SideFX Houdini. It is made for the Houdini community. Current version: 0.9.5 alpha. Authors: SWest (v0.9.5 alpha). License: https://creativecommons.org/licenses/by/4.0""" # Code for some initial variables actions = ('offset','clear','refit','cancel') inputs_valid = True def getNodes(): """A function for getting all the user's desired nodes to adjust.""" selected_nodes = hou.selectedNodes() inputs_valid = False if selected_nodes == (): message = 'Please select a node with keyframes and try again. ' hou.ui.displayMessage(message) else: inputs_valid = True return inputs_valid, selected_nodes def getAction(actions): """A function used to present the user with a set of tools related to manipulating keyframes. The user's choice is returned.""" choice = hou.ui.displayMessage('Please choose desired action:', buttons=actions, default_choice=0, close_choice=0) inputs_valid = True action = actions[choice] if action == 'cancel': inputs_valid = False return inputs_valid, action def getOffset(action_dict): """A function used to get the necessary inputs for the offset action. This include offset value and range. Todo: extract snippets to reusable functions. """ title = 'Offset' message = 'Offset settings' temp_dict = { 'offset': '1.0', 'range_start': '1', 'range_end': '10000' } input_labels = () for k,v in temp_dict.items(): input_labels += (k,) initial_contents = () for v in temp_dict.values(): initial_contents += (v,) help = """ Help for offset: offset A positive or negative number for frame offset. Example: -10 or 15.5 (float) range range_start: Start frame (float) range_end: End frame (float) """ buttons = ('OK','Cancel') severity = hou.severityType.Message choice, user_contents = hou.ui.readMultiInput(message=message, input_labels=input_labels, password_input_indices=(), buttons=buttons, severity=severity, default_choice=0, close_choice=-1, help=help, title=title, initial_contents=initial_contents) inputs_valid = True try: action_dict['offset'] = float(user_contents[0]) except: inputs_valid = False message = 'The offset value is not valid. ' try: action_dict['range_start'] = float(user_contents[1]) except: inputs_valid = False message = 'The range start value is not valid. ' try: action_dict['range_end'] = float(user_contents[2]) message = 'The range end value is not valid. ' except: inputs_valid = False if not inputs_valid: temp = hou.ui.displayMessage('Input error: '+message, title='Notification', buttons=('OK',)) if not choice == 0: inputs_valid = False # print the settings (debug) # for k,v in action_dict.items(): # print(f'{k} {v} {type(v)}') return inputs_valid, action_dict def getClear(action_dict): """A function used to get the necessary inputs for the clear action. This include range. Todo: extract snippets to reusable functions. """ title = 'Clear' message = 'Settings for the Clear action' temp_dict = { 'range_start': '1', 'range_end': '10000' } input_labels = () for k,v in temp_dict.items(): input_labels += (k,) initial_contents = () for v in temp_dict.values(): initial_contents += (v,) help = """ Help for Clear: Scoped parms or channels will be reset to default. Parms or channels can be categorized with tabs (folders) in the properties UI. To set parms as scoped shift+MMB on a UI folder or parm label. To disable a parm as scoped shift+ctrl+MMB on a tab or a parm label. range range_start: Start frame (float) range_end: End frame (float) """ buttons = ('OK','Cancel') severity = hou.severityType.Message choice, user_contents = hou.ui.readMultiInput(message=message, input_labels=input_labels, password_input_indices=(), buttons=buttons, severity=severity, default_choice=0, close_choice=-1, help=help, title=title, initial_contents=initial_contents) inputs_valid = True try: action_dict['range_start'] = float(user_contents[0]) except: inputs_valid = False message = 'The range start value is not valid. ' try: action_dict['range_end'] = float(user_contents[1]) except: inputs_valid = False message = 'The range end value is not valid. ' if not inputs_valid: temp = hou.ui.displayMessage('Input error: '+message, title='Notification', buttons=('OK',)) if not choice == 0: inputs_valid = False return inputs_valid, action_dict def getRefit(action_dict): """A function used to get the necessary inputs for the refit action. Todo: extract snippets to reusable functions. """ title = 'Refit' message = 'Refit settings' refit_dict = { 'refit': 'True', 'refit_tol': '1.0', 'refit_preserve_extrema': 'False', 'refit_bezier': 'True', 'resample': 'False', 'resample_rate': '1.0', 'resample_tol': '1.0', 'range': 'True', 'range_start': '1', 'range_end': '100', 'bake_chop': '1' } input_labels = () for k,v in refit_dict.items(): input_labels += (k,) initial_contents = () for v in refit_dict.values(): initial_contents += (str(v),) help = """ Help for refit: refit True: cubic refitting False: only resampling or range but no refitting refit_tol: refit tolerance in absolute value refit_preserve_extrema: preserves keys that are local minima or maxima refit_bezier True: new keyframes use bezier() False: new keyframes use cubic() resample True: Resampling is done before refitting False: No resampling resample_rate: 1.0 adds a keyframe for each frame. resample_tol: 1.0 meaning no resampling for subframes. range True: use range_start and range_end False: use first and last keyframe range_start: Start frame range_end: End frame bake_chop: an enumeration value """ buttons = ('OK','Cancel') severity = hou.severityType.Message choice, user_contents = hou.ui.readMultiInput(message=message, input_labels=input_labels, password_input_indices=(), buttons=buttons, severity=severity, default_choice=0, close_choice=-1, help=help, title=title, initial_contents=initial_contents) # for i, (k,v) in enumerate(refit_dict.items()): # refit_dict[k] = user_contents[i] # validate inputs inputs_valid = True # something is wrong with the following try: action_dict['refit'] = bool(user_contents[0]) action_dict['refit_tol'] = float(user_contents[1]) action_dict['refit_preserve_extrema'] = bool(user_contents[2]) action_dict['refit_bezier'] = bool(user_contents[3]) action_dict['resample'] = bool(user_contents[4]) action_dict['resample_rate'] = float(user_contents[5]) action_dict['resample_tol'] = float(user_contents[6]) action_dict['range'] = bool(user_contents[7]) action_dict['range_start'] = float(user_contents[8]) action_dict['range_end'] = float(user_contents[9]) action_dict['bake_chop'] = float(user_contents[10]) except: inputs_valid = False else: for k,v in action_dict.items(): print(f'{k} {v}') # action_dict.update(refit_dict) if not choice == 0: inputs_valid = False temp = hou.ui.displayMessage('refit is not yet fully implemented', title='Notification', buttons=('OK',)) return inputs_valid, action_dict def getParmsFromRange(action_dict, selected_nodes): """A function used for getting all parms with keyframes from the user's selection. It will return a dict with the parm path and keyframes as a list using json""" parms_input_dict = {} range_start = action_dict['range_start'] range_end = action_dict['range_end'] for node in selected_nodes: for parm in node.allParms(): parm_is_scoped = parm.isScoped() parm_keyframes_length = 0 try: parm_keyframes = parm.keyframes() except: break else: parm_keyframes_length=len(parm_keyframes) if(parm_keyframes_length > 0 and parm_is_scoped): keyframes_input = [] for i in range(0,parm_keyframes_length): keyframe_dict=parm.keyframes()[i].asJSON(save_keys_in_frames=True) frame_in=keyframe_dict['frame'] if ( frame_in >= range_start) and (frame_in <= range_end): keyframes_input.append(keyframe_dict.copy()) if len(keyframes_input) > 0: parms_input_dict[parm.path()] = keyframes_input return parms_input_dict def getChanges(action_dict, parms_input_dict): """A function used to calculate the intended changes to keyframes. It support different actions (tools). """ parms_output_dict = {} keyframes_output_list = [] action = action_dict['action'] if action == 'offset': offset = action_dict['offset'] range_start = action_dict['range_start'] range_end = action_dict['range_end'] for parm, keyframes in parms_input_dict.items(): keyframes_output_list = [] for keyframe in keyframes: keyframe_dict = keyframe.copy() frame_in=keyframe_dict['frame'] if ( frame_in >= range_start) and (frame_in <= range_end): if action == 'offset': frame_out=frame_in+offset keyframe_dict['frame'] = frame_out keyframes_output_list.append(keyframe_dict.copy()) if action == 'clear': if keyframe_dict not in keyframes_output_list: keyframes_output_list.append(keyframe_dict.copy()) parms_output_dict[parm] = keyframes_output_list return parms_output_dict def showIntentions(action_dict={}, parms_input_dict={}, parms_output_dict={}): """A function used to show the proposed changes based on the user input. The user may proceed with OK or cancel the action. """ action = action_dict['action'] range_start = action_dict['range_start'] range_end = action_dict['range_end'] inputs_valid = True report_str = '' report_str += f'action: {action}\n\n' report_str += f'selection range: {range_start}-{range_end}\n\n' if len(parms_input_dict.items()) > 0: is_keyframes = True else: is_keyframes = False # distinguish report type if is_keyframes: parm_count = 1 if action == 'offset': offset = action_dict['offset'] report_str += f'offset: {offset}\n\n' report_str += f'Parms and keyframes: \n\n\n' for i, (parm, keyframes) in enumerate(parms_input_dict.items()): keyframe_count = 1 report_str += (f'parm {parm_count}: {parm}\n\n') for k in range(0,len(keyframes)): report_str += f'keyframe {keyframe_count}\n\n' report_str += f'from: {keyframes[k]}\n\n' report_str += f'to: {parms_output_dict[parm][k]}\n\n' keyframe_count += 1 parm_count += 1 report_str += f'\n\n' if action == 'clear': report_str += f'Keyframes to be deleted: \n\n\n' for i, (parm, keyframes) in enumerate(parms_input_dict.items()): keyframe_count = 1 report_str += (f'parm {parm_count}: {parm}\n\n') #if len(keyframes) > 1: for k in range(0,len(keyframes)): report_str += f'keyframe {keyframe_count}\n' report_str += f'{parms_output_dict[parm][k]}\n\n' keyframe_count += 1 parm_count += 1 report_str += f'\n\n' choice = hou.ui.displayMessage(report_str, title='Proposed changes', buttons=('OK','Cancel')) if choice == 1: inputs_valid = False # no keyframes else: report_str += f'No keyframes are found in the selection. \n\n\n' inputs_valid = False choice = hou.ui.displayMessage(report_str, title='Proposed changes', buttons=('OK','Cancel')) if choice == 1: inputs_valid = False return inputs_valid def setSingleKeyframe(parm_str, keyframe_dict): """A function used to set a new keyframe using a definition from a dict.""" # a new keyframe from template keyframe_out = hou.Keyframe() # configure the new keyframe from the dict keyframe_out.fromJSON(keyframe_dict) # set a new keyframe hou.parm(parm_str).setKeyframe(keyframe_out) def deleteKeyframes(input_dict): """A function used to delete keyframes from a dict.""" for parm_str, keyframes in input_dict.items(): for keyframe_dict in keyframes: frame_out = keyframe_dict['frame'] hou.parm(parm_str).deleteKeyframeAtFrame(frame_out) def setKeyframes(input_dict): """A function used to set keyframes from a dict.""" for parm_str, keyframes in input_dict.items(): for keyframe_dict in keyframes: setSingleKeyframe(parm_str, keyframe_dict) def setScopedParmsToDefault(selected_nodes): """A function used to set parms to default. It is limited to scoped parms. It must be so otherwise all parms are reset to default and that might lead to undesired results.""" for node in selected_nodes: for parm in node.allParms(): parm_is_scoped = parm.isScoped() if parm.isScoped(): parm.revertToDefaults() def makeChanges(action_dict, parms_input_dict, parms_output_dict): """A function used to make actual changes to keyframes specified in the input dictionary. All inputs should already have been validated. The proposed changes should be accepted by the user before this stage.""" action = action_dict['action'] range_start = action_dict['range_start'] range_end = action_dict['range_end'] if action == 'offset': offset = action_dict['offset'] deleteKeyframes(parms_input_dict) setKeyframes(parms_output_dict) if action == 'clear': deleteKeyframes(parms_output_dict) if action == 'refit': pass #deleteKeyframes(parms_input_dict) #setKeyframes(parms_output_dict) # main: # get user inputs start inputs_valid, selected_nodes = getNodes() action_dict = {} # get user action if inputs_valid: inputs_valid, action = getAction(actions) if inputs_valid: action_dict['action'] = action # get various inputs based on selected action if inputs_valid: if action == 'offset': inputs_valid, action_dict = getOffset(action_dict) elif action == 'clear': inputs_valid, action_dict = getClear(action_dict) setScopedParmsToDefault(selected_nodes) elif action == 'refit': inputs_valid, action_dict = getRefit(action_dict) else: inputs_valid = False # get parms and keyframes from user selection if inputs_valid: parms_input_dict = getParmsFromRange(action_dict, selected_nodes) # calculate intended changes into parms_output_dict if inputs_valid: parms_output_dict = getChanges(action_dict, parms_input_dict) # present intended changes if inputs_valid: inputs_valid = showIntentions(action_dict, parms_input_dict, parms_output_dict) # perform changes if inputs_valid: makeChanges(action_dict, parms_input_dict, parms_output_dict) commKeyframeManipulator()
Edited by SWest - April 13, 2023 16:07:03
Interested in character concepts, modeling, rigging, and animation. Related tool dev with Py and VEX.
- Jonathan de Blok
- Member
- 253 posts
- Joined: July 2013
- Offline
Here is nice highlevel utility I wrote to control keyframes and handles, great for getting nice space- and temporally smooth easings.
The video is a bit older then current UI but the same principle remains
Part of my free and open toolkit: ttps://bitbucket.org/jcdeblok/jdb_houdinitoolkit/src/master/
The video is a bit older then current UI but the same principle remains
Part of my free and open toolkit: ttps://bitbucket.org/jcdeblok/jdb_houdinitoolkit/src/master/
Edited by Jonathan de Blok - May 23, 2023 09:52:43
More code, less clicks.
- Jonathan de Blok
- Member
- 253 posts
- Joined: July 2013
- Offline
And here is a small snippet to put in toolbar to shift all keyframes globally after the current time, great for adding some temporal space in complex animation setup without have to select all nodes.
Feel free to recycle bits if you find them useful!
Feel free to recycle bits if you find them useful!
import hou
#for debugging, can be omitted when used as shelf tool as that will populate the kwargs dict for us
if not "kwargs" in globals().keys():
kwargs={}
kwargs["shiftclick"]=False
res = hou.ui.readInput("Frames to shift keyframes after current frame", buttons=("OK", "Cancel"))
if res[0] == 0 and res[1].replace("-","").isnumeric():
currentFrame=hou.frame()
shift= int(res[1])
with hou.undos.group("Shift Keyframes"):
list(map(lambda animObj: hou.hscript( "chkeymv -r "+animObj.path()+"/* "+str(currentFrame)+" 20000 "+str(currentFrame+shift)+" "+str(20000+shift) ), list(filter(lambda node: len(list(filter(lambda parm: len(parm.keyframes())>1, node.parms() ))), hou.node("/" if kwargs["shiftclick"] else "/obj").allSubChildren() ))))
# this get all nodes from /obj or / (every context) depentingon shiftclick or not
# hou.node("/" if kwargs["shiftclick"] else "/obj").allSubChildren()
# This filter the nodes from the about to those that have parms that have more then 1 keyframe (parms with expression have a keyframe as well, and 1 keyframe parm are not animated since it will yield a contant value thus only use those with more then 1 parm)
# list(filter(lambda node: len(list(filter(lambda parm: len(parm.keyframes())>1, node.parms() )))
# The 'map' function then runs each node from the filtered list through a small lambda function that sets up and executes the hscript command 'chkeymv' with the correct settings
# map(lambda animObj: hou.hscript( "chkeymv -r "+animObj.path()+"/* "+str(currentFrame)+" 20000 "+str(currentFrame+shift)+" "+str(20000+shift) )
# The map function creates an iterator object that doesn't loop over all the items by itself so we force it to yeald by converting it into a list. The generated list itself isn't used
More code, less clicks.
-
- Quick Links