Lesson 5: Geometric Objects
 
 
 

Geometric objects are one of the the most important parts of 3ds Max, without whom there cannot be a scene. In this lesson we will show how to create the geometry of an object using a mesh, and then we will show how to read mouse actions to create the object.

Geometric object plug-ins are extended from the GeomObject class. However, they usually extend from a child class of GeomObject such as SimpleObject2 that already has the implementation of some of the functions. We can extend from that class if our geometrical object has a deformable mesh. A mesh surface is composed of a set of faces, where each face is a flat surface in 3D space formed by three or more 3D points.

In the project Lesson5a we define and implement a class named SampleGObject extended from SimpleObject2. An object of this plug-in is a geometry object with only 4 vertices and 3 faces. At this point we do not get involved with direct object creation using mouse commands because we want to focus on the mesh creation here. To escape mouse commands, we will use a utility plug-in that creates an instance of our geometry object once selected. We will present a more robust method to create geometric objects using mouse commands later in this lesson.

The class SampleGObject has the following structure:

class SampleGObject : public SimpleObject2 
{
public:

    SampleGObject() : objSize(2.0) { }

    // Member variable
    double objSize;

    // From BaseObject
    CreateMouseCallBack* GetCreateMouseCallBack() { return NULL; }

    // From SimpleObject
    void BuildMesh(TimeValue t);

    //From Animatable
    Class_ID ClassID() {return SampleGObject_CLASS_ID;}        
    SClass_ID SuperClassID() { return GEOMOBJECT_CLASS_ID; }
    void GetClassName(TSTR& s) { s = "Sample Geometric Object";}

    void DeleteThis() { delete this; }        
};

We will explain the SimpleObject2::GetCreateMouseCallBack() later in this lesson. The focus here is on the function SimpleObject::BuildMesh() that is called to build the mesh representation of the object. The time input parameter is to implement animated objects, in which the mesh parameters change over the time. We will cover that later in lesson 6 (parameter blocks). Any geometrical object should use a data member of type Mesh to store the built mesh. In our example, the parent class (SimpleObject) has this data member (Mesh SimpleObject::mesh) and we do not need to re-define it. The mesh in this example is composed of 4 vertices (each vertice is represented by a point3) and 3 faces. You can modify the number, coordinates and connection of the vertices in the above code to have your own geometrical object.

void SampleGObject::BuildMesh(TimeValue t)
{
    ivalid = FOREVER;
    mesh.setNumVerts(4);
    mesh.setNumFaces(3);
    mesh.setVert(0,objSize*Point3(0.0,0.0,0.0)); 
    mesh.setVert(1,objSize*Point3(10.0,0.0,0.0)); 
    mesh.setVert(2,objSize*Point3(0.0,10.0,0.0)); 
    mesh.setVert(3,objSize*Point3(0.0,0.0,10.0)); 

    mesh.faces[0].setVerts(0, 1, 2);
    mesh.faces[0].setEdgeVisFlags(1,1,0);
    mesh.faces[0].setSmGroup(2);
    mesh.faces[1].setVerts(3, 1, 0);
    mesh.faces[1].setEdgeVisFlags(1,1,0);
    mesh.faces[1].setSmGroup(2);
    mesh.faces[2].setVerts(0, 2, 3);
    mesh.faces[2].setEdgeVisFlags(1,1,0);
    mesh.faces[2].setSmGroup(4);

    mesh.InvalidateGeomCache();
}

As you can see in the sample code above, the SimpleObject::BuildMesh() needs to set the number of the vertices and faces of the object, set the coordinates of the vertices and define the faces by telling which vertices they connect. Note that the viewport will not be updated as long as all of the objects within are valid. That is why we are calling mesh.InvalidateGeomCache() in the end, so our object gets displayed in the viewport.

Our geometrical object does not use the mouse commands to create the object yet. We utilize a size variable (objSize) in this example (Lesson5a) and initialize it in the plug-in constructor. For now, it will not be changed anywhere in the program and will only be used for generating the coordinates of the mesh's vertices.

