Programmer 101

Introduction

Graphite is a large piece of software. This document aims at helping the reader to understand the structure of the software, and use it to do research in geometry processing.

Before describing the structure of Graphite, let me just talk a little bit about its history, this may help understanding the technical choices that were made. I started the development of Graphite in 2000, during my post-doc. My goal was to develop a research platform for geometry processing. Since geometry processing often requires interaction with the user, Graphite needed to be an interactive application, with tools, with a GUI etc... Developing this type of things can be extremely time-consuming, and I did not have the time. Therefore, my idea was to automate the process as much as possible. In other words, I wanted Graphite to be "intelligent" enough to generate its GUI automatically (which is nearly the case now). To do so, I used a concept available in most object-oriented language, called "introspection", or "reflection". The idea is that an object can know which class it belongs to, what are the methods available in that class, and what are the arguments of those methods. Then, Graphite's menus and dialog boxes can be generated "on the fly" from this information. The problem is that unfortunately, C++ does not have introspection. When we started, it was then necessary to rewrite all '.h' files in XML, to help Graphite find this meta-information. Fortunately, this is no-longer the case. With Wan-Chiu Li, we developed a technology called GOM (for Graphite Object Model) that adds introspection to C++. An open-source library called SWIG helped us a lot. Starting from the C++ parser included in SWIG, we developed a code-generator (gomgen) and an introspection API (GOM). With GOM, the programmer is freed from the uninteresting task of developing a GUI, GOM can generate a dialog box for all the methods of the classes declared to the system. All what the programmer needs to do is to replace the class keyword with gom_class for the classes that need to be exposed to GOM.

Knowing that, Graphite has a two-level structure. Lower-level classes do not know about GOM, and can be used in other projects, they are reasonably independent on the rest of Graphite. Higher-level classes provide a GOM interface to the objects declared at the lower-level, so that users can see them, apply commands to them etc... Another benefit of the GOM system is that it facilitates interfacing Graphite with a scripting language (GEL, for Graphite Embedded Language). We developed GEL by simply connecting GOM to the object model of Python. Once this connection is made, all GOM objects can be manipulated in Python, as any other Python object. Again, all what the programmer needs to do is to replace the class keyword with gom_class for the classes that need to be exposed to GOM.

Graphite sources are organized into packages (each package is compiled either as a library or as a plugin). The packages are located in src/packages/OGF. OGF stands for Open Graphics Foundation, which does not correspond to anything concrete for now. This just symbolizes our hope that working with Graphite will help people learning about geometry processing, about C++, and also encourage them to share their knowledge and realizations with others as we do.

Graphite structure

Lower levels

  • basic

Contains classes interfaced with the OS, generic containers, plugin management system (plugins are also called 'modules' in Graphite parlance), and the attributes management system (more on this later)

  • math

Geometry processing requires a lot of math, especially numerical analysis, number crunching, and geometry. This library provides object-oriented interfaces to a large number of libraries (most of them were initially developed in Fortran). See Credits for the list of libraries bundled in Graphite.

  • cells

Geometry processing also uses combinatorial topology. The cells library contains representations for point sets, (possibly non-manifold) polygonal lines, manifold polygon meshes, weakly heterogeneous volumetric grids and strongly heterogeneous volumetric grids, together with some algorithms to manipulate them.

  • image

Contains a simple class to represent images, and interface to standard image formats (PNG, JPG ...)

  • renderer and renderer_gl

Graphite has a virtual API that encapsulates rendering (renderer), and an implementation based on OpenGL (renderer_gl). It would be possible for instance to develop a version of Graphite based on DirectX, without changing the rest of Graphite (that only talks to the abstract renderer API).

The Graphite Object Model (GOM)

  • gom

Contains the reflection API (meta_class, meta_method, meta_property ....), the code generator (used by gomgen, see below), and an interface to an XML parser (GOM objects can be serialized to XML file, which is useful for describing some parts of the GUI). In addition, GOM provides a signal and slot mechanism. For instance, when a button is clicked, this emits a signal, that can be connected to a function that reacts to user input (a slot).

  • gom_basic

Contains some basic types exposed to the GOM system, including Connection (that connects a signal to a slot), Component (that enables defining new GOM objects in XML) and Interpreter (the abstract API for scripting languages connected to GOM).

  • gel (Graphite Embedded Language)

GEL connects GOM with the object-model of Python. This makes it possible to manipulate in Python all the objects of Graphite exposed to GOM, without needing to generate additional wrappers.

  • gomgen

