CS40 Midterm Project: Hierarchical Modeling with Scene Graphs

Due 11:59pm Saturday 20 March 2009
You may work with one partner on this assignment. For this lab, you will be implementing a scene graph (.gfx) parser and renderer. Many of the low level pieces of code have been provided for you, but it will take some time to digest the structure of the code. Follow examples to guide your additions. As a general rule, you do not need to modify/add any .h header files unless it is to uncomment functionality in gfx_reader.h. You will need to edit and add a small number of .cpp files.
Hierarchical Modeling
The goal of this project is to build an abstract hierarchical modeling system. The idea is to separate the modeling of a scene from the rendering of the scene. Scenes can be composed in a simple text format (.gfx described below) and then parsed and rendered by software. You will complete elements of the parser, implement a renderer, and then compose a hierarchical scene to display.
Simple Object Format (.obj)
We have talked about one way to model simple object using the obj (separate wiki summary) file format. The code provided to you handles a slightly modified model that does not support colors using an .mtl file or groups, but instead uses a separate tag vc to indicate colors of vertices. A vertex color can be specified using vc r g b a for the red, green, blue, and optional alpha components of the color. The color of vertex can be used when describing faces by adding the vertex color id after the normal id. For example f 1///1 2///1 3///1 describes a face with three vertices with geometry ids 1,2, and 3, no texture or normal components and color component corresponding to the 1st vc tag.

The obj format can only specify simple objects and cannot form hierarchical groups of objects. The code in gfx_parser_obj_simple.* can parse a simple object from a input file. The abstract struct that defines the internal representation of a simple object is in gfx_obj_simple.h. The function render_obj_simple in test_basic_gfx.cpp displays simple objects using OpenGL by converting the struct to appropriate OpenGL commands.

Scene format (.gfx)
For richer scenes, we will use a different (.gfx) file format. This file format can describe four general features: simple objects, composite objects, lights, and scenes. We describe the syntax of each of these features below. An example sample.gfx file in your lab directory gives concrete examples of each of these features.

Simple Objects

Simple objects can be specified in one of two ways:
OBJECT {name} SIMPLE
   {.obj syntax}
END
or
OBJECT {name} SIMPLE
   FILE {filename}.obj
END
where {name} and {filename} can be any alphanumeric string that begins with a letter.

Lights

The properties of a light can be specified using the follow syntax:
LIGHT {name} 
    # color for ambient, diffuse, and specular
    AMBIENT  {r g b a}
    DIFFUSE  {r g b a}
    SPECULAR {r g b a}
    # constant, linear, and quadratic attenuation
    CA {float}
    LA {float}
    QA {float}
END
Any of the fields above are optional, and the alpha value is optional in the color tags. Color values should be in the range 0 to 1, inclusive. Attenuation values should be positive. See gfx_command_light.h for details on how you should store information for lighting.

Composite Objects and Scenes

Composite object and Scenes are very similar. A composite object has the form:
OBJECT {name} COMPOSITE
  {List of Composite Commands}
END
A scene object has the form:
SCENE {name}
  {List of Composite and Scene Commands}
END
Each .gfx must have a SCENE named main. Commands are described below. Unless otherwise specified, a given command can appear in either a composite object or a scene.
Commands
Commands for scenes and composite objects have semantics similar to the openGL commands of the similar name. If you have questions about any of the commands below, let me know.
TRANSLATE {x y z}
ROTATE {deg x y z}
SCALE {x y z}
COLOR {r g b a}
PUSH
POP
SEPARATOR
For the color command, the alpha component is optional. A separator command is a POP followed by a PUSH. (Ironically, typing SEPARATOR is longer than typing POP\nPUSH, but you should support it anyways)
SUBOBJ {name}
The command SUBOBJ draws the specified object. The parameter name should refer to a composite or simple object defined earlier in the .gfx file.
GLUT {type} {params}
The GLUT command draws a solid glut object. Valid types are SPHERE, CUBE, TORUS, ICOSAHEDRON, OCTAHEDRON, TETRAHEDRON, DODECAHEDRON, CONE, and TEAPOT. Some of these shapes take one or more parameters. For a full description see pg764 of the OpenGL redbook or the glut docs. The struct GfxCGlut defined in gfx_command_glut.h can hold the necessary info for each type of GLUT object. The LOOKAT and LIGHT command are only valid in scenes and not in composite objects.
LOOKAT {eyex eyey eyez aimx aimy aimz upx upy upz}
LIGHT {name} {x y z} {w}
The light command indicates that the scene should place the light defined earlier in the .gfx with the name name should be placed at the location specified. The parameter w should be 0 for directional lights or 1 for positional lights. If unspecified, w should be interpreted as 0.
Writing the Parser
The class GfxReader in gfx_basic_reader.h has a complete definition of .gfx and .obj parsers. The implementation of the .obj parser has also been included in gfx_reader.cpp and gfx_parser_obj_simple.cpp. You will need to add functionality to parse the .gfx format. The is mostly done by writing the function doReadGfxGroup in gfx_reader.cpp and writing some other parsers for lights and composite objects/scenes in gfx_parser_light.cpp and gfx_parser_composite.cpp respectively. You will need to create both of these files. They should implement the function in their corresponding .h files, after which , you can uncomment some of the TODO items in gfx_reader.cpp.

Parsers for most of the commands that can appear inside composite objects and scenes are defined in gfx_parser_core.h and implemented in gfx_parser_core.cpp. The exceptions are GLUT commands and LIGHT commands which should be implemented in gfx_parser_glut.cpp and gfx_parser_light.cpp.

