go to Tutorials Page | go to 3DKingdoms.com

Some Notes on Programming 3D Editing Software

by Jonathan Kreuzer

header screenshot

Contents:


Introduction:

This document serves dual purposes, as a tutorial on some of the issues involved in creating a 3D editor, and as the beginning of a documentation in case I decide to release 3D Kingdoms Creator in some way. Since 3DKC hasn't been released, I've tried harder to present information that might be useful to someone making a 3D Editor, rather than detailed info about programming for 3DKC. I haven't written tutorials of this type before because I still have the lingering feeling this isn't my strong point. After all, I got my start programming in grade school by writing illegible(though working) Qbasic code, and still I'm not particularily insterested in arguments over what constitues good code design. Though now I have 8 years of experience writing 3D editors, so I have at least some qualification to make some suggestions. If you're already experienced in this type of thing, probably nothing here will be that helpful, but it can be interesting to read about how other people do things.

I like to do only minimal planning, and instead start by creating an easily expandible framework, and as the project progresses see what is needed and add it. I don't do a lot of pre-project design, and anything I do certainly isn't set in stone. That said, I do think program design is important, and there is a balance between pre-designing and jumping right in to coding. The better you know what you want to do before you start, the less time is wasted on things that you don't need or will be changed later.

All code is C++, which is my language of choice for very large performance-critical programs. In this case I literally have objects in a world, so it lends itself well to Object Oriented programming. I don't know what constitutes proper object-oriented programming, or elegant code, but the answers to these questions differ according to who you ask anyway. I just write code in the way that makes it easiest for me to write and maintain. As for coding guidelines, I've found them easy to follow since they don't really affect the code, and they are useful for the standardization of trivialities of naming and style among programmers.

 

Data Input/Output

One of the more important parts of creating an editor is the ability to be able to easily change what values objects include that you can save or edit. Back when I had just finished highschool and started on my first 3D level editor I didn't know this. Any time something needed to be changed, I'd first change it in the save, go through and resave every level, then add it to the loading too, a tedious process, with potential errors. (Also I would need to separately make sure the new values were editable and copy properly if objects were cloned.) So what can we do to ease this process?

Chunks - Each object or asset is responsible for writing its own data "chunk", which may call subchunks. For instance (Interface->Write) calls (World->Write) which will call (Object->Write) for each object in the world. Each of the Write functions also saves its own data in addition.

Version numbers - Each Chunk contains its own version number. The reason is that you can make any change you want to the data saved by a chunk without losing previously saved assets/levels, you just add a version number check for the changed code.

Data Transfer Class - I wrote my own data transfer class. See the appendix at the end for the source code to the class, minus the program specific stuff. It's not complicated and it's only about 65 lines of code. I usually like writing things like this myself because I like to have complete understanding and control of them, as I often alter them with program specific stuff to make them easier to work with. The purpose of the class is to allow one function to be used for all types of Data I/O, by allowing the input and output to be set by the class for the data transfered. This means you won't forget anything, because you only have to edit one place for all Data I/O.

Let's say you've just added a new variable 'c' to an object. Here's how you change the code. Perhaps eventually, once you know everything you need has been saved in the new format, you can remove the if statement for cleaner looking code, although it's reasonable to just leave it.

OLD CODE:
  void DataIO( CDataTransfer &IO ) {
    int Version = 1;
    IO.Var( Version );
    IO.Var( a );
    IO.Var( b );
  }

NEW CODE:
  void DataIO( CDataTransfer &IO ) {
    int Version = 2;
    IO.Var( Version );
    IO.Var( a );
    IO.Var( b );
    if (Version >= 2) IO.Var( c );
  }

Here are a few examples of how to initialize the class and call the dataIO functions

  // An Example to Load Data from pFile into the pWorld 
  CDataTransfer NewIO( pFile, CDataTransfer::FILE_LOAD );
  pWorld->DataIO( &NewIO );

  // An Example to Save pWorld into pFile
  CDataTransfer NewIO( pFile, CDataTransfer::FILE_SAVE );
  pWorld->DataIO( &NewIO ); 

  // To Get the size of an Object
  CDataTransfer GetSize( CDataTransfer::GET_SIZE );
  pObjs[i]->DataIO( &GetSize );
  nSize = GetSize.NumBytes();

Also available are MEM_LOAD and MEM_SAVE, which can be used things like cloning objects and saving undo data.

Since the DataIO function is called for both saving and loading, and the copy operator, and the GET_SIZE used with the statistics, it's much easier to add a variable than it would be otherwise. Because of the version number all old saved files will still work perfectly, and when resaved will be the latest version with the new information.

It's also possible you may want to add the size of each chunk to the format, then skip it if it's a newer version. Or in a similar vein I allow plugin objects, and if a plugin object that isn't loaded is present in the scene, it's loaded as a data chunk. Therefore it isn't lost, and is converted back to the proper object if the plugin is loaded.

 

Dynamic Dialogs

Dialog Screenshot

Variables need to be easily exposed to the user for editing, so a lot of this should be handled automatically by the dialog system. My dialogs are completely dynamically generated, but still each editable variable is added in the Dialog function. (For instance each object has a dialog function for editing properties that's called when it's selected.) It could more automatic, but I like to be able to build the dialog to look how I want with a few lines of code. Of course it could also be less automatic, with more control given to the look of the dialog, but more code required. I decided on what seemed to me a happy medium.