What remains is to have a utility plug-in that creates our geometry object and displays it in the viewport. This utility plug-in class is named Lesson5a and extends from UtilityObj. Once that utility plug-in is selected, in the Lesson5a::BeginEditParams() we create a new geometry object and assign it to a node in the viewport. The following code shows how it is done.

void Lesson5a::BeginEditParams(Interface* ip,IUtil* iu) 
{
    ...

    // Create an object 
    SampleGObject* myGeomObj = new SampleGObject();
    INode* node = ip->CreateObjectNode(myGeomObj);
    TimeValue t (0);
    Matrix3 tm(1);
    node->SetNodeTM(t,tm);
}
Note that the created object always appears at the origin of the viewport and with the fixed initial size. We also create our object by calling new on the SampleGObject. With this implementation the geometric object class is invisible from 3ds Max because there is no way for 3ds Max to know that this class exists. The only interface to your geometric object is the utility plug-in Lesson5a. As a result, there will be problems for loading and saving a scene where instances of SampleGObject exist. For a more stable implementation we will need to define the class descriptor (ClassDesc2) class for our geometric object and also change the functions in the DllEntry.cpp to declare to the 3ds Max that there are 2 plug-in classes in this project (you can keep your geometric object plug-in hidden from user by returning FALSE from it's <geometric_object_class_descriptor>::IsPublic(). In order to let 3ds Max know that the utility plug-in is creating an instance of the geometric object, we will need to create it as follows, instead of simply calling the new operator:
SampleGObject* myGeomObj = (SampleGObject*)ip->CreateInstance(GEOMOBJECT_CLASS_ID, SampleGObject_CLASS_ID); 

Using the above line 3ds Max will have the necessary information to create and manage the geometric object. Our sole purpose in this section is to demonstrate how the SimpleObject::BuildMesh() function works. For information about the Lesson5a::DlgProc() function and to see how to create an object without using the mouse commands, refer to the topic Dialog Based Creation of Objects in the programmer's guide. In the next section we explain how to use mouse commands to create a geometric object.

Reading mouse inputs

Our geometrical object is instantly created and displayed when the user selects the corresponding plug-in. However, users are used to create a geometrical object using mouse actions such as move, click and right click. 3ds Max receives these mouse actions and sends them to an object of a class named CreateMouseCallBack by calling the CreateMouseCallBack::proc() on that object. It is the responsibility of our plug-in to instantiate and keep a pointer as well as to destroy this object. We will need to implement the pure virtual class CreateMouseCallBack for this reason. Note that this class should have a pointer to our main plug-in class. This will allow it to pass the results of mouse actions to our plug-in. We also need to maintain at least two points in that class, one for keeping the coordinates of the previous point the mouse was pointing to, and one for the current point.

The sequence of 3ds Max calls to the CreateMouseCallBack::proc() function will enable our plug-in to trace the user's mouse actions. This includes both the viewport coordinate and the screen coordinate of each mouse action, a message for specific action (e.g. button pressed, button released, etc.) and finally an incremental counter on each action. You can refer to the documentation of the class CreateMouseCallBack for more information. In this example we only consider the two values for the msg parameter in 3ds Max call on proc(): MOUSE_POINT and MOUSE_MOVE. The first one is sent whenever the user presses or releases the mouse button and the second one is sent whenever the user moves the mouse pointer. Although the proc() function is called over and over when the user is moving the mouse pointer during the object creation, the message and point parameters do not change until the next time user presses or releases the mouse button. A scenario of the calling for this function is as follows:

  1. User moves the mouse on the viewport when the geometrical object plug-in is active. The proc() function is not called.

  2. User presses and holds the button on some point in the viewport. 3ds Max calls the proc function with message = MOUSE_POINT, point = 0.
  3. User moves the mouse while the button is pressed. 3ds Max calls the proc function over and over with message = MOUSE_MOVE, point = 1.
  4. User releases the button. The proc function is called with message = MOUSE_POINT, point = 1 (note that point is incremented comparing to the previous MOUSE_POINT message).
  5. User moves the mouse again. The proc function is called multiple times with message = MOUSE_MOVE, point = 2 (again, point is incremented comparing to the previous MOUSE_MOVE message).

3ds Max finishes the creation of the object when the Proc function returns CREATE_STOP.

We will call SimpleObject2::BuildMesh() from CreateMouseCallBack::proc(). Object creation will use mouse commands, and the mesh will be constantly re-built during the object creation. Our implementation of the class CreateMouseCallBack and its proc() function will look like this:

class SampleGObject2CreateCallBack : public CreateMouseCallBack 
{
    IPoint2 sp0;          //First point in screen coordinates
    SampleGObject2 *ob;   //Pointer to the object 
    Point3 p0;            //First point in world coordinates
    Point3 p1;            //We added this point. Second point in world coordinates.
public:    
    int proc( ViewExp *vpt,int msg, int point, int flags, IPoint2 m, Matrix3& mat);
    void SetObj(SampleGObject2 *obj) {ob = obj;}
};

int SampleGObject2CreateCallBack::proc(ViewExp *vpt,int msg, int point, int flags, IPoint2 m, Matrix3& mat )
{
    TimeValue t (0);
    if (msg==MOUSE_POINT||msg==MOUSE_MOVE) {
        switch(point) 
        {
        case 0: // only happens with MOUSE_POINT msg
            ob->suspendSnap = TRUE;
            sp0 = m;
            p0 = vpt->SnapPoint(m,m,NULL,SNAP_IN_PLANE);
            mat.SetTrans(p0); // sets the pivot location
            ob->objSize = 0.0;
            break;
        case 1:
            ob->suspendSnap = TRUE; 
            p1 = vpt->SnapPoint(m,m,NULL,SNAP_IN_PLANE);
            float speedFactor = 24.0f;
            ob->objSize = (Length(p1 - p0) / speedFactor);
            ob->mesh.InvalidateGeomCache();
            ob->BuildMesh(t);
            if (msg == 1)
                return CREATE_STOP;
            break;
        case 2:
            return CREATE_STOP;
        }
        ob->NotifyDependents(FOREVER, PART_ALL, REFMSG_CHANGE);
    } 
    else {
        if (msg == MOUSE_ABORT) return CREATE_ABORT;
    }
    return TRUE;
}

To link this class to your geometric object plug-in, a static object of this class is created:

static SampleGObject2CreateCallBack SampleGObject2CreateCB;
The GetCreateMouseCallBack() function of your plug-in registers its address in this static object and returns the address of the static CreateMouseCallBack object. 3ds Max by default will call <yourplugin>::GetCreateMouseCallBack() to access this static object and to call it's proc() function. This way 3ds Max will read the mouse commands as you have defined to create the geometric object. Note that the objSize is changing in the SampleGObject2CreateCallBack::proc()now. Although the mesh vertices are updated in the call of ob->BuildMesh() there, we still need to call mesh.InvalidateGeomCache() function to update the geometry of the mesh. Failing to do so will always show the object with the original size (zero, in this case) in the viewport.

At this point the viewport has yet no idea that the mesh has been changed. To inform the viewport of the update of the geometry, we need to call ob->NotifyDependents(). This function will send a signal to all the plug-ins that have a reference to our geometrical object (including the viewport) so they will know it has been updated.

One might expect the object to always appear at the origin because of the following line in the code:

mesh.setVert(0,objSize*Point3(0.0,0.0,0.0)); 

The above line will return the point3 (0,0,0) no matter what the objSize is. However, it will not be the coordinates of the vertex zero because the pivot of the object is set in the proc() function:

mat.SetTrans(p0);

This will set the coordinates relative to the first point in the space user has clicked on.

The geometric object in this lesson had only one parameter, objSize which does not change after the object is created so the read and write actions can be easily done for that single parameter. However, we need to implement extra features to be able to perform 3ds Max standars actions including but not limited to undo actions, exposing the parameter to MAX Script, updating the dependant plug-ins, responding to changes in the other entities in the scene, animating the parameters or saving the plug-in to the disk. There will be also more parameters as the plug-ins expand, so a standard flexible method of handling the parameters that supports all the mentioned actions will be very useful. In the next lesson we will introduce the parameter blocks that support all these actions and make it very easy to manage any number of parameters in your plug-ins.