Tutorial 1 - Converting the Bend Modifier

The aim of this tutorial is to explain the steps required in converting an existing plug-in from ParamBlock to ParamBlock2. To aid in this the Bend modifier found in the samples under "maxsdk\howto" will be converted. At each point the key issues will be addressed. The final converted plug-in is contained in the accompanying zip file. The source code should be read in conjunction with this document.

The tutorial is a descriptive step by step guide on how to convert an existing plug-in to support Paramblock2 interface. However the new system has many methods and variables so it is advisable to cross-reference this document with the 3ds Max SDK and the accompanying source code.

Note that at the time the Bend modifier was converted from Paramblock to Paramblock2, the conversion did not support Save To Previous. All plug-ins should always properly support Save to Previous. For an example of doing so, see method bool BoxObject::SpecifySaveReferences(ReferenceSaveManager& referenceSaveManager) in Tutorial 2 - Converting the Box Object.

Step 1 - Project Settings

To use the new methods, the following need to be added to the project:

Step 2 - Convert SimpleMod

The bend Modifier is sub classed from SimpleMod. However, there is a new class that adds support for Paramblock2 called SimpleMod2. This has a public data member, IParamBlock2 *pblock2, used instead of the IParamBlock *pblock provided by SimpleMod. It also provides implementations of ReferenceMaker::GetReference() and SetReference() which get and set the pblock2 pointer.

Original interfaces that are now redundant and can be deleted are as follows:

Also, the notification messages REFMSG_GET_PARAM_NAME and REFMSG_GET_PARAM_DIM are no longer required There are three additions to be made and these add direct access to the Parameter blocks maintained by the plug-in. These methods are from Class Animatable

The complete definition of BendMod is as follows:

class BendMod : public SimpleMod2 {
    public:

        BendMod();

        // --- Interhited virtual methods of Animatable
        void DeleteThis() { delete this; }
        void GetClassName(TSTR& s) { s= GetString(IDS_RB_BENDMOD); }  
        virtual Class_ID ClassID() { return BEND_CID;}

        void BeginEditParams( IObjParam  *ip, ULONG flags,Animatable *prev);
        void EndEditParams( IObjParam *ip,ULONG flags,Animatable *next);

        // Add Direct Paramblock2 Support

        int  NumParamBlocks() { return 1; }
        IParamBlock2* GetParamBlock(int i) { return pblock2; }
        IParamBlock2* GetParamBlockByID(BlockID id) { return (pblock2->ID() == id) ? pblock2 : NULL; }

        // --- Interhited virtual methods of ReferenceMaker
        IOResult Load(ILoad* iload);

        // --- Interhited virtual methods of ReferenceTarget
        RefTargetHandle Clone(RemapDir& remap = NoRemap());

        // --- Interhited virtual methods of BaseObject
        TCHAR *GetObjectName() { return GetString(IDS_RB_BEND2);}

        // --- Interhited virtual methods of SimpleMod
        Deformer& GetDeformer(TimeValue t,ModContext &mc,Matrix3& mat,Matrix3& invmat);        
        Interval GetValidity(TimeValue t);

        BOOL GetModLimits(TimeValue t,float &zmin, float &zmax, int &axis);
        void InvalidateUI();
};

Step 3 - Build the ParamBlockDesc2

The original modifier created its interface by using ParamUIDesc and ParamBlockDescID. Now it is all controlled by ParamBlockDesc2. The bend modifier has a relatively simple interface; for a more detailed look at ParamBlockDesc2 please refer to the PB2Utility sample.

First of all a list of parameter Ids are defined for use with the block descriptor and also for use with GetValue and SetValue. The order you declare these in is the order that they must be defined in the descriptor.

enum { bend_params,};
enum { bend_angle,
    bend_dir,
    bend_axis,
    bend_fromto,
    bend_to,
    bend_from,
};

The first line for the ParamBlockDesc2 constructor defines the block:

