FEATURE September 10, 2001
Using Max Script for Building Game Levels (Continued)
by Shailesh Watsa
Building the Final Export Mesh
Once the user has finished building the level mesh, they would export this out of Max and into the game engine or the lighting editor in our case. In case the game engine was using vertex lighting then there wouldn't have been much to do on the parent before exporting it. It would be more or less ready to export. However, our game engine and lighting editor requires the level mesh to have lightmaps assigned and mapped to them for it to light up later. Because of this, an export mesh has to be built which has appropriate lightmaps assigned to it.
When the user presses the button Build Export Mesh, the script file first makes a copy of the parent object, which shall be henceforth referred to as parent2. Then it checks for the primitives in scene that are not hidden and collects them into a list, which we shall call as the 'primitive list'. After which it hides everything in scene except parent2.
Parent2, being a booleaned object, will have a multi material assigned to it. The script breaks the faces in parent2 according to the sub materials being used. Then these detached meshes are reassigned with the appropriate materials. The code for this appears in Listing 2.
After this, each of these objects is further broken into smaller planar objects. The explode function in Max is used by the script to do this, hence the explode threshold determines the angle for breaking up into smaller objects. This occurs because a planar mapping is done on these objects later, for use by lightmaps. The code for this appears in Listing 3.
When the mesh is broken into planar surfaces, some of these planar surfaces (mostly the floor) turn out to be huge objects that stretch across many primitives. We call these objects rogue objects. These rogue objects require huge lightmaps, and chunks of this lightmap would go to waste due to many unmapped areas. Therefore, these objects are checked and broken down, such that the broken objects do not stretch across more than one primitive.
To do this, all the exploded objects are taken. Their bounding boxes are checked against the bounding boxes of the primitives that exist in the primitive list. If the object's bounding box does not lie within the bounding box of any single primitive, it is a rogue object.
Once all the rogue objects have been identified, each of these rogue objects are taken and the bounding box of their individual faces are checked against the primitives from the primitive list. Sets of faces that lie within a single primitive are taken and detached into separate objects. At the end of this process, each detached object falls into a single primitive.
Once the objects are broken down, each of these objects is taken and applied mapping co-ordinates on the second channel. The second channel is used because the first channel has already been used to map the diffuse texture.
At this point, I would like to thank Simon Feltman for the wonderful plugin Multimap.dlx, which he has written. Without this plugin, I wouldn't have been able to access the 2nd channel mapping co-ordinates in Max.
In order to apply mapping co-ordinates to the objects, first the face normal of each object is determined. The script then checks which world axis is closest to the face normal (this will be referred to as the 'closest axis'). After checking, the object is applied with mapping co-ordinates perpendicular to this closest axis. Following this, the mapping co-ordinates are fit to the object extents, such that the mapping co-ordinates extend from 0 to 1. The code for this appears in Listing 4 and Listing 5.
Once the object is mapped with mapping co-ordinates on the 2nd channel, the right sized bitmap has to be created and assigned to the object as a lightmap. For do this, the area covered by the object along the closest axis is determined. This area is scaled by a factor to get the size in pixels. We'll term this as the object area in pixels. The user, while building the mesh, can set this factor. It gives the user control over the resolution of the lightmaps that are created. A large factor will yield small lightmaps and a small factor will result in large lightmaps.
The bitmaps that are created have to be square, and their height and width in pixels have to be a power of 2. The script then checks for the optimal bitmap size starting from the smallest in order to accommodate the 'object area in pixels' that was previously calculated. The code can be found in Listing 6.
Now the right bitmap size has been ascertained and the object has been mapped on the 2nd channel. However, the object mapping extends from 0 to 1, i.e. the entire bitmap area, whereas the actual object area in pixels, occupies only a region in that bitmap. Therefore, the mapping co-ordinates are scaled to fit the 'object area in pixels' in proportion to the bitmap size that has been ascertained.
After scaling the mapping co-ordinates, a new material is assigned to the object, which is basically a copy of the original material that the object had. The newly created blank bitmap is assigned as the lightmap by assigning it to the selfillum channel of the object's material. The set of objects at the end of this process will have proper textures and lightmaps mapped and assigned to them. Since every broken object gets a lightmap, it results in a huge number of lightmaps being used by the entire level mesh. Hence, these individual tiny lightmaps have to be clubbed into bigger ones.
To club these lightmaps, a proper criterion has to be determined and used. The script uses the original primitives that the objects resulted from in order to club their lightmaps. By doing this, the number of lightmaps should be equal to the number of primitives that were used to build the level mesh. The user also has the control to club them further by assigning a common ID to sets of primitives. The user can assign a user property called ClubID to the primitives before the export world is built. Sets of primitives are assigned a common ClubID value. When the script clubs the lightmaps, it also checks for primitives with the same ClubID and clubs the lightmaps of the objects accordingly.
To do all this, the objects have to first be linked to the primitives that they resulted from. Each primitive is taken, and all the export objects are checked against the primitive. The bounding extents of each export object is taken and checked if it falls entirely within the bounding extents of the primitive. Though this produces good results, some objects resulting from primitives that in turn fall totally into a bigger primitive, could be linked to the bigger primitive.
To avoid this, all the planes in the primitive are taken and collected into an array or collection called arrplanes. Then, the first face of each of the export objects, which fall into the primitive's bounding box, are taken and checked against the planes in the arrplanes array. The object has resulted from that primitive, if all three vertices of this face lie on any of the planes in the arrplanes array. Such objects are assigned the CreateID user proper of the primitive. This is because any object that was created by breaking the parent or parent2 should have faces which lie on one of the planes of the primitive that they resulted from. The code for this appears in Listing 7.
Once the links are completed, the script begins clubbing the lightmaps. A script function for clubbing these lightmaps has been made. When called, the function automatically determines the correct clubbed bitmap size for the lightmaps to be clubbed locates and remaps the mapping co-ordinates to vacant areas in the clubbed map and also reassigns the selfillum map of the object with the newly clubbed lightmap. It also provides a one-pixel edge to these mapped areas to accommodate bleeding from adjacent pixels, which is caused when bilinear filtering is used in the engine.
Once the clubbing is complete, the mesh is ready for export. All the objects are then grouped into one group called Parent_world_for_export, so that it is easy to select the group and export into the game engine. In case the user needs to modify or extend the game level, all they have to do is delete the group, unhiding all the primitives and the parent object that are currently hidden, and work on with the level mesh.
In addition to this some other properties are also set on these export objects such as ClubID and Ambient colour, which are all derived from the Primitives that they fall in.
Creating Custom Primitives
The simple-object plugin is used to create scripted primitives, for use in building level meshes. A detailed description on this can be seen in Max script help under the topic Scripted simple-object plugin. It covers the creation of such scripted objects in detail.
The tower_plugin definition that can be found in the Max script help has been modified to build the custom primitives. The 'Stairs with railing' that is shown in figure3 is one such custom primitive.
Figure 3: A custom primitive 'Stairs with railing' with a bend modifier applied.
These custom primitives are very useful, and can give you good results when used with other Max modifiers.
Continue to page 3. >>
(Originally published in Gamasutra August 2001.)