Creating Procedural Objects in 3D Studio MAX

Contents

Preliminaries

References

A Matter of Class

Node Picking

What's a Procedural Object and Why Would You Want to Create One?

A procedural object is created by an algorithm that takes a set of parameters that define the object and produces the object representation from that set. For example, we can write a procedure to create a box, as follows. Given the center, the width, and the height, the procedure generates a list of faces and vertices that constitute the mesh representation of the box. We could also create the box mesh manually by specifying the vertices and faces, but that's very inconvenient. In many cases it is necessary to create the object procedurally; for example, it would be very tedious to create a sphere by explicitly specifying vertices and faces.

It is possible to create procedural objects inside 3D Studio MAX to get data into your game engine since a procedural object in MAX can store information other than whats required for the visual representation. For example, suppose you want to specify certain locations in the level which you will use in your code to place characters and objects. You could create a Location object that stores the position and an id number. The designer can create various instances of Location objects within the level and assign a unique id to each; this data can then be exported into a file and used by the game engine. Or you could create a grid for pathfinding information and assign attribute values to each square - occupied, free etc.

In this article we will see how to write a plug-in to create a simple procedural object in Max 3.1. In order to cover the basics and provide a starting point for programmers to create their own procedural objects, we'll focus squarely on the MAX SDK and won't get involved in the intricacies of creating complicated objects like teapots and ringwaves. We'll stick to the bare minimum required to write a functional procedural object plug-in for the most part and after that approaches to advanced techniques will hopefully be clear. We'll also try to avoid getting into discussions of theoretical aspects; I'd rather just tell you where to look in the MAX SDK help file to find a detailed treatment of the topic. The code for a demo demonstrating the concepts found in this article can be found here.

Since it's not much use creating this kind of procedural object if you can't use it in your game engine, we'll take a quick look at how to export them.

