4

I am having trouble designing the part of my application that deals with geometry. In particular, I would like to have a hierarchy of classes and separate methods for intersections.

The problem

The hierarchy would be something like this:

  • Geometry
    • Mesh
    • Parametric
      • Box
      • Sphere

And the intersection methods something like:

namespace intersections
{
  bool intersection( const Box &, const Box &);
  bool intersection( const Box &, const Sphere &);
}

This is quite simple. The problem arises now, when I want to store all geometries together in a single structure, say for example a std::vector (or a KD-Tree, or whatever).

To do that, I need to use a std::vector<Geometry*>. However, reading from this vector would get me Geometry* objects and thus I have no way of calling the appropriate intersection function.

Example of the problem:

std::vector<Geometry*> arrGeometry;

// add the elements
arrGeometry.push( new Box() );
arrGeometry.push( new Sphere() );

// ... some more code

// try to intersect them?
Geometry* g1 = arrGeometry[0];
Geometry* g2 = arrGeometry[1];
bool intersecting = intersections::intersection( g1, g2 ); //< As expected, this does
                                                          // not work

If I implemented the algorithms inside the geometry objects the problem could be solved by a visitor and some pretty weird function call bouncing.

However, I would like to keep the intersection algorithms outside the Geometry classes. the reasons are:

  • to avoid deciding which should have the ownership (eg. where do you implement the intersection between box and sphere, in Box or in Sphere?)

  • to avoid cluttering the geometry objects will all that can be done to geometry, which is quite a lot (just to name a few: voxelize it, compute intersections, applying constructive geometry operators...). Thus, separating logic from data is quite desirable here.

On the other hand, I need to have a hierarchy instead of templates because for some things the specific geometry can be abstracted away... (eg. for storing it in a std::vector, or a KD-Tree, or... ).

How would you solve this? Is there any design pattern appropriate for this? I tried looking at some libraries but I ended up more confused that I already was...

The easiest way (which is sometimes used) is to use RTTI (or to fake it) and downcastings, but that is not exactly maintainable... (adding a new geometry implies change a lot of switch statement though all the code).

Any thoughts?

Thanks you very much in advance.

jmeseguerdepaz
  • 332
  • 2
  • 7

3 Answers3

2

