oscillator
"""
This script is for quickly generating useful oscillating patterns of keyframes, often used in animation for noise,
vibration, settles, and cycles. The patterns available can be uniform values (all the same), randomized values,
and ramping up/down (gradually increasing/decreasing). The fequency in frames can be set as uniform
(always the same number of frames apart), or various amounts of "jitter", which uses weighted probabilities
to randmomize the number of frames between each key.
Author: Eric Luhta
ericluhta@gmail.com
Put in scripts folder.
To run:
import oscillator
reload(oscillator)
"""
import pymel.core as pm
import random as rand
class Oscillator:
"""Class for oscillator values and methods
:param curve: The selected curve
:param frequency: The amount of frames between each key set
:param amplitude: How much to increase the value of the key value's baseline for larger ranges of motion
:param jitter: The amount of randomization desired in the frequency, adding frames here and there that are
different amounts apart from the frequency value
:param is_uniform: if the generated keys will all be the same, rather than randomized values
:return: nothing
"""
def __init__(self):
self.used_curves = {} # dict of curves used to keep amplitude from increasing with each use
def not_even_index(self, index):
"""Check if it's an even numbered index for positive/negative phases"""
return index % 2 == 0
def get_difference(self, curve):
"""Find the largest difference between 2 keys in a curve so the generated values make sense in the
context of the curves overall range of motion"""
key_values = pm.keyframe(curve, query=True, vc=True, absolute=True)
return (max(key_values) - min(key_values)) * .5 # split the difference
def get_amplitude_base(self, curve):
"""Uses the average value of all keys on the curve to establish a base value for the range of the
alternating keys. If the average is less than 1, it sets it at 1.
:param curve: Which selected curve to look at
"""
average = self.get_difference(curve)
if average <= 1.0 and average >= 0:
average = 1.0
elif average < 0 and average >= -1:
average = -1.0
return average
def set_oscillator_keys(self):
for curve in self.curves:
# keep track of what curves have been used since the tool was opened to avoid the average value
# increasing every time it's used, as well as staying consistent when changing the amplitude
amplitude_base = self.check_used_curves(curve)
# build the list of frames that will have keys set
frame_list = self.create_key_times(curve)
if self.is_uniform:
self.set_uniform_keys(curve, amplitude_base, frame_list)
elif self.does_ramp:
self.set_ramp_keys(curve, amplitude_base, frame_list)
else:
self.set_randomized_keys(curve, amplitude_base, frame_list)
def check_used_curves(self, curve):
if curve not in self.used_curves:
amplitude_base = self.get_amplitude_base(curve)
self.used_curves[curve] = amplitude_base
return self.used_curves[curve]
def set_uniform_keys(self, curve, amplitude_base, frame_list):
"""Generate evenly oscillating keys with no randomization of values"""
value = self.amplitude * amplitude_base
for index, frame in enumerate(frame_list, 1):
self.set_key(curve, index, frame, value)
def set_ramp_keys(self, curve, amplitude_base, frame_list):
"""Ramp up/down in value throughout the range of keys"""
max_value = self.amplitude * amplitude_base
ramp_range = len(frame_list)
key_vals = pm.keyframe(curve, query=True, sl=True, vc=True)
# if the selected curve is not flat, use a different method so the set keys will follow the curve's slope
if key_vals[0] != key_vals[-1]:
self.set_slope_ramp_keys(curve, frame_list, max_value, ramp_range)
else:
# calculate the gradually increasing/decreasing value based on the number of keys to generate,
# i.e. 12 keys = 1/12*max_value, 2/12*max_value etc
for index, frame in enumerate(frame_list, 1):
if self.ramp_up: # increase gradually
value = (index / float(ramp_range)) * max_value
else: # decrease gradually
value = ((ramp_range-index) / float(ramp_range)) * max_value
self.set_key(curve, index, frame, value)
def set_slope_ramp_keys(self, curve, frame_list, max_value, ramp_range):
"""When the selected range has different start/end values, this method will ensure the keys follow along the
curve's slope"""
for index, frame in enumerate(frame_list, 1):
pm.setKeyframe(curve, time=frame, insert=True)
if self.ramp_up:
value = (index / float(ramp_range)) * max_value # positive phase
else:
value = ((ramp_range-index) / float(ramp_range)) * max_value
if self.not_even_index(index): # negative phase
value *= -1
pm.keyframe(curve, edit=True, time=(frame,frame), relative=True, vc=value)
def set_randomized_keys(self, curve, amplitude_base, frame_list):
"""Randomize key values based on a value range derived from curve's base amplitude * multiplier(amplitude)"""
for index, frame in enumerate(frame_list, 1):
value = rand.uniform((.1 * amplitude_base),(self.amplitude * amplitude_base))
self.set_key(curve, index, frame, value)
def set_key(self, curve, index, frame, value):
"""Determine the appropriate value for what phase of the oscillation we're at and where the baseline is"""
# figure out where the curve is sitting on the value spectrum
baseline_value = self.get_baseline_value(curve)
if self.not_even_index(index): # positive phase
value += baseline_value
else: # negative phase
value = baseline_value - value
pm.setKeyframe(curve, time=frame, insert=True)
pm.keyframe(curve, edit=True, time=(frame, frame), absolute=True, vc=value)
def get_baseline_value(self, curve):
"""checks the start/end key values to figure out where the baseline to set values from is, be it 0 or otherwise.
This is for when the start and end key values of the range are not 0"""
key_vals = pm.keyframe(curve, query=True, vc=True, sl=True)
# if start/end are the same, just use that value
if key_vals[0] == key_vals[-1]:
return key_vals[0]
else:
# otherwise find the median between them
return (key_vals[0] + key_vals[-1]) / 2
def add_jitter(self, jitter):
"""Adds the jitter based on the weighted values passed from createKeyTimes"""
total = sum(weight for choice, weight in jitter)
random_number = rand.uniform(0, total)
upto = 0
for choice, weight in jitter:
if upto + weight >= random_number:
return choice
upto += weight
def create_key_times(self, curve):
"""
Creates a list of the frames to set the alternating keys on
:param frequency: How many frames between each key
:param jitter: The amount of randomization to use on the frequency
:param curve: Which curve we're operating on
:return: frame_list
"""
frame_list = []
jitter_list = self.JITTER_DICT[self.jitter]
key_times = pm.keyframe(curve, query=True, tc=True, sl=True)
start = key_times[0]
end = key_times[-1]
# cast start/end lists into integers
start_frame = int(start + self.frequency)
end_frame = int(end)
# delete any keys between the selected range, start frame math is to avoid first key never changing
pm.cutKey(curve, clear=True, time=(start_frame-(self.frequency-1), end_frame-1))
# make sure we start on the right frame
frame_list.append(start_frame)
if self.jitter != 0:
while frame_list[-1] < end:
frame_list.append(frame_list[-1] + self.add_jitter(jitter_list))
else:
for frame in range(start_frame, end_frame-1, int(self.frequency)):
frame_list.append(frame_list[-1] + self.frequency)
if frame_list[-1] >= end:
del frame_list[-1]
return frame_list
def setup_oscillator(self):
self.curves = pm.keyframe(query=True, sl=True, name=True)
self.amplitude = self.amplitude_sldr.getValue()
self.frequency = self.frequency_sldr.getValue()
self.jitter = self.jitter_sldr.getValue()
self.is_uniform = pm.checkBox(self.is_uniform_chbx, q=True, v=True)
self.does_ramp = pm.checkBox(self.does_ramp_chbx, q=True, v=True)
self.ramp_up = pm.radioButton(self.ramp_up_rbtn, q=True, sl=True)
check_ok = True
if len(self.curves) == 0:
pm.warning('[oscillator.py] Please select a curve')
check_ok = False
for curve in self.curves:
if len(pm.keyframe(curve, sl=True, q=True)) < 2:
pm.warning('[oscillator.py] Please select at least 2 keyframes per curve to establish the range')
check_ok = False
continue
if check_ok:
print(self.curves, self.frequency, self.jitter, self.is_uniform, self.does_ramp, self.ramp_up)
# The choices of randomization. Each choice contains a two item list, the first item is a choice, the second
# is the choice's weight, determined only by the difference between the other weights.
self.JITTER_DICT = {0: [[self.frequency, 10]],
1: [[self.frequency, 8], [self.frequency-1, 2]],
2: [[self.frequency, 7], [self.frequency-1, 3]],
3: [[self.frequency, 6], [self.frequency-1, 4]],
4: [[self.frequency, 5], [self.frequency-1, 5]]}
self.set_oscillator_keys()
# Interface
def windowUI(self):
"""Tool window and UI """
def radio_btn_switch(state):
'''Turns on/off the radio buttons in the ramp settings if Ramp checkbox is enabled'''
buttons = [self.ramp_up_rbtn, self.ramp_down_rbtn]
for button in buttons:
pm.radioButton(button, edit=True, editable=state)
# if ramp is on, make sure uniform is off
if state:
pm.checkBox(self.is_uniform_chbx, edit=True, v=False)
def ramp_chbx_off():
'''If the ramp checkbox is turned off, turn off the radio buttons associated with it'''
pm.checkBox(self.does_ramp_chbx, edit=True, v=False)
radio_btn_switch(False)
self.windowID = "oscillator"
if pm.window(self.windowID, exists=True):
pm.deleteUI(self.windowID)
self.tool_window = pm.window(self.windowID, title="Oscillator", width=405, height=235, mnb=False, mxb=False, sizeable=True)
self.main_layout = pm.columnLayout(w=405, h=235)
# Main tool tab
self.main_column_layout = pm.rowColumnLayout()
self.image_path = pm.internalVar(upd=True) + "icons/relic_oscillator_header.png"
self.header = pm.image(w=400, h=100, image=self.image_path)
self.is_uniform_chbx = pm.checkBox(label='Uniform', onc=pm.Callback(ramp_chbx_off))
self.btn_rows = pm.rowColumnLayout(nc=3, w=380)
# Ramp settings
self.does_ramp_chbx = pm.checkBox(label='Ramp', onc=pm.Callback(radio_btn_switch, True), ofc=pm.Callback(radio_btn_switch, False))
self.ramp_rdo = pm.radioCollection()
self.ramp_up_rbtn = pm.radioButton(label='Up', sl=True, editable=False)
self.ramp_down_rbtn = pm.radioButton(label='Down', editable=False)
pm.setParent(self.main_column_layout)
# Core settings
self.amplitude_sldr = pm.floatSliderGrp(label='Amplitude', field=True, value=1.0, minValue=.1, maxValue=20.0)
self.frequency_sldr = pm.intSliderGrp(label='Frequency', field=True, value=2, minValue=1, maxValue=20)
self.jitter_sldr = pm.intSliderGrp(label='Jitter', field=True, value=0, minValue=0, maxValue=4)
pm.button(label="DO IT", bgc=(.969, .922, .145), command=pm.Callback(self.setup_oscillator))
pm.showWindow(self.tool_window)
osc = Oscillator()
osc.windowUI()