static ParamBlockDesc2 bend_param_blk ( bend_params, _T("Bend Parameters"),  0, &bendDesc, P_AUTO_CONSTRUCT + P_AUTO_UI, SIMPMOD_PBLOCKREF,
    //rollout
    IDD_BENDPARAM, IDS_RB_PARAMETERS, 0, 0, NULL,

The flag P_AUTO_CONSTRUCT tells the system that the reference will be created and handled by the owner. If it is set then the reference number must be given after the flags, in this case SIMPMOD_PBLOCKREF. The actual creation is achieved in the call to C`lassDesc2::MakeAutoParamBlocks(). This will create the Parameter map and also create the references using the ref number supplied.

If P_AUTO_UI is used, then this tells the system that UI will be automatically created during calls to ClassDesc2::BeginEditParams(). If this is specified then further dialog information is required including Dialog ID, Dialog Name, Append Closed Flag, and the Dialog proc to handle additional initialization (in this case it is set to NULL). Once the block has been defined, the parameters need to be defined. The definitions for all the parameters are very similar so only one will be described here:

bend_to,    _T("BendTo"),    TYPE_FLOAT,    P_ANIMATABLE,    IDS_TO,
    p_default,    0.0f,
    p_range,     0.0f,     BIGFLOAT,
    p_ui,        TYPE_SPINNER, EDITTYPE_UNIVERSE, IDC_BEND_TO, IDC_BEND_TOSPIN, SPIN_AUTOSCALE,
    p_accessor,    &bendPBAccessor,
    end,

The first line contains required elements. The first two elements are ID and internal name. The next defines the parameter type; in this case it is a float value. A list of flags follows in which P_ANIMATIBLE is defined which means that the parameter can be animated. The last entry is the local name which will be used by Trackview, Schematic View and MAXScript. When assigning the internal and localized names, be careful that the names given are not the same as any of the MAXScript node level property names, such a 'Position'. To see the complete list of MAXScript node level property names type into the MAXScript Listener: print (getpropnames node). Following this is a list of details, which define how the parameter will work. In particular is P_UI, which specifies what Dialog Resource will control the parameter. p_accessor holds the pointer of a PBAccessor class which allows you to check the data of the parameter during calls to GetValue() and SetValue(). More details on PBAccessor can be found in Step 4.

When assigning the non-localized parameter name for each parameter, use the non-localized name in the explicit MAXClass descriptor for the class. The p_ms_default values also come from the explicit MAXClass descriptor.

When specifying the type of the parameter, and the pb1 type is TYPE_FLOAT, look at pb1 code's GetParameterDim method to see what dimensioning is used. The following table shows the pb2 parameter type to use for each parameter dimension type.

PB1 Parameter Dimension Type PB2 Parameter Type
defaultDim TYPE_FLOAT
stdWorldDim TYPE_WORLD
stdAngleDim TYPE_ANGLE
stdColorDim TYPE_FLOAT
stdColor255Dim TYPE_COLOR_CHANNEL
stdPercentDim TYPE_PCNT_FRAC
stdNormalizedDim TYPE_FLOAT
stdSegmentsDim TYPE_FLOAT
stdTimeDim TYPE_TIMEVALUE

The full ParamBlockDesc2 is listed below.

static ParamBlockDesc2 bend_param_blk ( bend_params, _T("Bend Parameters"),  0, &bendDesc, P_AUTO_CONSTRUCT + P_AUTO_UI, SIMPMOD_PBLOCKREF,

//Dlalog rollout
    IDD_BENDPARAM, IDS_RB_PARAMETERS, 0, 0, NULL,

// params

    bend_angle,         _T("BendAngle"),    TYPE_FLOAT,    P_ANIMATABLE,    IDS_ANGLE,
        p_default,    0.0f,
        p_range,     -BIGFLOAT, BIGFLOAT,
        p_ui,        TYPE_SPINNER, EDITTYPE_FLOAT, IDC_ANGLE, IDC_ANGLESPINNER, 0.5f,
        end,

    bend_dir,        _T("BendDir"),    TYPE_FLOAT,    P_ANIMATABLE,    IDS_DIR,
        p_default,    0.0f,
        p_range,     -BIGFLOAT, BIGFLOAT,
        p_ui,        TYPE_SPINNER, EDITTYPE_FLOAT, IDC_DIR, IDC_DIRSPINNER, 0.5f,
        end,


    bend_axis,         _T("bendAxis"), TYPE_BOOL, 0,    IDS_AXIS,     
        p_default,     2,
        p_ui,         TYPE_RADIO, 3,IDC_X,IDC_Y,IDC_Z,
        p_vals,        0,1,2,
        end,


    bend_fromto,         _T("FromTo"), TYPE_BOOL, 0,    IDS_FROMTO,     
        p_default,     FALSE,
        p_ui,         TYPE_SINGLECHEKBOX, IDC_BEND_AFFECTREGION,
        end,


    bend_to,            _T("BendTo"),    TYPE_FLOAT,    P_ANIMATABLE,    IDS_TO,
        p_default,    0.0f,
        p_range,     0.0f, BIGFLOAT,
        p_ui,        TYPE_SPINNER, EDITTYPE_UNIVERSE, IDC_BEND_TO, IDC_BEND_TOSPIN, SPIN_AUTOSCALE,
        p_accessor,    &bendPBAccessor,
        end,

    bend_from,        _T("BendFrom"),    TYPE_FLOAT,    P_ANIMATABLE,    IDS_FROM,
        p_default,    0.0f,
        p_range,     -BIGFLOAT, 0.0f,
        p_ui,        TYPE_SPINNER, EDITTYPE_UNIVERSE, IDC_BEND_FROM, IDC_BEND_FROMSPIN, SPIN_AUTOSCALE,
        p_accessor,    &bendPBAccessor,
        end,


    end
    );

If the plug-in that you are converting maintains references that are handled in a non-trivial manner, then it may be easier to specify the parameter is of type P_OWNERS_REF. This means that the owner of the paramblock will handle the references not the block itself. When using the P_OWNERS_REF flag the p_ref specification needs to filled out with the ref number to use. Using this system means that the references will be handled in the same way as in your original plug-in.

Step 4 - Parameter Checking

In the original Bend Modifier a dialog procedure was used to control the values of the "to/From" parameters. With the ParamBlock2 system, a new class called PBAccessor has been implemented to allow developers to have a SetValue and GetValue call back mechanism. This allows continuous monitoring of the parameter change. PBAccessor has two methods, Get() and Set(). In the bend modifier case, Set() has been used to check the values of 'bend_from' and 'bend_to'.

PBAccessor::Set() is passed a PB2Value Structure which holds the data being changed and an ID of the parameter changing. The following PBAccessor changes the value of bend_from and bend_to depending on a comparison to each other.

class bendPBAccessor : public PBAccessor
{
public:
    void Set(PB2Value& v, ReferenceMaker* owner, ParamID id, int tabIndex, TimeValue t)    // set from v
    {
        BendMod* u = (BendMod*)owner;
        IParamMap2* pmap = u->pblock2->GetMap();
        float from, to;

        switch(id)
        {
            case bend_from:
                u->pblock2->GetValue(bend_to,t,to,FOREVER);
                from = v.f;
                if (from >to) {
                    u->pblock2->SetValue(bend_to,t,from);
                }
                break;
            case bend_to:
                u->pblock2->GetValue(bend_from,t,from,FOREVER);
                to = v.f;
                if (from>to) {
                    u->pblock2->SetValue(bend_from,t,to);
                }
                break;
        }
    }
};

Step 5 - Loading Old Data

Paramblock2 provides an automatic mechanism for loading in old paramap data. To take advantage of this system the original ParamBlockDesc is used, but now using the newly created parameter Ids.

static ParamBlockDescID descVer0[] = {
    { TYPE_FLOAT, NULL, TRUE, bend_angle },
    { TYPE_FLOAT, NULL, TRUE, bend_dir },
    { TYPE_INT, NULL, FALSE, bend_axis } };

// The current version
static ParamBlockDescID descVer1[] = {
    { TYPE_FLOAT, NULL, TRUE, bend_angle },
    { TYPE_FLOAT, NULL, TRUE, bend_dir },
    { TYPE_INT, NULL, FALSE, bend_axis },
    { TYPE_INT, NULL, FALSE, bend_fromto },
    { TYPE_FLOAT, NULL, TRUE, bend_to },
    { TYPE_FLOAT, NULL, TRUE, bend_from } };

static ParamVersionDesc versions[] = {
    ParamVersionDesc(descVer0,3,0)
    };

In the BendMod::Load() method a callback is registered which maps the incoming IDs to new enumerated IDs. It is important to note that old Parameter maps still need to be loaded using the original mapping techniques before the new parmblock Ids are used.

ParamBlock2PLCB* plcb = new ParamBlock2PLCB(versions, 1, &bend_param_blk, this, SIMPMOD_PBLOCKREF);
iload->RegisterPostLoadCallback(plcb);

Step 6 - Derive from ClassDesc2

ClassDesc2 is subclassed from ClassDesc and it is used for plug-ins using the Paramblock2 system. It contains a table of ParamBlockDesc2s for all the parameter blocks used by the plug-in and a number of methods, including access to the block descriptors, auto user interface management, auto param block2 construction, and access to any automatically-maintained ParamMap2s. To use this class, simply replace all reference of ClassDesc with ClassDesc2.

Step 7 - Rename pblock to pblock2

All GetValue() and SetValue() calls used the original pblock pointer to IParamBlock. This needs to bechanged so that it uses theIPramBlock2pointerpblock2`.