So what object should we create? The object should be simple so people who want to create other kinds of objects won't get bugged by details they see as irrelevant, but it should be complex enough to examine all the basic concepts related to procedural objects. A portal object should fit the bill. Our portal object will be defined by four vertices on a plane, a source cell, and a estination cell, and could be used to implement a simple portal-culling scheme. In our example, the cells will be the standard MAX boxes. If you don't know about portal culling you can read up on it at the Flipcode site (http://www.flipcode.com/). In any case it's not all that important; we'll just see how we'd go about creating an object with the above-mentioned properties without worrying about how to use it in a real-world application.

Preliminaries

I'll assume that you're familiar with the basics of MAX itself, if not the SDK. Any previous experience of creating plug-ins of any type would be very helpful. We'll keep things simple so you should be able to follow most of the stuff even if you've had no exposure to the MAX SDK before this.

We'll need the following

One of the best sources of information about the MAX SDK is the help file and we'll often look to it for details, so it's a good idea to keep it open in the background while reading this article. Now is a good time to open it and bookmark the first page, the one titled 'Plug-In Software Development Kit'; all references will be relative to this page. Also keep in mind that to look up a method, we need to type 'methods, MethodName' in the help file search index and to look up a class we need to type 'class ClassName'.

Plug-in Basics

All MAX plug-ins are Windows DLLs and are usually located in the 'Stdplugs' and the 'Plugins' directories. Typically, 'Stdplugs' contains the plug-ins that come along with MAX and 'Plugins' has third-party plug-ins that you download from the Net or write yourself. There are various categories or types of plug-ins that can be created using the MAX SDK; procedural objects is one of them. We could also create, among others, plug-ins to import/export files, system plug-ins like Biped, object modifiers like Bend and Taper, and atmospheric plug-ins for effects like fog. For a complete list, look in the 'Plug-In Types Overview' section of the help file. There is a standard filename extension associated with each type which is used by MAX when it is searching for plug-ins to load. Procedural object plug-in DLLs have the filename extension DLO. File export plug-ins have the extension DLE, utility plug-ins have the extension DLU and so on. For a complete list look under 'Writing Plug-In Applications / Plug-In Directory Search Mechanism'

The MAX SDK is a set of classes that developers can combine and extend to create plug-in applications. There is an elaborate class hierarchy and the idea is that we tap into it at the right places to get our job done. You can have a look at this hierarchy under 'Plug-In Architecture Overview'. To write a MAX plug-in we'd start by identifying what type of plug-in it should be. Based on this, we'd derive a class for our plug-in from one or more of the classes in the SDK. This would give us certain inherited virtual functions that we would need to implement in our class. These functions would be called by MAX at the appropriate time. For example, if we wanted to do file export, we'd derive from the class SceneExport and implement the function DoExport() which would be called by MAX when the user asked to export the scene. Once we have control, we have access to all kinds of scene information and can call functions implemented inside MAX to do our stuff.

We are real programmers and we want to get right down to the metal, so we won't use any MFC or appwizards to create our plug-in. It's not very complicated. We create a new project of type Win32 Dynamic Link Library / Empty DLL Project, add our source code and the required MAX lib files to the project and build it. We must tell the VC++ IDE to look for header files in the MAX SDK 'Include' directory as well. And we must make sure that the DLL has the DLO extension and ends up in the MAX 'Plugins' directory.

One additional thing to keep in mind is that MAX requires us to add a DEF file to our project. If we don't do this, MAX won't load our plug-in. The DEF file looks like this.

LIBRARY PortalObj
EXPORTS
              LibDescription @1
              LibNumberClasses @2
              LibClassDesc @3
              LibVersion @4
SECTIONS
              .data READ WRITE

You can use this DEF file for all your plug-ins; just keep changing the library name.

The Plug-in's Interface to MAX

Every MAX plug-in must implement a set of functions that are called by MAX when the plug-in is first loaded. These functions provide MAX with information about the plug-in and are called LibNumberClasses(), LibClassDesc(), LibDescription() and LibVersion(). For an explanation of these functions look under 'Writing Plug-In Applications / DLL Functions and Class Descriptors'.

We get a default DllMain() when we build a DLL using VC++ but we implement our own anyway because we need to do some initialization at startup.

Our implementation of these functions is shown in Version1.cpp. We've created a do-nothing PortalObj class that will be our main plug-in class and we'll fill it up as we go along. We've written our own DLLMain() so that we can save the DLL instance handle.

Let's see what we've accomplished with this code. Actually this is enough for us to make our presence felt in MAX. At this point we can create a project workspace and add Version1.cpp to it. We need to add the following lib files from the SDK 'Lib' directory: maxutil.lib, geom.lib, core.lib and mesh.lib. We need to add comctl32.lib if it's not already there, and also the DEF file. Now we can build the first version of our plug-in. Next, we startup MAX, go to the Create branch of the command panel, select Geometry (the default), click on the drop-down box that says Standard Primitives and we see the new entry Portal. However, don't try to create a portal right now or else MAX will crash because our PortalObj class is still a dummy and doesn't implement all the functions required.

The User Interface

Now that we know what it takes to make MAX acknowledge the existence of our plug-in, we'll create our user interface. The user will create the portal by clicking and dragging with the mouse in one of the viewports in the same way you'd create a box or a sphere so we don't really need any specific user interface elements for the creation phase, but after that he'll want to select the source and destination cells. We'll create a rollup page to facilitate this and add it to the command panel. The page is pretty simple and looks like this:

The way it works is as follows. To select a source or destination cell, the user can click on the appropriate button. This action will place him in Pick mode after which he can click on a node in one of the viewports to select it as the source or destination cell. We can throw in a check to make sure that the node he selects is a box. If all is OK, we'll display the name of the selected node in the appropriate edit box.

A rollup page is simply a dialog box created using the resource editor. We must make sure it's exactly 108 pixels wide. I guess that's because it needs to fit nicely into the command panel. When you create the dialog box, in the Dialog Properties, go to the Styles pane and set the style to Child and the border to None. In the More Styles pane tick the Visible check box. We can use the defaults for everything else. Then we add the static text, the two edit boxes, the two buttons and we're done.

The rollup page is now ready to be added to the command panel. How do we do this? It so happens that whenever a procedural object is being created or modified MAX calls a couple of functions inside the plug-in so we'll know what's going on. These functions are BeginEditParams() and EndEditParams(), declared as virtual functions in class Animatable which is right at the top of the class heirarchy shown in 'Plug-In Architecture Overview'. We'll be deriving our plug-in class from a class that is in turn derived from Animatable so we'll inherit these functions as well and override them. When our BeginEditParams() gets called, we'll add the rollup page to the command panel using the AddRollupPage() function and when EndEditParams() gets called we'll delete it by calling DeleteRollupPage().

We need to implement a DialogProc function for our rollup page the same way we'd do it for an ordinary dialog box in a Windows application.

The code that includes all this is shown in Version2.cpp. The rollup page is in Resource.rc. For the time being, we've derived our class straight from Animatable and our DialogProc just returns TRUE in response to the WM_INITDIALOG message. We add the resource file with our rollup page to the project and use the code in Version2.cpp to get the next version of our plug-in.

If we now click on the Portal button in the Create branch of the command panel we can see the rollup page. Almost anything we do after this, including moving the mouse pointer in a viewport will cause a crash for the same reason as before and it's about time we did something about it. We need to put in the code required to create the portal in a viewport after which we can come back to the DialogProc, add functionality to the user interface controls and start picking source and destination cells.

The portal needs to maintain pointers to the source and destination cells. We could store this information in class variables, but there are some issues to be considered. For example, say we've assigned a node as the destination cell to a portal and we store a pointer to the node in a class variable. We can't write the pointer to the disk when the user saves the file, so we'd have to save something like the node name instead. Then when the file is next opened, we'd have to search for the node by name and initialize our pointer again. That's not so bad but consider another problem. What happens if the node is deleted? We end up with an invalid pointer.

We have a simple, clean solution to these issues in MAX's concept of References. Let's get a handle on this next.

________________________________________________________

References