The types of dialog items possible are:

Button: Sends a message to the interface. (See messaging sytem.)
Button (asset) : Displays as asset name on a button. Clicking brings up an asset chooser, and sets an asset id.
Button (color): Displays as a color, and brings up a color chooser to set the value.
CheckBox: Checkbox for boolean values.
CheckBoxF: Checkbox for flags, that is checking/unchecking turns a bit on/off.
ComboBox: ComboBox that sets a value to the item chosen.
Edit: Edit box for numerical values(int/float/short)
Edit: Edit box for a string
Slider: A draggable slider control is used to set & display a value, in addition to an edit box.
Text: Just displays the text as a static item.

Here's a theoretical sample dialog showing some of the possiblities:

  Dialog( CDynamicDialog *pDlg ) 
  {
    static char *sStrings[4] = {"Chain", "Rope", "Cloth", ""};
    pDlg->SetWidth( 1 );
    pDlg->ComboBox( sStrings, &m_nType, CWM_SET_TYPE );	
    pDlg->Space( 5 );
    pDlg->Edit( "Length", &m_fLength );	  
    pDlg->Edit( "Elasticity", &m_fElastic );
    pDlg->AddSpace( 5 );
    pDlg->ColorButton( "Wire Color", &m_WireC );
    pDlg->SetWidth( 2 );  
    pDlg->Button("Copy", CWM_COPY_COLOR);
    pDlg->Button("Paste", CWM_PASTE_COLOR);
  }

Most of these have an optional final parameter that will tell the dialog system to send a message whenever the control is changed. Therefore any code can be run if more than just the variable itself needs to be updated after the variable is changed.

 

Messaging System

The purpose of the messaging system is in some ways similar the C++ overloadable virtual functions. If a message is not processed by the specific object type it can send the message on to the default object message processesor. In my view the general messaging system and well-defined overloadable functions compliment each other and I use them both. Below I give a very general idea of some of the advantages of each.
Advantages for messaging system: Messages can be easily exchanged between the interface and any objects or editors because of their well-defined format. For example, they're easily attached to a control in the dialog system. They can be easily logged. It's also easier to add a new message than a specific function.
Advantages for C++ overloading: More complex parameters are possible(without a pointer to a structure.) There's more compile-time checking. They're better integrated into the IDE's intellisense. Also I usually like to use a separate function for individual messages for clearer code, so this way functions are called directly.

Here's an example of a box object processing messages. I primarily use messages for interfacing with the dialogs, as with create_object, or to throw in some optional small functions like get_tricount. Originally when a pointer was needed I passed it nParam, but not only was that a bit more confusing, it wasn't 64-bit compatible, so I added a data pointer. This example doesn't really show much, but I wanted at least some kind of code sample here.

int cwOBox::ProcMessage( int Msg, int nParam, void *pData )
{
switch (Msg)
  {
  case CWM_CREATE_OBJECT:
    ComputeBoxMesh( );
    return 1;

  case CWM_GET_TRICOUNT:
    return 2 * 2 * ( nSegsW*nSegsL + nSegsW*nSegsH + nSegsL*nSegsH );
	}
return cwObject::ProcMessage( Msg, nParam, pData );
}

 

Asset Manager

chooser windows

Screenshots: Mesh Chooser | Animation Chooser | Texture Chooser

The 3DKC world is made up of objects, but assets are just as important. Asset types include textures, meshes, characters(skeletons), animations, and sounds. Each asset has a unique string name id and numerical id. Objects can use these ids to activate or call functions on specific assets. For example, you can have a mesh object, but instead of holding all the data for the mesh itself, it just stores the id of the mesh asset. This way you can easily create instances of the mesh asset at different locations, and you can overload the default asset shaders and textures (and some other variables) on each individual mesh object.

Asset management is something that will be pretty much the same across asset types, and thus a good area to save time with reusable code. So I have a base asset type, and the asset manager class that does things like loading, saving, keeping track of names, directory structure, unloaded files, and other tasks of that nature. In addition there's the 'chooser' that allows the user to select an asset from a visual display, view its statitics, and manage directories. Adding a new asset type is done by subclassing the base type, and they all get to use all the functionality provided by the asset manager.

Screenshot: Skeleton Editor

Asset Editors: Optionally, asset types can have an asset editor. Since 3DKC can be put in asset editor mode instantly at any time, this makes it very easy to tweak assets at the same time you build a scene or level. Perhaps the asset editor merely allows the user to set game-specific or rendering flags, but it can also be a full featured editor that can completely alter the asset or create it from scratch, as is the case with the skeleton editor, which is too complicated to describe any real details. In overview, the skeleton editor is used to build a bone structure, attach a mesh and set/alter bone attachments and weights. You can save at any time and see how it looks in the world editor, and switch back to the asset editor if more changes are required.

 

Appendix

cwDataIO.h : Code for my data transfer class. Mainly for reference for my 3DKC description, but it should be usable if you want to use it for anything else, just keep the notice.

Weekly Articles related to general editor programming: Managing cg Shaders | Coding Style |Using C++

Weekly Articles related to engine programming: Portal Culling | Shadowing Methods