Geometric objects are one of the most important parts of 3ds Max, without which 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 use 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. The object created by our plug-in 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. To avoid 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, where 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 vertex 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 setting which vertices they connect to. 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() at 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.
The last step 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); }
SampleGObject* myGeomObj = (SampleGObject*)ip->CreateInstance(GEOMOBJECT_CLASS_ID, SampleGObject_CLASS_ID);
By using the above line of code, 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.
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, and we implement a plug-in that does this in the Lesson5b project. 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 for using this function is as follows:
The user moves the mouse on the viewport when the geometrical object plug-in is active. The proc() function is not called.
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 looks 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;
At this point the viewport still has no idea that the mesh has been changed. To inform the viewport of the update to the geometry, we need to call ob->NotifyDependents(). This function sends 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 standard actions including but not limited to undo actions, exposing the parameter to MAX Script, updating any dependent plug-ins, responding to changes to other entities in the scene, animating the parameters, or saving the plug-in to disk. There will also be more parameters as the plug-ins expand, so a standard flexible method of handling the parameters that supports all the above-mentioned actions is very useful. In the next lesson we will introduce parameter blocks which support all these actions and make it very easy to manage any number of parameters in your plug-ins.