I have in mind another solution, if you are concerned about speed, this have O(1) complexity, but you may need program for auto generation of c++ code for mode geometry types. You can make array of intersection functions (pseudocode, i don't have any compiler at hand):

enum GeometryType
{
    TypeMesh,
    TypeParamBox,
    TypeParamSphere,
    MaxType,
};

bool intersection( const Mesh*, const Mesh* );
bool intersection( const Mesh*, const Box* );
bool intersection( const Mesh*, const Sphere* );

bool intersection( const Box*, const Mesh* );
bool intersection( const Box*, const Box* );
bool intersection( const Box*, const Sphere* );

bool intersection( const Sphere*, const Mesh* );
bool intersection( const Sphere*, const Box* );
bool intersection( const Sphere*, const Sphere* );

template<class T1,class T2>
bool t_intersection( const Geometry* first, const Geometry* second )
{
    return intersection( static_cast<const T1*>( first ), static_cast<const T1*>( second ) );
}

typedef bool (*uni_intersect)( const Geometry*, const Geometry* );

const uni_intersect IntersectionArray[] = // 2D array all possible combinations
{
    t_intersection<Mesh,Mesh>,
    t_intersection<Mesh,Box>,
    t_intersection<Mesh,Sphere>,

    t_intersection<Box,Mesh>,
    t_intersection<Box,Box>,
    t_intersection<Box,Sphere>,

    t_intersection<Sphere,Mesh>,
    t_intersection<Sphere,Box>,
    t_intersection<Sphere,Sphere>,
};

bool main_intersection( const Geometry* first, const Geometry* second )
{
    const unsigned index = (unsigned)(first->Type) * (unsigned)MaxType + (unsigned)(second->Type);
    return IntersectionArray[ index ]( first, second );
}

Or alternative:

const uni_intersect IntersectionArray[] = // 2D array all possible combinations
{
    t_intersection<Mesh,Mesh>,
    t_intersection<Mesh,Box>,
    t_intersection<Mesh,Sphere>,

    nullptr, // skip mirrored functions
    t_intersection<Box,Box>,
    t_intersection<Box,Sphere>,

    nullptr,
    nullptr,
    t_intersection<Sphere,Sphere>,
};

bool main_intersection( const Geometry* first, const Geometry* second )
{
    const unsigned Type1 = unsigned(first->Type);
    const unsigned Type2 = unsigned(second->Type);
    unsigned index;
    if( Type1 < Type2 )
        index = Type1 * (unsigned)MaxType + Type2;
    else
        index = Type1 + Type2 * (unsigned)MaxType;

    return IntersectionArray[ index ]( first, second );
}
ErikEsTT
  • 356
  • 1
  • 3
  • 14
  • Pretty interesting... but isn't this quite similar to having the switch statement? If I remember properly compilers implemenet switch statements with jump tables (http://en.wikipedia.org/wiki/Branch_table) which is essentially what you have here, I think :). You would still need the enum of previous solutions but I guess that's better than resorting to a code generator. That said, I am not quite sure about whether Visual Studio 2012 is generating a jump table or not. – jmeseguerdepaz Oct 01 '12 at 18:52
  • First, `Base` object need to know from which it was derived, so you **will** have some sort of enum, or virtual function table. [CRTP](http://stackoverflow.com/q/262254/1569168) is not an answer for you, unless you will have container for each derived type. Anyway, did you read [this](http://en.wikipedia.org/wiki/Multiple_dispatch#C.2B.2B) wikipedia article? – ErikEsTT Oct 01 '12 at 20:44
  • Second, in this answer is single array, which is single dereference, solution with 2 switch (hopefully converted to branch tables) is 2 dereferences, or (if compiler is very good) single dereference (you need to check generated [assembly](http://msdn.microsoft.com/en-us/library/367y26c6) ), with [virtual functions](http://stackoverflow.com/q/12582040/1569168), there is 2 dereferences for single virtual function ( class->vft->func() ), so totaly 4 dereferences si required. Dereference is fast, unless you will have millions of objects. – ErikEsTT Oct 01 '12 at 20:45
  • Sorry, I don't know what I was thinking, of course this and the previous solution require an enum. I considered CRTP but I discarded it. Finally, as for this last solution, it is a clear winner in Visual Studio 2012 :D . 0.104 seconds with the same number of intersections that takes 0.170 seconds with the 2-switch method. I have uploaded an updated benchmark to [here](http://www.mediafire.com/?8a2qfuxrkwrq8n5) – jmeseguerdepaz Oct 01 '12 at 22:19
  • **BONUS**: It is possible to check at _compile time_ whether all intersection algorithms (at least the right number of them) have been added to the jump table or not. Its a matter of adding `static_assert( (sizeof(IntersectionArray)/sizeof(uni_intersect) ) == ((unsigned)MaxType * (unsigned)MaxType) , "Intersection algorithm(s) missing" );` after the IntersectionArray declaration. That makes this method the fastest and now close to the safest (boost::any). – jmeseguerdepaz Oct 01 '12 at 22:26
  • I was waiting just in case someone had another idea, but yes :) – jmeseguerdepaz Oct 02 '12 at 22:08
1

This problem is similar to this.

class Geometry
{
public:
    enum eType
    {
        TypeMesh,
        TypeParamBox,
        TypeParamSphere,
    };

    Geometry( eType t ): Type( t ) { }
    ~Geometry() { }

    const eType Type;
};


class Mesh : public Geometry
{
public:
    Mesh(): Geometry( TypeMesh ) { }
};


class Parametric : public Geometry
{
public:
    Parametric( eType t ): Geometry( TypeMesh ) { }
};


class Box : public Parametric
{
public:
    Box(): Parametric( TypeParamBox ) { }
};


class Sphere : public Parametric
{
public:
    Sphere(): Parametric( TypeParamSphere ) { }
};


namespace intersections
{
    bool intersection( const Geometry* first, const Geometry* second );
    template <typename T>
    bool t_intersection( const T* first, const Geometry* second );

    bool intersection( const Box*, const Box* );
    bool intersection( const Box*, const Sphere* );
    bool intersection( const Sphere*, const Box* );
    bool intersection( const Sphere*, const Sphere* );
}


void foo_test()
{
    std::vector<Geometry*> arrGeometry;

    // add the elements
    arrGeometry.push_back( new Box() );
    arrGeometry.push_back( new Sphere() );

    // ... some more code

    // try to intersect them?
    Geometry* g1 = arrGeometry[0];
    Geometry* g2 = arrGeometry[1];
    bool intersecting = intersections::intersection( g1, g2 );
}


bool intersections::intersection( const Geometry* first, const Geometry* second )
{
    switch( first->Type )
    {
    case Geometry::TypeParamBox: return t_intersection( static_cast<const Box*>( first ), second );
    case Geometry::TypeParamSphere: return t_intersection( static_cast<const Sphere*>( first ), second );
    default: return false;
    }
}


template <typename T>
bool intersections::t_intersection( const T* first, const Geometry* second )
{
    switch( second->Type )
    {
    case Geometry::TypeParamBox: return intersection( first, static_cast<const Box*>( second ) );
    case Geometry::TypeParamSphere: return intersection( first, static_cast<const Sphere*>( second ) );
    default: return false;
    }
}

Or if you don't use inheritance:

struct Mesh{};

struct Box{};

struct Sphere{};


enum GeometryType
{
    TypeMesh,
    TypeParamBox,
    TypeParamSphere
};


struct Geometry
{
    GeometryType Type;

    union
    {
        Mesh* pMesh;
        Box* pBox;
        Sphere* pSphere;
    } Ptr;
};



namespace intersections
{
    bool intersection( const Geometry* first, const Geometry* second );
    template <typename T>
    bool t_intersection( const T* first, const Geometry* second );

    bool intersection( const Box*, const Box* );
    bool intersection( const Box*, const Sphere* );
    bool intersection( const Sphere*, const Box* );
    bool intersection( const Sphere*, const Sphere* );
}



void foo_test()
{
    std::vector<Geometry*> arrGeometry;

    // add the elements
//  arrGeometry.push_back( new Box() );
//  arrGeometry.push_back( new Sphere() );

    // ... some more code

    // try to intersect them?
    Geometry* g1 = arrGeometry[0];
    Geometry* g2 = arrGeometry[1];
    bool intersecting = intersections::intersection( g1, g2 );
}


bool intersections::intersection( const Geometry* first, const Geometry* second )
{
    switch( first->Type )
    {
    case TypeParamBox: return t_intersection( first->Ptr.pBox, second );
    case TypeParamSphere: return t_intersection( first->Ptr.pSphere, second );
    default: return false;
    }
}


template <typename T>
bool intersections::t_intersection( const T* first, const Geometry* second )
{
    switch( second->Type )
    {
    case TypeParamBox: return intersection( first, second->Ptr.pBox );
    case TypeParamSphere: return intersection( first, second->Ptr.pSphere );
    default: return false;
    }
}


NOTE: if you know type of one geometry, you can use templated function:

Box* pBox;
Geometry* pUnknownGeometry;
bool intersecting = intersections::t_intersection( pBox, pUnknownGeometry );


NOTE2: you can write identical functions like this:

bool intersection( const Box* first, const Sphere* second );

bool intersection( const Sphere* first, const Box* second )
{
    return intersection( second, first );
}


EDIT:

If you reduce problem to double dispatch, you can use this technique.

For intersection treat hierarchy as:

  • Geometry
    • Mesh
    • Box
    • Sphere

and for other actions:

  • Geometry
    • Mesh
    • Parametric
      • Box
      • Sphere
Community
  • 1
  • 1
ErikEsTT
  • 356
  • 1
  • 3
  • 14
  • That's exactly why I referred to when I was talking about faking RTTI (your GeometryType enum) + downcastings. Although I do like how you handled having two switch to resolve the two arguments. I would have gone for an inline function to have the switch centralized though. As for the reference to the other question: thanks! I would have never find it :) – jmeseguerdepaz Sep 28 '12 at 17:03
  • @jmeseguerdepaz If you don't know type at compile time, RTTI or fake RTTI is only option. You can hide it no matter how you want, but it always appear somewhere, because C++ si strongly typed language. – ErikEsTT Sep 28 '12 at 18:22
  • Well, not really. When you don't know the type and you just have one parameter a Visitor (http://en.wikipedia.org/wiki/Visitor_pattern) is a good choice: types are resolved at run-time. Probably a Visitor pattern (with a lot of function calls bouncing between objects) could also be used here (to achieve triple-dispatching) but I am pretty sure that in that case the solution would be worse than the problem... – jmeseguerdepaz Sep 28 '12 at 19:16
0

How about you represent your intersection algorithms as classes which can decide, if they can process the set of geometries they are getting, and you provide an intersection proxy which has the original interface. This would look like this:

class IIntersection
{
   public:
     bool intersection( const Geometry &, const Geometry &);
     bool accept( const Geometry &, const Geometry &);
}

class IntersectionBoxSphere : public IIntersection
{
public:
   bool intersection( const Geometry &, const Geometry &);
   bool accept( const Geometry & a, const Geometry &b)
   {
      return ((dynamic_cast<Box>(a) != NULL || dynamic_cast<Box>(b) != NULL)
              (dynamic_cast<Sphere>(a) != NULL || dynamic_cast<Sphere>(b) != NULL))
   }
}

class IntersectionBoxbox  : public IIntersection
...
    /**collect some where all algorithms*/
    IIntersection* myintersections[2];
    myintersections[0] = new IntersectionBoxSphere()
    myintersections[1] = new IntersectionBoxbox()
...
/**decide here which to use */
bool intersection( const Geometry &a, const Geometry &b)
{
    for ( i ... )
     if (myintersections[i]->appect(a,b))
       return myintersections[i]->intersection(a,b)
}
Uli Klank
  • 199
  • 10
  • I think it's quite an elegant twist to the switch+downcasting technique, really. However, given that I won't add intersection algorithms at runtime I don't know if it is worth the performance hit of having the IntersectionXY classes in a vector and substituing a simple switch for a sequence of boolean decisions (accept / !accept). – jmeseguerdepaz Sep 28 '12 at 16:59