3D primitives visualization using a virtual interface

O.Couet

(last update 16/07/2004)

This paper describes how to handle the 3D primitives visualization in ROOT using various 3D visualization engines (X3d, OpenGL, coin3d, OIV etc...).

Content
  1. The current status
  2. The new proposed approach
    1. The abstract interface
    2. TPad extensions
    3. CreateScene
    4. SHAPE::Paint
    5. TBuffer3D

1. The current status

3D primitives, like all the ROOT objects to be drawn, are stored in the current active TPad. 3D primitives can be visualized in three different ways:
  1. In the current TPad using a simple 3D visualization technique based on line drawing. This technique is based on the TView class. It can handle line attributes like colors. Some interactions like rotation are possible. Pictures can be save in gif, PostScript, PDF and SVG formats.

  2. Using the X3d tool. A separated window is created in which the 3D scene is drawn. The drawing is very fast even on big data structure. This tool is very convenient for fast interactive manipulations. Several visualization techniques can be used: hidden surface or hidden line removal, stereo view. But the drawing is often wrong and changes depending on the point of view. It is not possible to generate PostScript output.

  3. Using OpenGL. A separated window is created in which the 3D scene is drawn using the OpenGL graphic library. The scene is rendered using the ZBuffer technique. The rendering is very precise. Depending on the hardware capabilities (OpenGL often uses 3D hardware accelerators) the interactive manipulation is feasible. It is not possible to generate vector PostScript output.
The rendering of each primitive is done by specific Paint methods. The Paint methods contain specialized code ("if" statements) depending on the way the user choose to visualize the scene (in the Pad, using X3d or OpenGL). Note that the scene is always rendered in the current Pad even if it is displayed with OpenGL or X3d viewers.

The code of a typical Paint method is shown below. It contains special code to fill the X3D buffer (a data structure used by the X3D renderer), paint the OpenGL points and paint the primitive in the current Pad.

      void TBRIK::Paint(Option_t *option)
      {
      // Allocate memory for points
      
         SetPoints(points);
      [...]
         if (!rangeView  && gPad->GetView3D()) PaintGLPoints(points);
      
      // Allocate memory for segments
      [...]
      
      // Allocate memory for points
      [...]
      
      // Allocate memory for polygons
      [...]
      
          // Paint in the pad
          PaintShape(buff,rangeView);
      
          if (strstr(option, "x3d")) FillX3DBuffer(buff);
      [...]
      }
This example shows the TBRIK::Paint method. TBRIK is part of the old 3D primitives collection which is now replaced by the TGeo system. Nevertheless, the TBRIK case is still valid because the TGeo primitives are rendered in the same way by the TGeoPainter class.

The X3d and OpenGL viewers are invoked using a special method of TPad called x3d. The method has an option which can be equal to "opengl" or "x3d" depending which viewer one wants to use. This method's code looks like the following:

      void TPad::x3d(Option_t *option)
      {

         if (!strcasecmp(option, "OPENGL")) {
            // Disconnect the previous 3D context
            if (fPadView3D) fPadView3D->SetPad(0);
            fPadView3D = gVirtualGL->CreatePadGLView(this);
            if (!fPadView3D) {
               gROOT->LoadClass("TGLKernel", "RGL");
               fPadView3D = gVirtualGL->CreatePadGLView(this);
               if (!fPadView3D) Error("x3d", "OpenGL shared libraries not loaded");
            }
            if (fPadView3D) { 
                Modified();
                Update();
            }
            return;
         }   

         TPluginHandler *h;
         if ((h = gROOT->GetPluginManager()->FindHandler("TViewX3D"))) {
            if (h->LoadPlugin() == -1) return;
            h->ExecPlugin(5,this,option,"X3D Viewer",800,600);
         }
      
      }
In this code, two different techniques are used to invoke the OpenGL or X3d viewers. They both allow to load the graphic libraries only when needed.

The way it is done in the OpenGL case is now obsolete and we will prefer the technique used in the X3d case, using the ROOT "Plugin Manager".

To use a class in the "Plugin Manager", it has to be declared in the file $ROOTSYS/etc/system.rootrc. The class TViewerX3D is declared as "Plugin" in the file $ROOTSYS/etc/system.rootrc the following way:

Plugin.TViewerX3D:  *  TViewerX3D    X3d    "TViewerX3D( [...] )"

2. The new proposed approach

The basic requirements are:

2.1 The abstract interface

To access the various 3D packages in a transparent and uniform way, a common abstract interface will be used.

This abstract interface (virtual class) will be named TVirtualViewer3D. Its concrete implementations will be:

TViewerX3D, TViewerOpenGL, TViewerOIV inherit from TVirtualViewer3D.

The class TVirtualViewer3D will start a new "3D viewer" in a separated window. Like now, starting a new "3D viewer" will be triggered by TPad::x3d (a better name name for this method can be find later on) The new TPad::x3d will be much simpler than the current one:

      void TPad::x3d(Option_t *option)
      {
         // Invokes a 3D viewer
         
         fViewer3D = 0;
         fViewer3D = TVirtualViewer3D::Viewer3D(option);
         if (fViewer3D) {
            fViewer3D->CreateScene(option);
         } else {
            Error("x3d", "Cannot load 3D viewer with option: %s",option);
         }
      }
