Overview: This lab will work through the process of creating a simple “Exporter” MAX Plug-in. The goal of the lab will be to familiarize the developer with the basics of creating a MAX plug-in, and the basics of exporting 3ds max scene data.
This lab is composed of 5 phases. At the beginning of each phase you will find a link to a .zip file that contains source code for the exporter that is relevant to the topic of that phase. Phase 0 provides the basic Microsoft Visual C++ v6 project skeleton. Each phase builds upon what was discussed in the previous ones, so simply replace and add the files in the source of the current phase to the previous ones.
Table of Contents
PHASE 0: STARTING OUT
PHASE 1 : GEOMETRY
PHASE 2 : TEXTURES AND LIGHTING
PHASE 3: ANIMATION AND MODIFIERS
PHASE 4: CUSTOM DATA
OTHER STRATEGIES, FUTURE DIRECTIONS
In order to help in identifying the various entries in this document the following legend will be applied:
MaxSDK Classes are in this format: IDerivedObject
MaxSDK Methods are in this format: DoExport()
Filenames are in this format: export.cpp
Source code that is only relevant to this tutorial are in this format: PerFaceData
PHASE 0: STARTING OUT
Source code for Phase 0: phase0.zip
We’ll start with the process of creating our own “skeleton” plug-in from scratch.
In general, when creating plug-ins, I try the following steps in the following order:
- If a skeleton for the plug-in types exists, I start with that
- If not, I use the AppWizard, if the wizard supports the plug-in type
- Otherwise, I do the following
Create a new Win32 DLL project in MSDEV. I usually put the plug-in project under the MAXSDK directory.
I setup the project configs to build the plugin DLL, with the appropriate extension, into the MAXSDK\PLUGIN directory. I use relative paths and add ..\include to the include path and ..\lib to the lib path. I setup the C Runtime settings as appropriate (Debug Multithreaded DLL for Debug and Multithreaded DLL for Release), and create the Hybrid project configuration (by copying the Debug config, but setting the C Runtime to release-mode Multithreaded DLL)
I usually add 2 CPPs ([PLUGINNAME].CPP and DLLENTRY.CPP), the former will hold the various implementation code for the actual plugin-type derived class(es), the latter will hold the requirement C-style plugin DLL exports. Likewise, I usually add a PLUGINNAME.H that has the class definitions of the plugin-type derived class(es).
I add the appropriate [PLUGINNAME].DEF file, with the appropriate 3ds max DLL exports. The def file looks something like:
.data READ WRITE
I add a string table to the resources, along with a version resource. If the plug-in has a dialog, I add one. For this simple exporter lab, there is no additional dialog (well, there’s the about box, which doesn’t really count).
I then proceed to add the basic classes that all plug-ins need. I start with coding the DLL exports, which are more or less boilerplate, into DLLENTRY.CPP. This in turn (via LibClassDesc) leads directly to the creation of the required plug-in ClassDesc.
The DLL exports are fairly self-explanatory. DLLMain (not an export per se) initializes some custom controls. LibDescription will return the localized string that describes the plugin. LibNumberClasses returns the number of plug-in type classes the plug-in DLL provides (for this example, we only provide 1). LibClassDesc returns the ClassDesc for 3ds max to use to identify and create plug-in type objects. LibVersion returns the version of MAX the plug-in works with, which is always defined in the 3ds max headers as VERSION_3DSMAX.
LibClassDesc often just returns the address of a single static ClassDesc. For this tutorial, I derived from ClassDesc2. In general, for those familiar with this process, I recommend working with ClassDesc2 and ParamBlock2 instead of their predecessors. In general, the previous versions will be phased out in the future.
As discussed earlier, the ClassDesc acts mainly as a class factory for the main derived plug-in class. In this case, the main plug-in class we will derive from SceneExport. In our plugin we declare derivations of both ClassDesc2 and SceneExport. Typically, a single static instance of the derived ClassDesc is created and returned by LibClassDesc. The derivation also returns category, class ID, and name information about our derivation of SceneExport. The SDK has a tool (GENCID.EXE) that provides an easy way to create unique Class Ids.
This brings us to the plug-in type object derivation. We provide a derivation for SceneExport in our header (MyExporter). The basic required overrides for SceneExport are briefly explained as follows:
int ExtCount() : Returns the number of file extensions that the exporter provides (for our example, we provide 1, which I arbitrarily decided would be “MEP”).
const TCHAR * Ext(int n) : Returns the 3-letter file extension string for the given extension index (0-based).
const TCHAR * LongDesc() : Provides the “long” description of the file export format.
const TCHAR * ShortDesc() : Provides the “short” description of the file export format, shown in the file export dialog drop-down.
const TCHAR * AuthorName() : Provides the string that describes the author and/or company of the exporter plugin.
const TCHAR * CopyrightMessage() : Provides the string that shows the copyright for the given exporter and/or exporter format.
const TCHAR * OtherMessage1() : not used
const TCHAR * OtherMessage2() : not used
unsigned int Version() : Somewhat redundant, but returns a version number for the exporter.
void ShowAbout() : Shows an about box, accessible by the user from the main MAX export dialog.
int DoExport() : The actual export method. BOOL SupportsOptions() : Returns TRUE if the exporter supports some custom export options. Currently, only one option (export selected versus export entire scene) exists.
Most of the methods are basic returns of localized strings from the string table, or constants/defines. The real “meat” of an exporter plugin is the DoExport method, which we’ll expand in the next sections. This example is simple and therefore has no custom exporter options. You may want to take a look at the exporter samples, and the skeleton exporter, for a simple way to provide a special “before export” dialog that has exporter-specific options.
I added some protected methods (DoHeader, DoNodes, DoTailer) in my basic plan of exporting scene-global information, followed by per-node information, followed by any “end delimiter” file information (if nececssary). I also added some cached pointer member variables, and a cached FILE pointer to open/write to during the actual export.
- Note: Some of the above can be read about in more detail in a section of the SDK helpfile titled “Creating a New Plug-In Project”.
- Note 2: You may note that in the SDK samples, many different styles of code organization are used. Feel free to use the style you feel most comfortable with.
- Create a sub-directory off your MAXSDK directory called “Exporter”
- Un-zip / Copy Phase0 project/source files into Exporter directory
- Start MSDEV, and open Exporter project
- Examine the Project and Build settings. Notice the extra Hybrid Build configuration. Notice the relative lib paths and the added MAX libraries, the output destination, and the C-Runtime type (Multithreaded DLL or Debug Multithreaded DLL)
- Examine the project files. First, look at the EXPORTER.DEF file and notice the four DLL exports
- Look at the DLLENTRY.CPP file, and examine each of the DLL exports closely, including the DLLMain. This CPP is complete, and we will not need to modify it for the remainder of the session
- Look at EXPORTER.H. Notice the #define MYEXP_CLASSID , and the MyExporter declaration.
- Look at EXPORTER.CPP. Notice the MyExporterClassDesc declaration and implementation. Notice the (mostly empty at this point) implementation of MyExporter.
- Examine the Project resources, which at this point include a generic About Box Dialog resource, a generic Version resource, and a String Table resource.
- If you have the compiler and 3ds max handy, feel free to go ahead and compile the plugin (suggested you use Hybrid Build Configuration at this point) and try using it. In 3ds max, you can either doa File->Export orFile->Export Selected (if there are objects selected), and get the File Export Dialog. In that dialog, if the plug-in is properly loaded, you should find the “*.MEP” MyExporter format. Naturally, at this point, if you export to this format, you’ll get an empty file.
PHASE 1 : GEOMETRY
Source code for Phase 1: phase1.zip
For the first actual 3ds max scene contents we export, we’ll concentrate on just the geometry of geometric scene objects.
To facilitate working with nodes in the 3ds max scene, we first need to create a node enumerator method that walks over and processes the nodes in the MAX scene. There are various ways to do this: provide our own method that walks the scene manually, or utilize IScene and TreeEnum callbacks via ExpInterface. For this example, we’ll code our own enumerator from scratch.
For some brief backgrounder information on 3ds max's Scene hierarchy, the basic layout is of a scene tree, with a single “root” Node, whose children are all the nodes in the scene. If a node has a heirarchy (via a 3ds max “link”, or perhaps based on the layout of the object, e.g. bones), this will be represented in the “tree” as well. The Schematic view gives a pretty good visual idea of what this more or less looks like.
The Root node is somewhat special, and can be accessed via the Interface::GetRootNode utility method. The Root node doesn’t represent any actual geometry or the like.
For our node walker, the basic pseudo-code looks something like:
If the ptr isn’t valid, exit
If the ptr isn’t selected, and we’re exporting only selected nodes, exit
If the user canceled (usually via ESC), exit
Evaluate the object, determine what type of object it is, and call
a specific helper method that exports that type of object
For each child of the current ptr Node
The code that will call the “nodeEnum” will be in MyExporter::DoNodes, and look something like:
Get a count of the children of the Root Node
For each child of the Root Node
Check for cancel and break if cancelled
- Based on the above pseudo-code, write the given portions of code. You’ll need to look at INode ( ::Selected(), ::NumberOfChildren(), ::GetChildNode(), ::EvalWorldState()), Interface ( ::GetRootNode(), ::GetCancel()) and possibly ObjectState and Object (::SuperClassID()) in the SDK docs
- Compare your code with the code snippet from Phase 1 exporter-nodeenum.cpp
ObjectState and Node::EvalWorldState are probably worth a bit more explanation. As you may already know, the 3ds max geometry pipeline maintains a “Object” that flows up the pipeline, and can essentially be represented by different, animated states, starting with a “Base” object, then possibly into a “Derived” object with a modifier, and so on.
EvalWorldState tells the node to take it’s Object “stack” and effectively make a collapsed copy, such that the resulting ObjectState contains the final end-result of the stack. This is also what is effectively used at Render time.
- Note: I also added some code to MyExporter::DoHeader that exports some basic header file information (date/time created, title string, etc). Feel free to examine.
3ds max Meshes are triangle polygonal meshes. Every geometric (renderable) object in a 3ds max scene must be able to convert itself to a triangle mesh. Anything in the scene collapsed to a editable mesh will already be in tri-mesh form, however primitives (modified or not) may not be. To get the triangle mesh, we use the Object::ConvertToType API and request a TRIOBJ_CLASS_ID.
When calling ConvertToType, it’s important to check the returned result pointer, and see if it’s the same as the original Object pointer. If so, this implies the Object didn’t generate any new stuff, and thus we shouldn’t delete the result. Otherwise, the Object did generate a new mesh, and we, the caller, are responsible for freeing the mesh.
Once we have a mesh, we extract and dump out the number of vertices and number of faces. This information is available as APIs off the Mesh class.
As the mesh data is in Object Space, we need to get the Node Transform Matrix (for the current frame). We export this first in generic row-major format.
We then export the world-space point of each mesh vertex, multiplying the vertices by the object TM.
We then export the faces, described by three indices into the vertex array. The vertices, taken in counter-clockwise order, describe the “front” (positive normal) of the face. We also export the “edge visibility”, which describes, for a given edgeA->B, if 3ds max draws a highlight line in the viewport or not, and also if in edit-mesh-edge mode, if the edge can be selected. Finally, we export the smoothing group for each face, which could be used with the vertex normals (which we don’t export for now, left as an exercise) to smooth the mesh via Phong or other shading method.
For this phase, we do not export any mesh material or UV information. This will be done in Phase 2.
- In the last part, we had nodeEnum call a utility method to export GeomObjects (in the code sample this is MyExporter::ExportGeomObject). Try implementing ExportGeomObject to export the Node Transform Matrix, and if the Node can convert itself to a TriObject, try exporting the Mesh information. You’ll need to look at Inode::GetNodeTM() and the Matrix3/Point3 classes in the SDK docs for getting the matrix information. You’ll need to look at the Object::ConvertToType() and TriObject and Mesh class in the docs for exporting the mesh information.
- Compare the code with the code fragment exporter-geom.cpp. The code fragment has both the mesh and the patch (next) exporter code – the mesh exporter code is the second “chunk” in ExportGeomObject.
For patches, we provide an additional code path in ExportGeomObject that looks for PATCHOBJ_CLASS_ID and exports the patch information. Patches in the 3ds max scene can be patch grids, or any object converted to an editable patch.
As with Meshes, we use ConvertToType to get the PatchObject out. The PatchObject will contain the PatchMesh object, which contains all the overall geometric information (total verts, vectors, edges). We export all the information for both triangular and quad patches. Note that a PatchMesh can contain a collection of actual tri/quad patches, described by indices into the various overall PatchVert/PatchVec/PatchEdge arrays.
Patches can be either TriPatch or QuadPatch, defined by either 3 or 4 bezier edge splines. 3ds max uses bicubic (degree 3, with effectively 4 “control” points) patches. It does something fairly tricky when connecting Tri and Quad patches together, which is to internally promote Tri patches to quadric (degree 4) to get good continuity between patches.
How you interpolate your patch geometry is up to you – the process 3ds max uses is somewhat beyond the scope of this tutorial, but this is documented in the SDK helpfile.
For this tutorial, we’re done with basic geometry information. There are various other geometric things that could be exported – for example, we could export NURBs data, Shapes (splines and the like), or export primitive information for game engines that have built in basic primitive support.
- Try writing the code to export out the PatchObject information, added in a separate method or to the body of ExportGeomObject (I did the latter). You’ll need to examine PatchObject, PatchMesh, and Patch classes in the SDK docs.
- Examine and compare to exporter-geom.cpp
- Finally, compile and build the plug-in, and try exporting some scenes in 3ds max with either mesh or patch objects.
PHASE 2 : TEXTURES AND LIGHTING
Source code for Phase 2: phase2.zip
First, we need to actually export the materials used in the 3ds max scene. This is part of our “header” or global information.
We iterate over the list of Material used in the 3ds max scene via the Interface::GetSceneMtls() API. For each mtl, for this tutorial, we just print out basic name information. If the mtl has subtexmaps, we print out one-level deep information on the legal subtexmaps. Also, for the purposes of an example, if the subtexmap happens to be a Bitmap Texmap, we print out the filename of the bitmap.
- Update DoHeader by exporting information about the Scene Materials, using the guidelines above.
- Compare to exporter-scenemtls.cpp
Texture Vertices in Meshes
For meshes, we first add to the section of code that exports mesh Face information, and append the face’s Material ID number to the list of face information. This is specifically for multi-materials. Note that the ID is usually set up in advance by the user. Note also that the MatID should be mod’d by the number of submats in a multi-mat.
We then need to get the texture vertices out of the mesh. Each mesh can support up to 99 different texture “channels”, each channel having a different UVW mapping. The mapping information is contained in the texture vertices. Note that typically, the default UVW mapping is contained in map channel #1. Map channel #0 is usually reserved for color-per-vertex info, and for this tutorial, we will not export this information. Channels 2-99 can be used for almost any other purpose, but we’ll treat them the same as channel 1 for this exporter.
Each Mesh face has a corresponding Texture Face. The Texture Faces contained in the Mesh have the corresponding three texture vertices that make up that “face”. Note that each texture face can have different associated texture vertices, so there is not a one-to-one mapping between mesh vertices and mesh texture vertices (thankfully).
Texture Vertices in Patches
For patches, getting texture vertex information is actually similar to what we did for meshes – the APIs of PatchMesh are slightly different, but pretty close.
Each Patch within a PatchMesh should have a corresponding TVPatch, which in turn contains 4 (and always 4) corresponding texture vertex indices. This is similar to a TVFace in the Mesh class.
- For this step, the example code is somewhat embedded in the routine, so it’s easier to just look at the relevant portions of updated code in ExportGeomObject in exporter.cpp in phase2. Pay attention to the three portions which are:
- Exporting the basic node material name (and wire color, if no material was assigned)
- Exporting the texture vertices information for the texture channels for patches
- Exporting the texture vertices information for the texture channels for meshes
For most engines/export formats, lighting data is just as important as material information. As such, we add the ability to export basic light information in our exporter.
We first add information about the color of the ambient light in the scene in the exporter header. We simply pull the Ambient light color at the current frame using Interface::GetAmbient.
We add another helper method ExportLightObject and a check for LIGHT_CLASS_ID in our node enumerator. In ExportLightObject, we extract the GenLight and LightState information from the evaluated (via GenLight::EvalLightState) object. Note that for this sample, we ignore the fact that many aspects of the light could be animated.
For this tutorial, we export the light type, the intensity and color, the TM of the light, and the TM of the target (if the light happens to have a target, like a targeted spot or directional light), and some basic shadow information. There are various other things that can be exported, including the exclusion list of objects the light affects and ignores. For simplicity, we don’t export this info, but see GenLight and LightState for more details.
- Try adding code to export the global ambient light as described above to DoHeader.
- Try updating nodeEnum to call a separate newly added method ExportLightObject.
- Try implementing ExportLightObject as described above.
- Compare with exporter-lights.cpp
PHASE 3: ANIMATION AND MODIFIERS
Source code for Phase 3: phase3.zip
We’ve managed to export a lot of “static” information in our exporter, but it’s now worth taking a quick look at exporting some of the more dynamic information in a 3ds max scene, including keyframe animation information, pipeline modifier information, and things like IK information.
The first thing we do is introduce the concept of frames of animation by adding basic number-of-frames info to our export file header – start and end frame numbers (thereby also exporting the number of frames), and the frame rate in seconds. This information is easily accessible via the Interface pointer.
As mentioned previously, there’s a number of things related to lights that can be animated, that we won’t export for simplicity.
- Update DoHeader and export the number of frames in the scene animation as described above. You’ll need to read about the TimeValue and Interval classes, and utilize Interface::GetAnimRange(), GetTicksPerFrame() and possibly GetFrameRate().
- Compare with the changes made to DoHeader in exporter.cpp in phase 3 (made at the end of the routine).
There may be some instances where you may need to extract modifier information on a particular scene object. One popular example is exporting information from the 3ds max R4.x “Skin” modifier.
Keeping things simple, we just want to export the “Bend” modifier data if found. To access the modifiers (if any) for a given scene node, we need to work with the “unevaluated” pipeline. That is, we want to access the derived or base objects within the pipeline without looking at the “final” world state object.
This is relatively simple. Given a node, we can look at the existing ObjectRef via INode::GetObjectRef(). If this Object is a Derived Object (super class ID is GEN_DERIVOB_CLASS_ID), then there exist modifiers in the pipeline. We can cast the Object as an IDerivedObject and iterate over the modifiers associated with the derived object in the pipeline.
The one trick with this is being able to access the given Modifier derived class once it is found (via looking for a given modifier class ID). For this example, as the Bend modifier is part of the collection of default 3ds max modifiers, we cut-n-paste the class definition of BendMod from BEND.CPP, and tweak some of the inline definitions to compile in our project. Clearly, there are a lot of problems with this – if we were to use any of the actual BendMod methods, we would have to link with the modifier properly, and have some sort of shared header, etc. This is what a number of “public” modifiers do (e.g. Physique, Morpher Modifier, COMSkin). Definitely take a look at the “MorpherView” sample in the SDK that uses an exported Morpher modifier API.
The other thing to consider when working with modifiers is that they could potentially store node-specific data separately from the modifier instance. This is often due to a single modifier being instanced for multiple nodes (e.g. single bend modifier applied to multiple selected objects with use-pivots unchecked results in a single “bend” applied to the bounding box of the collection of objects). The ModContext stores the local OS TM, bounding box of the application of the modifier instance, and a LocalModData ptr, which points to a cache of modifier info that needs to be kept separate for each node.
Normally, to retrieve the ModContexts for nodes, you have to start with a given instance of the actual modifier (or “modapp”) from the derived object, and use it to iterate over the various node-associated ModContexts that it “owns”. Alternatively, you can use Interface::GetModContexts to get a list of ModContexts and associated nodes. In general, it’s often easier for the developer if the original developer of the modifier provides APIs that essentially wrap this functionality, rather than having a developer manually iterate over a modifiers’ ModContexts.
For this sample, we just want the basic class definition, to access the given paramblock at the right object offset. We do this and extract the bend angle out of the BendMod pblock. The bend angle will not be node specific (a single bend applied to multiple nodes has a single bend angle)
We then iterate over the remaining modifiers (since it’s legal to have multiple Bend modifiers associated with one derived object) and any other derived objects in the pipeline.
- For this part first look at the added BendMod declaration in exporter.h in Phase 3. Again, this isn’t necessarily the preferred way of doing things, as noted above.
- Look then at the code added to the end of ExportGeomObject. Look specifically at the portion that gets the ObjectRef, and looks for IDerivedObjects and gets the modifiers, if any. Skip the rest (related to the key animations) for the next section.
3ds max makes heavy use of animated parameters in parameter blocks to make things easier for animators. Game engines typically don’t need this information, but it’s possible that they might.
For this tutorial, we’ll get the keyframe values of the just-recently-added bend angle, if any exist. We assume that the pblock bend angle value is controlled by a controller that supports keyframes, and we also assume that only the three basic float keyframe controllers (linear, bezier, and TCB) are supported. Given this, we get the keycontroller interface, get the number of keys, loop over the keys and output the angle value and frame for the given key.
Note that for animated TM information, the best bet is to either get TMController information, if the controller is keyframed and you can imitate the interpolation in your engine, or iterate over the TM in each frame and output the TM. The ASCIIEXP exporter demonstrates an example of this.
- Now examine the part of the updated ExportGeomObject that gets the keyframes out of the bend modifier angle value. Notice that the code assumes a controller is available for the angle param that is keyframeable. If this were not the case, we’d have to sample.
- If you have the phase3 project set, you can try building it, and creating a scene in 3ds max with a cylinder or other primitive that has an animated bend applied, then exporting the object to look at the results.
Bones and IK
Often useful for game engines is exporting bone and IK information. Unfortunately, 3ds max default Bones and IK system is mostly closed and we can only export minimal information from it. In general, 3ds max bones are best used via additional means, such as the Skin modifier. See COMSkin for a partial example of exporting skin information.
Since it’s non trivial, and we’re trying to keep this tutorial simple, we’ll just discuss some possible ways for exporting bones and IK information.
Bones are linked via node hierarchy. There is always a single base node, that is controlled by a IKMasterController. All other bones in the hierarchy are controlled by IKSlaveControllers. During IK solving, the system calls various Control level methods to determine the results on the IK end effector. 3ds max IK controllers computer the results, but use core code to do this, so the solution process is somewhat hidden.
PHASE 4: CUSTOM DATA
Source code for Phase 4: phase4.zip
With most game engine situations, you’ll want to export additional non-3ds max custom node data to be interpreted only by your engine. This can include anything from “power-up” object properties, to special global illumination data, to anything in-between.
As it turns, there are several ways to apply custom data in 3ds max . These can be broken up as follows:
- Per-node custom data
- Per-mesh-vertex custom data
- Texture-channel custom data
Per-node custom data -- AppData
One of the most basic ways to associate any extra custom data with a given 3ds max node is via something called “AppData”. AppData is basically a chunk of data associated with any animatable (note that this can include things like controllers, modifiers and materials, along with typical scene nodes) that can be attached and retrieved from an animatable, and will automatically get saved and loaded from the MAX scene file.
AppData is always specified and designed in advance by the 3ds max plug-in developer for a specific purpose – AppData associated with animatables have ClassIDs identifying the plugin/object that originally created and attached the appdata.
For this tutorial, we add a simple ExportAppData method, and only call it when processing geomobjects (again, for simplicity). We use the appdata classID defined in the SDK “AppData” utility sample SDK\SAMPLES\UTILITY\APPDATA.CPP, and cast the resulting appdatachunk (if found) to a TCHAR and output it.
- One notable caveat: AppData does not get copied if a scene node is copied by the user (via shift-drag or the like).
Per-node custom data – UserProps
Another way to associate custom data with a 3ds max node is using “UserProps”. There are APIs off Inode that support associating string, integer, float, or boolean data with a scene node.
The properties are “named”, and for this tutorial, we’ll use names “UserBool”, “UserFloat”, “UserInt”, “UserString”. Create another utility method (ExportUserProps) and call it after calling ExportAppData in GeomObject processing.
- Caveat 2: UserProp data does copy when nodes are copied.
Per-Vertex custom data
3ds max provides a way to embed basic numeric (float) information with each vertex in a polygonal mesh. This results in a single value per mesh vertex, so a simple box would usually have 8 possible custom values.
In 3ds max, there are up to 99 vertex-data channels. Don’t confuse these with the 99 texture channels (admittedly, this is pretty easy to confuse), accessed via APIs off the Mesh class (vDataSupport and vertexFloat). For 3ds max, channel 0 is normally the soft-selection data channel, and channel 1 is normally the vertex weight channel.
Per-Face custom data
Beginning in 3ds max R4.0 a new capability to host user-defined data on a per-face basis was introduced.
This feature allows a Mesh object to support up to 99 custom data channels for Faces. Each channel provides an array to store data as defined by the developer. The types of data will often be standard items such as floats and ints, however, pointers to more sophisticated objects may be stored as well.
There are already existing samples in the MaxSDK that cover this great detail. Two of these samples are the PerFaceData modifier (maxsdk\samples\howto\perfacedata) and its exporter FaceDataExport ( maxsdk\samples\howto\perfacedata\facedataexport ).
For purposes of this tutorial, the MyExporter code implements the same functionality that is found in FaceDataExport. So, if you apply the PerFaceData modifier to a test object in your scene, MyExporter will find that applied per-face data and export it.
Much more detailed information can be found in the MaxSDK help documentation under the topics "What's New in the Max 4.0 SDK : Custom Face Data Storage", "Class IFaceDataMgr", and "Class IFaceDataChannel".
Texture-channel custom data
The texture channels we talked about when exporting node texture information can, in fact, be used for any floating-point data, not necessarily just UVW data.
The important different herein between texture channel data and per-vertex data is that each mesh (and patch) face has its own “set” of texture “vertices’. So using the simple box example given previously, a single custom texture channel can contain 24 separate custom values.
We’ve already exported the texture channel data, so we won’t code this again. An ADN white paper details various strategies and ways to use the extra texture channels for custom data.
- Examine the exporter.cpp in Phase 4. The AppData example in the added ExportAppData method makes the assumption that the appdata was added from a separate utility plugin, and uses the ClassID from this plugin to both find and determine the type of appdata present.
- ExportUserProps looks for userprop data named UserBool, UserInt, UserFloat, UserString. As noted in the code, you can actually experiment with this by building the exporter, and in 3ds max , on a given scene object, right-clicking on it, going to the user-defined properties tab, and in the large editbox, typing in something like “UserInt=45”. Doing an export of this should result in the proper userprops data exported.
- ExportPerVertData exports the various per-vertex data in a manner similar to the way we exported the texture channel data.
OTHER STRATEGIES, FUTURE DIRECTIONS
The exporter plug-in we’ve created is a somewhat simple example, but provides a pretty broad spectrum of the various parts of the 3ds max architecture.
However, in some circumstances, an exporter may not be enough. The general problem behind the typical use of an exporter with any 3D application is that there is information loss between the application and the eventual game or game engine that uses the exported data. Since information is effectively getting translated between formats, there is bound to be some information that either won’t be possible to replicate in one of the formats.
Additionally, the process is somewhat manual, and prone to errors. A level designer would prefer to use 3ds max as the level engine, but will have to periodically export the data, check it in the game, and tweak. This is the usual process, but in many ways, things are changing to make this more of a way of the past.
The future of 3ds max is one of separation of graphics algorithms from 3D “Design” UI and application “wrappings”. In other words, the basic graphics algorithms will become “portable”. 3ds max itself will become mainly an application wrapper around portable graphics “modules” that can be used in other applications, including games.
Many aspects of 3ds max are already like this. Remember that 3ds max uses the plug-in SDK for a large percentage of the application. SDK and ADN sample code exposes the graphics algorithms used for certain modifiers, etc. that can be ported to a game engine or the like. These sort of things will continue in the future releases of 3ds max .
Already, additional samples emphasize this “separation” concept. The “COMSkin” example demonstrates a way to provide an “external” COM DLL that provides the graphics algorithms (in this case tessellation algorithms), and both 3ds max and Game engine “wrappers” around the external module. In this way, the exact same algorithms can be used by designers in 3ds max and players in the game, and the actual visible output is the same.
As game developers, bear in mind that it can work both ways. If you have a finally tuned piece of code in your current game engine that does a great job with real-time subdivision surfaces, and you want game level designers to be able to see and use the same process in 3ds max , consider exporting the code into a separate module, and writing a 3ds max modifier that wraps around this.
The idea of separating base-level implementation from application-level wrappers is nothing new. However, it’s a very useful idea that can save significant time in the long run.