splinecraft
1/16/2017 - 9:54 PM

oscillator

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()