fViewer3D belongs to TPad. It is a pointer to TVirtualViewer3D.

TVirtualViewer3D::Viewer3D is a static method in TVirtualViewer3D which returns a pointer to a TVirtualViewer3D. This method uses the plug-in manager facility:

      TVirtualViewer3D* TVirtualViewer3D::Viewer3D(Option_t *option)
      {
         // Create a Viewer 3D according to "option"
      
         TVirtualViewer3D *viewer = 0;
         TPluginHandler *h;
         if ((h = gROOT->GetPluginManager()->FindHandler("TVirtualViewer3D", option))) {
            if (h->LoadPlugin() == -1) return 0;
            viewer = (TVirtualViewer3D *) h->ExecPlugin(1, gPad);
         }
         return viewer;
      }
The FindHandler method directly pass as second parameter the TPad::x3d option. This will allow to pick directly the right concrete implementation of TVirtualViewer3D in the file $ROOTSYS/etc/system.rootrc which will be something like that:
Plugin.TVirtualViewer3D:    x3d TViewerX3D    X3d   "TViewerX3D(TVirtualPad*)"
+Plugin.TVirtualViewer3D:   ogl TViewerOpenGL OGL   "TViewerOpenGL(TVirtualPad*)"
+Plugin.TVirtualViewer3D:   oiv TViewerOIV    OIV   "TViewerOIV(TVirtualPad*)"
TVirtualViewer3D.h :
      //////////////////////////////////////////////////////////////////////////
      //                                                                      //
      // TVirtualViewer3D                                                     //
      //                                                                      //
      // Abstract 3D primitives viewer. The concrete implementations are:     //
      //                                                                      //
      // TViewerX3D   : X3d viewer                                            //
      // TViewerOpenGL: OpenGL viewer                                         //
      // TViewerOIV   : Open Inventor                                         //
      //                                                                      //
      //////////////////////////////////////////////////////////////////////////

      class  TVirtualViewer3D {

      protected:
         TVirtualPad    *fPad;       // pad to be displayed in a 3D viewer

      public:
         TVirtualViewer3D();
         TVirtualViewer3D(TVirtualPad *pad);
         virtual     ~TVirtualViewer3D();
         virtual void CreateScene(Option_t *option); // Creates a 3D scene from the TPad
         virtual void UpdateScene(Option_t *option); // Called by TBuffer3D::Paint

         static  TVirtualViewer3D *Viewer3D(Option_t *option);

         ClassDef(TVirtualViewer3D,0) // Abstract interface to 3D viewers
      };

2.2 TPad extensions

To implement the new scheme TPad should be extended in the following way:

New data members:

     TBuffer3D        *fBuffer3D;         // Contains the current primitive description
     TVirtualViewer3D *fViewer3D;         // Current TVirtualViewer3D
New methods:
     TBuffer3D *AllocateBuffer3D(Int_t n) // Allocates the needed space in fBuffer3D
     TBuffer3D *GetBuffer3D()             // Returns a pointer to fBuffer3D 
     TVirtualViewer3D *GetViewer3D() {return fViewer3D;}

2.3 CreateScene

Each real class inheriting from TVirtualViewer3D should implement the method TVirtualViewer3D::CreateScene. The skeleton of this method is something like that:
      void TVirtualViewer3D::CreateScene(Option_t *option)
      {
         TObject *obj;


         // Loop over all the primitives inheriting from TAtt3D   
         // Depending on the viewer we might have several loops
         // like that with different option values 
         TObjLink *lnk = fPad->GetListOfPrimitives()->FirstLink();
         while (lnk) {
            obj = lnk->GetObject(); 
            if (obj->InheritsFrom(TAtt3D::Class())) {
               obj->Paint(option);  
            }
            lnk = lnk->Next();
         }

      }
Because obj->Paint is recursive we cannot fill the 3D scene graph or compute the scene size directly in CreateScene. We need the UpdateScene called from gPad:
      void TVirtualViewer3D::UpdateScene(option)
      {
         // Called by TBuffer3D::Paint

         if (option == 'bla') [do something]
         if (option == 'bli') [do something else]
      }

2.4 SHAPE::Paint

      void SHAPE::Paint(Option_t *option)
      {
         Int_t NbPnts = ...;
         Int_t NbSegs = ...;
         Int_t NbPols = ...;


         // A) Allocate the needed space in the TBuffer3D in gPad 
         TBuffer3D *buff = gPad->AllocateBuffer3D(3*NbPnts, 3*NbSegs, 6*NbPols);
         if (!buff) return;


         // In case of option "size" it is enough to fill the buffer sizes 
         buff->fNbPnts = NbPnts;
         buff->fNbSegs = NbSegs;
         buff->fNbPols = NbPols;
         if (strstr(option,"size")) {
            buff->Paint(option);
            return;
         }


         // B) Fill TBuffer3D 
         buff->fPnts[ 0] = -fDx; buff->fPnts[ 1] = -fDy; buff->fPnts[ 2] = -fDz;
         ...

         ... convert the points coordinates into the master reference system 
             with fGeom->LocalToMaster() is case of TGeo shapes and
             with gGeometry->Local2Master() in case of TShapes ... 

         buff->fSegs[ 0] = c   ; buff->fSegs[ 1] = 0   ; buff->fSegs[ 2] = 1   ;
         ...
         buff->fPols[ 0] = c   ; buff->fPols[ 1] = 4   ;  buff->fPols[ 2] = 0  ;
         ...


         // C) Paint buff 
         buff->Paint(option);
      }
