Dynamic Creation of Knobs

In the vast majority of cases, knob use in NUKE is static to that Op, that is, they’re created up front, maintained for the Op’s lifetime, and then destroyed automatically. Some semblance of dynamism is given by the ability to show and hide knobs dependent on the state of others, but the knob’s themselves are simply hidden - they’re still present. Other approaches for dynamic content involves using knobs with the ability to display arbitrary amounts of data, for example the knobs-table_knob. This is perfectly adequate most of the time, but there may come a point that you actually need to destroy or create knobs on the fly, for example as the Reader class does to create or destroy knobs dependent on the underlying reader/writer type being used. To do this, NUKE provides replace_knobs functionality.

Now, replace_knobs is a little tricksy in it’s details, so if you can avoid using it (instead utilizing the show/hide or dynamic content knobs approaches mentioned before) then we recommend you do so. There are of course circumstances where you need to, so in this section we’ll run through the nitty gritty of the implementation, with reference to the simplest case example included in the NDK as the DynamicKnobs.cpp sample.

Using replace_knobs requires implementation in two stages:

  • Create a static function to be used as the knob creation callback. This is responsible for creating the dynamic knobs, dependent on whatever criteria you define.

  • Call two Op level functions add_knobs and replace_knobs in your Knobs and knob_changed calls respectively. Both are passed your callback as a void*, call your callback passing the required Knob_Callback used to create knobs, and return the number of knobs created.

add_knobs, as the name suggests, simply creates the initial set of dynamic knobs. If no dynamic knobs are required by default, then it doesn’t need to be called (and indeed, in the DynamicKnobs.cpp sample this is the case).

replace_knobs takes the same arguments as add_knobs, with the addition of an int defining the number of knobs to be replaced. In most normal circumstances this’d be the number of knobs created by the last call to add_knobs or replace_knobs.

Check out the salient parts of DynamicKnobs as reproduced below:

class DynamicKnobs : public NoIop
{
    bool    _showDynamic;   //Storage for our static knob. This controls whether dynamic knobs are shown or not.
    int     _numNewKnobs;   //Used to track the number of knobs created by the previous pass, so that the same number can be deleted next time.
    float   _dynamicKnob;   //Storage for our dynamic knob. Normally this would be dynamic (heap allocated) itself, but shown as local for simplicity.
public:
    DynamicKnobs(Node* node) : NoIop(node),
    _showDynamic(false),
    _numNewKnobs(0),
    _dynamicKnob(0.0f)
    {
    }

    virtual void knobs(Knob_Callback);
    virtual int  knob_changed(Knob*);
    static void addDynamicKnobs(void*, Knob_Callback);                                 //Our add knobs callback, used by add_knobs and replace_knobs.
    bool    getShowDynamic()      const { return knob("show_dynamic")->get_value(); }  //KNOB_CHANGED_ALWAYS is set, so can't use _showDynamic directly.
    float*  getDynamicKnobStore()       { return &_dynamicKnob; }                      //Get for dynamic knob store. As above, simplified.
    //... snip.
};

void DynamicKnobs::knobs(Knob_Callback f)
{
    Bool_knob(f, &_showDynamic, "show_dynamic", "Show Dynamic");
    SetFlags(f, Knob::KNOB_CHANGED_ALWAYS);

    //If you were creating knobs by default that would be replaced, this would need to
    //be called to create those.
    //_numNewKnobs = add_knobs(addDynamicKnobs, this->firstOp(), f);

    //Call the callback manually this once to ensure script loads have the appropriate knobs to load into.
    if(!f.makeKnobs())
        DynamicKnobs::addDynamicKnobs(this->firstOp(), f);
}

int DynamicKnobs::knob_changed(Knob* k)
{
    if(k==&Knob::showPanel || k->is("show_dynamic")) {
        _numNewKnobs = replace_knobs(knob("show_dynamic"), _numNewKnobs, addDynamicKnobs, this->firstOp());
        return 1;
    }
    return NoIop::knob_changed(k);
}

void DynamicKnobs::addDynamicKnobs(void* p, Knob_Callback f) {
    if(((DynamicKnobs*)p)->getShowDynamic()) {
        Float_knob(f, ((DynamicKnobs*)p)->getDynamicKnobStore(), "dynamic_knob", "Dynamic Knob");
    }
}

As you can see, we stash the numbers of knobs created in an Op instance variable _numNewKnobs and pass it into each future call of replace_knobs. This also demonstrates two important facets of using replace_knobs:

  • If your knob replacement criteria are driven by another knob’s value then that driver knob needs to have the KNOB_CHANGED_ALWAYS flag set on it. This ensures knob_changed, and thus replace_knobs is always called when required. Note that this has the side effect of not synchronizing the driver knob’s source data variable prior to knob_changed, so that value cannot be relied upon to define the creation criteria. As you can see above in the getShowDynamic function, you need to use the driver knob’s knob object’s get_value method.

  • If your knob’s are needed to store and restore data to and from the NUKE script then you have to additionally kick off your callback manually (after the required data restore to existing knobs has been done). This is defined in the example above; once the f.makeKnobs() true (the Knob objects are created) pass has been done and Knobs has again been called, but with f.makeKnobs false. By the end of this method, you can be sure that the driver knob’s have had their data values restored from script, and so any dynamic knob’s which may have had data stored into the script need to be created to provide a place for the script read to restore their values to. Note that at this point, certain node characteristics may not have been set up in such a way as can be queried. For example, you won’t be able to query the number of node inputs reliably. To handle this sort of circumstance, synchronize this characteristic with a hidden knob (for example, the number of node inputs with an Int_knob), such that when this knob value gets restored you’re able to query that to set up your various dynamic knobs correctly.

The add_knobs and replace_knobs methods also allow a void* pointer to be passed, which can be used for managing a class which stores both data defining knob creation criteria and for managing the data store variables of the replacement knobs (if required). An excellent example of extending this interface to provide a generic mechanism which manages it’s own underlying stores is presented in Jonathan Egstad’s DynamicKnob’s example which can be found on Nukepedia. As you can see in the simplest case example presented, we simply use the Op itself, and use static stores for the dynamic knob stores.

Exercise: Extending DynamicKnobs

Since utilising replace_knobs can be tricky, we’d recommend starting off your first foray by extending the above example. For this exercise, take the existing DynamicKnobs example, and:

  • Rework it to dynamically allocate data stores for the dynamic_knob on the heap, destroying them as required to prevent leaks.

  • Now rework it to replace the static Bool_knob with an knobs-Int_knob, and create a matching number of dynamic knobs (with dynamic data store for each one) to the current value of the Int_knob.