Generates GOM meta-information for all the classes of a package.

The framework

  • skin and skin_qt4

skin defines an abstract widget toolkit (a set of GOM objects for Window, Icon, Menu etc...). skin_qt4 is the implementation of skin for Trolltech's QT library. Separating both levels makes Graphite independent on the used toolkit. When we started Graphite in 2000, we were using OSF Motif, then we switched to Qt. In 2006, we switched from Qt3 to Qt4. Each time, we only needed to rewrite skin_qt, without needing to touch the rest of Graphite. We also got an experimental version of skin for GTK, and another one completely in OpenGL (based on a toolkit named GLGooey).

  • scene_graph

scene_graph defines all the basic concepts that the user can see in Graphite, namely:

  • Grob: (Graphite Object) base-class for the objects that are listed in the scene-graph
  • Shader: a possible way of visualizing a Grob
  • Command: a class that should appear as menus and dialog boxes in the GUI. Commands are associated with a Grob.
  • Tool: a way of interacting with a Grob using the mouse.

Plugins

  • surface

Introduces types for point-sets, possibly non-manifold polygon lines and manifold polygon meshes. These are Grob wrappers around types defined in cells.

  • volume

Introduces types for voxel grids, weakly and strongly heterogeneous grids. These are Grob wrappers around types defined in cells.

  • parameterizer

Introduces new commands, new tools and new shaders for surfaces.

  • quick_start

This one is a "sand box", meant for you to try creating new commands, without the hassle of creating a new plug-in (we will see later how to do that).

  • devel

This plugin let you create quickly new plugin skeletons for Graphite. Learn more on Devel.

First adventures in Graphite: playing with the quickstart plugin

Now the best way of understanding how Graphite works is to try modifying it. In order to ensure that you do not wreck your version, there is a "sandbox" plugin, called quick_start, that declares commands, shaders and tools attached to the Surface object. Modifying these commands, shaders and tools is a good starting point for learning how to program in Graphite.

1) Activate quick_start in Graphite

  • run Graphite
  • Menu File->Preferences
  • Modules tab
    • enter quick_start (in the zone right to the Add... button (1) )
    • press the Add... button (now quick_start should appear in the list)
    • press the Save configuration file button (2)
    • quit and restart Graphite

2) See what quick_start offers

Quickstart registers new commands, shaders and tools attached to the Surface object. We will see how they appear and how they are coded.

Commands

