If a programmer can translate an artist's work into a real-time 3D environment, the game will be that much better. For many programmers, 3D Studio MAX is the tool of choice for pre-calculating and real-time 3d scenes. And, in the opinion of this author, Microsoft's DirectX API is the most efficient API for games on Windows platforms
This article explains how to create a 3DS MAX plugin to export the artist's work into your real-time 3D engine. My instructions will take you through both the MAX and Direct3D APIs. While no experience is necessary, you should be familiar with Visual C++, Windows, Direct3D (using the Immediate Mode's Draw Primitives) as well as computer graphics basics.
I'll start with the MAX's API and cover how to use it to create a new plugin. With this knowledge, we can implement the the plugin's project using Visual C++. For real concrete stuff, for instance mesh and material exportation, you'll have to wait for the next articles.
Introduction to MAX plugin Development
3DS MAX primarily deals with meshes concerning the realization of 3d objects. Patches and NURBS are also managed, but because the powerful geometry pipeline makes it possible to convert them into meshes, it shouldn't be a problem. The exportation of NURBS-based objects would have made things very difficult and inappropriate for real-time 3D. This flexibility is the reason why 3DS MAX is becoming the standard as far as game development is concerned. If you already own the second version, you'll enjoy its performance compared to the first version. Many changes were made in this second version, and as a result, there is an incompatibility of plugins between the two versions. Getting around this incompatibility requires only a recompilation and maybe a few changes. So, with that in mind, techniques explained in this article will work, regardless of the MAX SDK version you have.
What do I need?
On the software side, you'll need Microsoft Visual C++. While the 4.0 version is good enough to create plugins for the first version of 3DS MAX, you'll imperatively need the 5.0 version if you use the MAX2 SDK. As for APIs, you'll of course need a version of MAX's SDK and DirectX 5.
While CPU Speed is not extremely important on the hardware side as it doesn't have any incidence on the compilation/testing time, I strongly recommend you at least 64 Mb RAM. If you have less, MAX, Visual C++ and your project won't be able to stay in memory. When this happens, compilation time will be multiplied by 5, and MAX will take at least 20 seconds to load. This, needless to say, can be maddening.
128 Mb RAM is sufficient, if your plugins project is inside a workspace which already contains projects such as your 3d game engine.
The MAX SDK gives developers the possibility to create many kinds of plugins, from Procedural Objects to Video Post Processing. A plugin's type called "File Export" is of particular interest. However it's not the type I would recommend. Export plugins are made to export a whole MAX scene, without any interactions with the user, which is, in our present case, not what we want. The utility plugin is more flexible just in case you want to select scene components to export, and how you want to export them.
As a MAX plug-ins developer you'll have to understand MAX's internal structure. The rest of this article will introduce you to fundamental concepts such as the geometry pipeline, the manipulation of the scene's nodes, calculus implementation and the plugins' interface. If you like Oriented Object Programming, you'll love MAX's API: everything has been made according to the OOP philosophy, you'll find classes for almost everything you can think about (Mesh, Face, Point, Matrix…).
What's a node? In MAX, a node is a visual representation of an object (here, the term 'object' means 3D object, light, camera, shape, etc…). Nodes are linked to each other to make a hierarchy. The root node of this hierarchy is the MAX scene. To realize a MAX exporter plugin, it's important to be familiarized with the manipulation of nodes, as they're the starting point of your conversion.
What can we do with a node? A node is created and controlled from the MAX's class INode. I'm not going to detail this class, the MAX's help file will do it better than me, but I'll just comment on a few things that will be useful for our exporter.
The main function of a node is to reference the object it is associated with. This object's state can be evaluated in the world (its representation in the viewport) at a given time. See the geometry pipeline, for more information. Once this object is evaluated, you can retrieve all of its specific data, we'll see later how to do that.
Concerning the hierarchy…from a node, you can get its parent node, the number of its children and a specific child node, using the INode::GetParentNode(), INode::NumberOfChildren and INode::GetChildNode() methods. To scan the whole scene, you just have to get the root node (using a function of the plugins' interface), and then using the functions given below.
Other properties like the node's transformation matrix (a transformation matrix is made of a position, an orientation and a scale factor), the transformation matrix of the object, its reference and its name can be retrieved.
Now you know how a node basically works, let's explain how objects work with the help of the geometry pipeline.
The Geometry Pipeline
This is the most fundamental concept and innovation of MAX. If you have already worked with 3D Studio/DOS, you know how the creation of meshes work. You could start by creating many 2D shapes with the 2D shaper and then create a 3D object based on those shapes using the 3D lofter. To apply modifications (vertices displacement, Boolean operations, mapping, etc…) on it you could use the 3D editor. Each time you made a modification on a 2D shape or 3D object, the previous state was lost as the modification was directly applied on the entities (faces, vertices, 2d spline) that made up the 2D shape or 3D object.
MAX uses another approach to create geometric components called a "procedural approach". Each node contains a node pipeline. At the beginning of this pipeline there is the Base Object. Each time you apply a modification to your node, the Modifier you used is referenced in the node pipeline, with the parameters you set. Then, the geometry pipeline evaluates the node's pipeline to create the World Space State of the node's object. This World Space State is what you can see in the MAX's viewport.
Ok, let's take model what a "top". Don't expect a nice one, I'm just a programmer!
This model can be made in two steps: the creation of a sphere and the application of a Taper on it.
When you create a sphere, a new node is also created and inserted into the MAX scene. The object referenced by the node is the node's Base Object(the sphere). As it is a procedural object, the sphere object is not made of faces and vertices at this stage. This object can be generated into a mesh, depending of the parameters you specified.
Figure 1 below is the geometry pipeline representation of the node at this stage and its World Space State.
In step two, a Taper Modifier was applied on our node, you can see the result in Figure 2 (I've also assigned a texture map). The node now points on what we call the 'Derived Object'. This Derived Object is the result of the node's pipeline interpreted by the geometry pipeline.
Following the Derived Object is the Taper Modifier, and finally, our Base Object: the sphere.
Suppose that you apply a bend to the object. This Modifier will appear in the pipeline right after Derived Object and before the Taper. With the help of MAX's Modifier Stack you can change parameters of any Modifiers you want (even the base object). You can also remove or insert modifiers anywhere you want in the Stack. Anytime the object's World Space State will be recomposed: that's the job of the geometry pipeline.
The World Space State object of a node can be evaluated using the INode::EvalWorldState() method. This method returns a C++ object of the class ObjecState. From this object, you can retrieve the object evaluated by the geometry pipeline. The evaluated object is also a C++ object of a class that is derived from the Object class. MAX implements many classes that are derived from the Object class.
Later in this series, we will see classes such as:
TriObject: defines a mesh object made of triangles
CameraObject: defines either a Free or Target Camera
LightObject: defines either an Omni, Directional, Target Spot or Free Spot Light
A Bit of Calculus
MAX's API offers a set of classes and functions to make the utilization of matrices, points and quaternions easier. Since the manipulation of points is not really a big challenge, I'm not going to overview the Point3 class. Talking about quaternions would go beyond the scope of this article, as they are mostly used when dealing with animation. So only fundamentals of matrices are explained. Here we go.
Transformation Matrices. Transformation matrices are used to switch from one given coordinate system to another; for example, to transfer the coordinates of an object's vertices from local to the world space coordinate system. Transformation matrices are also used to position nodes in the scene. The transformations provided by these matrices are: translation, rotation and scaling. Such matrices can be created and used with the Matrix3 class of MAX.
In MAX, matrices have 4 rows of 3 columns. Usually, 3D matrices are 4x4 instead of 4x3. It's also the case for MAX, but as the last column is always [0 0 0 1] for such matrices, it's implicitly managed by the class and functions dealing with it.
The coordinate system used by max is a right-handed one, with counter-clockwise angles when looking to the positive way of an axis, as shown in Figure 3 below.
Now let's see how the identity, translation, rotation and scaling are encoded. An identity matrix has the property to keep a vector or a matrix unchanged during a vector/matrix or matrix/matrix multiplication. It's encoded as follows:
You can initialize a matrix to the identity by passing 1 to the constructor of Matrix3 class or by calling the method Matrix3::IdentityMatrix().
Translation is stored in the matrix this way:
Translation can be set using Matrix3::SetTrans(), retrieved using Matrix3::GetTrans(), set to null using Matrix3::NoTrans().
The matrices for rotations around the three axes are shown below. The rotation's angle is always given in radians.
These matrices can be created using global functions RotateXMatrix(), RotateYMatrix(), RotateZMatrix(). An incremental rotation around one specific axis can be performed using the Matrix3's methods Matrix3::RotateX(), Matrix3::RotateY(), Matrix3::RotateZ().
A method of the Matrix3 class is also provided to create a Yaw/Pitch/Roll angles matrix (using the rule of Euler's angles) : Matrix3::RotateYPRMatrix().
Scale factors are encoded like this:
Scaling factor can be set using Matrix3::SetScale(), or reset to 1 using Matrix3::NoScale().
For the last part of this article we're going to take a brief look at how the plugin can get access to MAX features.
The Plugin's Interface
The Plugin's Interface is a C++ Interface retrieved during the plugin's initialization. The term Interface signifies that a C++ object contains methods only. The Plugin's Interface is used to provide APIs that the MAX offers to the plugin. From the list of APIs it provides, the following pertains to our exporter.
Command Panel - Rollup Page Method
As the plugin's type we will use is a Utility Plugin, we'll have to design a dialog that will appear during the plugin's initialization in the Utility Plugin's Pan. This dialog will appear under the form of a Rollup Page. Methods of the Plugin's Interface will be used to Add and Delete this Rollup Page dialog.
Nodes Related Methods
By using functions of this API, we can access the Root Node of the scene, accessing a Node from its name.
Provides functions to call the Interactive Nodes Selection Dialog of MAX. Useful to select Nodes we want to export.
Standard MAX dialog
The Track View Pick Dialog, Material Browse Dialog can be called using this API.
Next time, we'll see how to create a Plugin's Project, how to integrate MFC inside the plugin, how to debug it, and check out a simple example of a Utility Plugin.
Loic Baumann is creator and lead programmer at Fatal Design., a small, French-based, game development company. For the last five months, Mr. Baumann has kept himself busy working on a development environment to create real-time 3d applications. Check out his progress at perso.aic.fr:80/lbaumann. Mr. Baumann can be reached at [email protected]