For performance reasons we might need some special cases in SHAPE::Paint. For instance In the X3D case we have to compute the memory size needed by the X3D scene before filling it (option size) so a first pass over all the primitives in the pad is required. In that case the step B) is not needed.

For performance reasons again, we will not use the option parameter because finding the option value with strstr() takes time. Instead we will use a Int_t (or bit) data member of TBuffer3D.

      enum EBuffer3DOption {kPAD, kRANGE, kSIZE, kX3D, kOGL};
TBuffer3D will have an parameter called option which will be set once before looping over all the shapes, and when needed the option value will be tested that way:
      if (buff->fOption == TBuffer3D::kSIZE) {
         buff->Paint(option);
         return;
      }

2.5 TBuffer3D

This new class contains a primitive description. It consists in Points, Edges and Polygons. It is very close to the current C struct X3DBuffer:
      class TBuffer3D : public TObject {

         private:

         public:
            enum EBuffer3DType {kBRIK,   kPGON, kPCON, kSPHE,   kTUBE,   kTUBS,
                                kTORUS,  kXTRU, kLINE, kCSHAPE, kPARA,
                                kM3DBOX, kMARKER };      

            enum EBuffer3DOption {kPAD, kRANGE, kSIZE, kX3D, kOGL};

            TBuffer3D();
            TBuffer3D(Int_t n1, Int_t n2, Int_t n3);
            virtual  ~TBuffer3D();

            void ReAllocate(Int_t n1, Int_t n2, Int_t n3);
            void Paint(Option_t *option);

            TObject  *fId;       // Pointer to he original object
            Int_t     fOption;   // Option (see EBuffer3DOption)
            Int_t     fType;     // Primitive type (see EBuffer3DType)
            Int_t     fNbPnts;   // Number of points describing the shape
            Int_t     fNbSegs;   // Number of segments describing the shape
            Int_t     fNbPols;   // Number of polygons describing the shape
            Int_t    *fSegs;     // c0, p0, q0, c1, p1, q1, ..... ..... ....
            Int_t    *fPols;     // c0, n0, s0, s1, ... sn, c1, n1, s0, ... sn
            Int_t     fPntsSize; // Current size of fPnts
            Int_t     fSegsSize; // Current size of fSegs
            Int_t     fPolsSize; // Current size of fSegs
            Double_t *fPnts;     // x0, y0, z0, x1, y1, z1, ..... ..... ....

            ClassDef(TBuffer3D,0) // 3D primitives description
      }
Note that in case of primitives like 3D polylines the number of polygons will be 0. Only points and segs are useful in that case (this is how it is currently implemented in TPolyLine3D::Paint).

Even if the shape description is independent from the shape type, it might be useful to know the shape's type (see EBuffer3DType, fType) to allow special treatment in particular 3D viewer.

TBuffer3D::Paint :

A possible implementation could be the following:

      void TBuffer3D::Paint(Option_t *option)
      {
         Int_t i, i0, i1, i2;
         Double_t x0, y0, z0, x1, y1, z1;
         TVirtualViewer3D *viewer3D;
         TView *view;
      
         // Compute the shape range and update gPad->fView
         switch (fOption) {
            case kRANGE:
               x0 = x1 = fPnts[0];
               y0 = y1 = fPnts[1];
               z0 = z1 = fPnts[2];
               for (i=1; i x1 ? fPnts[i0] : x1;
                  y1 = fPnts[i1] > y1 ? fPnts[i1] : y1;
                  z1 = fPnts[i2] > z1 ? fPnts[i2] : z1;
               }
               view = gPad->GetView();
               if (view->GetAutoRange()) view->SetRange(x0,y0,z0,x1,y1,z1,2);
               break;
      
            // Update viewer
            case kSIZE:
            case kX3D:
            case kOGL:
               viewer3D = gPad->GetViewer3D();
               if (viewer3D) viewer3D->UpdateScene(option);
               break;
      
            // Paint this in gPad
            case kPAD:
            default:
               if ( fType==kMARKER ) {
                  view = gPad->GetView();
                  Double_t pndc[3], temp[3];
                  for (i=0; iWCtoNDC(temp, pndc);
                     gPad->PaintPolyMarker(1, &pndc[0], &pndc[1]);
                  }
               } else {
                  for (i=0; iPaintLine3D(ptpoints_0, ptpoints_3);
                  }
               }
               break;
         }
      }