In Graphite, commands are what you invoke from a menu. They are applied to the current object (the one selected in the object list, on the left of Graphite's window). They can have parameters, that are entered in dialog boxes. We will now see how this works, and how to create a new command.

If you take a look at OGF/quick_start/commands/surface_quick_start_commands.h, you will see :

    enum RobotType {
        r2d2    = 0,
        z6po    = 1,
        krag    = 2,
        malla   = 3,
        G7zark7  = 4,
        G1nonos1 = 5,
        nono    = 6
    } ;

    gom_class QUICK_START_API SurfaceQuickStartCommands : public SurfaceCommands {
    public:
        SurfaceQuickStartCommands() { }

    gom_slots:
        void command_example(
            double my_value,
            int my_integer,
            const Color& my_color,
            bool my_flag,
            RobotType my_robot,
            const std::string& my_string
        ) ;

        void algorithm_under_test(
            double arg1 = 0, double arg2 = 0, double arg3 = 0,
            double arg4 = 0, double arg5 = 0, double arg6 = 0
        ) ;

    } ;

The class SurfaceQuickStartCommands has two member functions (command_example and algorithm_under_test) that will appear as entries in the QuickStart menu. The gom_class keyword indicates that this class should appear in the GOM system, and the gom_slots keyword identifies the functions that correspond to commands (for which GOM will generate a dialog box). The QUICK_START_API keyword is there to overcome some oddities in Visual C++, that requires one to manually declared which symbols are exported and imported by DLLs (if you want to know, it expands to __declspec(DllExport) when quick_start is compiled, or __declspec(DllImport) otherwise, and is ignored under Linux).

To see the menu, load a surface in Graphite. Each entry of this menu opens a dialog box. The entries in these dialog boxes correspond to the parameters declared to the functions. The nice thing is that each dialog box is "automagically generated" by Graphite. Note the RobotType enum, converted into a combo box with entries that correspond to the symbolic values of the enum (you can even declare how your own data types are mapped to widgets, but we will see that later). Now let us try something: you will add your own function there. Let's insert its declaration, right after algorithm_under_test, for instance :

   void my_first_function_in_Graphite(
      const std::string& s= "hello, world",
      int nb_iter = 10,
      double epsilon = 0.1
   ) ;

Now insert the body of the function in OGF/quick_start/commands/surface_quick_start_commands.cpp:

   void SurfaceQuickStartCommands::my_first_function_in_Graphite(
      const std::string& s,
      int nb_iter,
      double epsilon 
   ) {
      Logger::out("SurfaceQuickStartCommands") << " s=" << s << " nb_iter=" << nb_iter << " epsilon=" << epsilon << std::endl ;
   }

Now recompile Graphite (using Build project in MSVC, or make from the build directory under Linux. You will see some messages saying that GOM is re-generating some files.

Restart Graphite, load a surface, open the GEL terminal (GEL->Show terminal, and invoke my first function in Graphite from the QuickStart menu. Congratulations ! You just created your first dialog box (without writing any line of GUI code, GOM is doing all the work for you). The Reset button reloads "factory setting", i.e. default values declared in the .h file.

Now in the GEL terminal, try this:

  cmd = scene_graph.current().query_interface("QuickStart") ;
  gel.inspect(cmd) ;

You will see that GEL also knows about your new function. You can try calling it:

  cmd.my_first_function_in_Graphite("foobar", 1, 2.3) ;

Congratulations ! You just declared your own extension to the GEL language (again without writing a single line of code). Your own function can be called like any Python function (GEL = Python + GOM).

Summary:

  1. Simply extend a Commands class, and GOM does all the hard work for you.
  2. If what you need to do is just creating a couple of functions, then try modifying OGF/quick_start/commands/surface_quick_start_commands.h and .cpp.
  3. You can also create your own plugin, and Graphite knows how to do that for you (remember, I'm lazy, I'd rather let Graphite do the hard work). We will see that later.

Shaders

Depending on the application (or the research project), one may need to display Graphite object in different ways, possibly not planned by us and not implemented in Graphite Core. This will be even more obvious when you will learn about Graphite Attributes, that allow information to be attached to a mesh dynamically. Fortunately, it is simple to extend Graphite with new ways of displaying objects, that are called shaders. One can change the shader associated with the current object by selecting it in the pull down menu, top right of Graphite's main window.

quick_start adds two shaders. Let us take a look at the one named QuickStart. There is a surface shrink parameter that you can play with, and a similar parameter for the mesh. Let us now take a look at how this is implemented (in OGF/quick_start/shaders/quick_start_surface_shader3d.h):

    gom_class QUICK_START_API QuickStartSurfaceShader3d : public PlainSurfaceShader3d {
    public:
        QuickStartSurfaceShader3d(Surface* grob) ;
    gom_properties:        
        void set_surface_shrink(int x) { surface_shrink_ = x ; update() ; }
        int get_surface_shrink() const { return surface_shrink_ ;         }
        void set_mesh_shrink(int x)    { mesh_shrink_ = x ; update() ;    }
        int get_mesh_shrink() const    { return mesh_shrink_ ;            }

    protected:
        virtual void draw_surface(RenderingContext* out) ;
        virtual void draw_mesh(RenderingContext* out) ;

    private:
        int surface_shrink_ ;
        int mesh_shrink_ ;
    } ;

No big surprise, this is again a gom_class. This means there is an automatic system that generates the GUI for us. The elements of that GUI are those declared as gom_properties. A gom_property is simply defined by a name, a type, and a get_, set_ pair of functions. In case you wonder why: there are more elements in the GUI than those declared here, this is because GOM is aware of inheritance. The other elements are declared in the base class, see OGF/surface/shaders/plain_surface_shader3d.h and its ancestor.

Tools, and much more...

You are not limited to what we just showed you! Beside commands and shaders, you also can create tools to interact with a grob using the mouse, and your own grobs. however, these are beyond the scope of this introductory tutorial. Now that you have completed your first plugin modification, you should try to create one yourself, as it's the best way to get to the next level (QuickStart is just a sandbox, and you don't want to play in a sandbox all your life, do you ?).

But remember, the idea is to spend more time on what is important, your algorithms, and to keep to a minimum the work needed to integrate it in Graphite. That's why we created Devel, a plugin to write plugins!

Devel

Now that you have understood how this works, you can use the Devel plugin to generate the skeletons for you. Start either with the Devel Tutorial or with a more formal description, in the Devel Manual.