Writing the parser shouldn't be too bad once you understand some of the basic techniques used to parse simple objects and a few of the commands in gfx_parser_core.cpp.

Writing the Renderer
To test your parser, you will write a renderer in test_basic_gfx.cpp (or write your own class), to render the main scene in a .gfx file. See render_composite in test_basic_gfx.cpp for an idea of how to get started. If things are working correctly, you should be able to display the sample.gfx which should look like the image below
A sample image. Anyone read alt tags?
Creating a Scene
Write your own .gfx file that specifies a scene and render that scene with your code. A few obj files have been placed in /usr/local/doc/ if you would like to include them in your scene. You may want to check the bounding boxes of these objects before placing them in your scene.

Add camera controls so that you can navigate through your scene. You should just need to adjust your lookat function to walk through the scene. You should be able to translate and rotate the camera using keyboard or mouse controls.

Where to start
The code may look intimidating, but hopefully it isn't too bad. Start by looking over the basic structure of doReadGfxGroup in gfx_basic_reader.cpp. This shows how the parser identifies the OBJECT {name} SIMPLE like and parses a simple object by calling the appropriate parser retval = GfxObjSimpleParser::process( item->ptr.simple,fin, m_pInput, &msg );. You will need to do something similar to handle composite objects, lights, and scenes. Not that doReadGfxGroup places objects, lights, and scene into a linked list of GfxGroupItem objects. The details of group items can be found in gfx_group.h.

Parser commands is handled somewhat differently. The implementations for parsing TRANSLATE, PUSH, and POP should give you a good idea of how to parse the others. SUBOBJ, LIGHT, and GLUT may be a bit more tricky. Recall that the commands for LIGHT and GLUT are implemented in separate files to keep the code a bit more manageable.

The utilities in gfx_input.h will do most of the heavy lifting related to file I/O and getting string, name, and number tokens. You can get an idea of how it works by looking at some of the parsers mentioned above.

The renderer is pretty easy once the parser is done. Just read the list of commands from the GfxScene object (g_pScene in test_basic_gfx.cpp) and translate the data in the command into OpenGL. Note that GfxScene is identical to GfxObjComposite defined in gfx_command_core.h.

If you have any questions about the structure or use of the supplied code, let me know and I will add updates and clarifications as need to this page.

Summary
For this project you should complete the implementation of the parser, as specified above, implement a renderer for models parsed by the gfx parser, and create a scene of your own design that you can navigate through. You may add new commands to the parser if you wish, but this in entirely optional at this point.

Looking at the project top-down may also provide some insight. The actual executable is test_basic_gfx.cpp. The renderer needs to be implemented in the function render_composite, which takes a single GfxObjComposite* as a parameter. This pointer corresponds to the main scene and is found by the find_scene function in test_basic_gfx.cpp which takes as one of its two parameters a list of group items (OBJECTS, LIGHTS, and SCENES). This GfxGroup object is set up using the method readGfxGroup in the GfxReader class in the gfx_basic_reader.* files. The GfxReader class is the main parser, but most of the work is offloaded to several helper classes and functions.

readGfxGroup is just a thin wrapper around doReadGfxGroup which reads individual group items (OBJECT, LIGHT, and SCENE). The simple object case is done for you, but you need to add code to handle the other cases. In each case, you follow a similar pattern to the simple object case and call an appropriate parser, e.g., GfxLightParser in gfx_parser_light.cpp for LIGHT group items, and GfxObjCompositeParser in gfx_parser_composite.cpp for composite objects and scenes.

Scenes and composite objects are similar enough that they can be processed by the same function. The only real difference is that some commands are valid only for scenes and not composite objects. To handle this, the GfxReader class contains two hash tables that map command names to their appropriate parsers. See m_t_cps_parser and m_t_scene_parser for examples. These hash tables are passed as the third parameter to GfxObjCompositeParser::process. The header file lists this hash table as void * lookup, but you can/should change this to GfxHashTable<string, GfxCommandParser*> * lookup. You may need to also add #include "gfx_parser_core.h" to the list of header files in gfx_parser_composite.h.

In the composite object parser, you will just be parsing a list of commands. The general model for this is read in each line until you reach the "END" token. For each line, pull off the first string of any non-empty or non-commented line for the name of the command and check if the command has a parser in the hash table. If so, call that command parser and save the command info in a list of commands inside the GfxObjComposite * pObj.

Testing
Testing this incrementally can be a bit daunting. It may seem like an all or nothing approach, but you can definitely proceed incrementally. Start by stubbing out the composite object parser by just reading from the input file and echoing back lines until you reach the "END" token for the group item. Once you do this, have your test_basic_gfx.cpp try and parse the file. If you are lucky, it will crash with an error when it cannot find the parser for "CA" when trying to read the light group item. This means you will need to stub out the light parser in the same way. If you get through this step, you will be able to read all the lines in the file (but parse only a few of them). No go back and modify your composite object and light parsers to actually parse the individual lines in the group item block. Look at the translate parser for examples on how to do this. Once you have this core of your parser working, make sure the items linked list in doReadGfxGroup is correctly populated so that the pGroup struct is initialized correctly. Once this is done, check in your test_basic_gfx.cpp code that you are actually getting the correct names of your group items back from ReadGfxGroup. At this point, find_scene should work and it is off to write and test the renderer. This part is fairly straightforward to debug as you can bring one feature up at a time and check if it is correctly implemented.
Submit
Once you are satisfied with your programs, hand them in by typing handin40 at the unix prompt. You may run handin40 as many times as you like, and only the most recent submission will be recorded. This is useful if you realize after handing in some programs that you'd like to make a few more changes to them.