Physics Simulation Forum

 

All times are UTC




Post new topic Reply to topic  [ 18 posts ]  Go to page 1, 2  Next
Author Message
PostPosted: Sun Jul 31, 2011 6:43 am 
Offline

Joined: Tue Dec 25, 2007 1:06 pm
Posts: 398
Convex decomposition is the process used to decompose a concave mesh into a set of convex meshes. This process can be used to compute the collision shape used by physic engines to represent the mesh, expecially when the shape is supposed to model dynamic objects.

I've made a new Bullet Demo about it.
It's based on the library HACD ("Hierarchical approximate convex decomposition" by Khaled Mamou), which should be included in the Bullet library package (in the "Extra" section) starting from Bullet version 2.79 (at the time of writing this post, it's present in the Bullet SVN repository).

The demo can be used in three ways:

1) As a common Bullet Demo, it's similiar to the appConvexDecompositionDemo, but with a better interface.
The heart of the demo is the (header only) file (and class) "btHACDCompoundShape.h", that can be easily integrated into existing applications and that takes a btStridingMeshInterface* as an argument in its constructor together with an optional structure with some decomposition parameters.
Currently the demo is composed by four scenes; keys '[' and ']' can be used to change the current scene.

To compile the demo, a reference to HACD.lib is needed and STL is required (STL is also used in lib HACD itself).
The demo depends on lib BulletWorldImporter too, and glut (or freeglut) is needed.

IMPORTANT: Please copy the file "appHACDDemo.exe.cmdlineParams.txt" AND the folder "appHACDDemoMeshes" in the same directory of the executable "appHACDDemo.exe".
The folder "appHACDDemoMeshes" contains three simple OBJ files I've made using Wings3d, that are needed in some of the scenes (together with the "Bunny mesh" and the "Torus mesh" that were included in appGImpactTestDemo).

2) As a standalone decomposition tool that takes a Wavefront OBJ file (recommended because the loader keeps track of all the submeshes in the model), or an .OFF file as an argument (on Windows you can drag the model directly onto the appHACDDemo.exe icon in Explorer). The resulting "plain" Bullet btCompoundShape can be optionally saved to disk as a file with the ".bcs" (Bullet Collision Shape) extension. Additional decomposition paramters are not passed through the command line (too many), but can be edited directly in a text file named "appHACDDemo.exe.cmdlineParams.txt", in the same folder of appHACDDemo.exe).

".bcs" files can be loaded in plain Bullet applications with the following code:
Code:
   #include <BulletWorldImporter/btBulletWorldImporter.h>   // In "Bullet/Extra/Serialize". Needs link to: BulletWorldImporter.lib
   btCollisionShape* Load(const char* filename,bool verbose)   {
      btBulletWorldImporter loader(0);//don't store info into the world
      loader.setVerboseMode(verbose);
      if (!loader.loadFile(filename)) return NULL;
      btCollisionShape* shape = NULL;
      if (loader.getNumCollisionShapes()>0) shape = loader.getCollisionShapeByIndex(0);
      // Hope there are no leaks.
      return shape;
   }
The advantages of loading ".bcs" files, instead of adding "btHACDCompoundSape.h" to a project are:
1) No need to use STL.
2) No need to link to HACD.lib
3) Only simple models can be decomposed "at runtime" with "btHACDCompoundSape.h", because the decomposition
process can take several seconds. Much better to load btCompoundShapes directly from disk.

3) As a ".bcs" (Bullet Collision Shape) file viewer, when the command line argument has a ".bcs" extension (on Windows you can drag it onto the .exe).

Screenshot:
Attachment:
HACDDemo12.jpg
HACDDemo12.jpg [ 37.29 KiB | Viewed 10617 times ]
Source code + precompiled binary for Win32 (compiled with Visual C++ 2008 and /MD, Visual C++ 2008 Redistributable Package required):
Attachment:
HACDDemo.7z [622.9 KiB]
Downloaded 634 times

Hope you like it and find it useful :) .

UPDATED: 15 Jan 2012:
-> btTriangleMeshesEx can be assigned to btCollisionShapes/btCollisionObjects and displayed with the 'r' key (Limitation: only static meshes are supported (display lists are used)).
Now it's possible to compare the decomposed collision shape to the input mesh.
-> btConvexHullComputer is used instead of btShapeHull (better visualization of the decomposed collision shape).
-> CollisionShapes with a striding mesh interface inside them can be directly decomposed (and optionally used as input meshes).
-> Some new decomposition parameters added.

-> Frustum culling added.
-> Free camera mode added ('a' key).
-> Ortho mode modified a bit ('o' key).
-> Raytrace screenshots can be shown/hidden ('R' key (uppercase)).
-> Texture support added.

-> Bug: when meshes are displayed ('r' key), the (btShapeHull based) stencil shadows may produce artifacts, color bleeding and blinking.
The 'g' key can be used to disable them.

NEW SOURCE CODE: 22 May 2013:
Attachment:
File comment: source code only (for developers)
HACDDemo(source_code).7z [143.82 KiB]
Downloaded 249 times
This code is similiar to the old one, but it is:
1) Ready for "Asset Import Library" version 3 integration (see: LOAD_MESHES_FROM_ASSET_IMPORT_LIBRARY in "btTriangleMeshEx.h").
2) Ready for experimental integration with CURRENT version of the V-HACD library git repository,
available at: https://code.google.com/p/v-hacd (see: EXCLUDE_VHACD in "appHACDDemo.h").
If you do not need to load formats different from .obj and .off, and you don't want to play with V-HACD,
I suggest you keep on using the old code (that comes with an exe for Win32), instead of spending time
trying to compile this version.


Last edited by Flix on Wed May 22, 2013 8:49 am, edited 2 times in total.

Top
 Profile  
 
PostPosted: Thu Sep 01, 2011 10:33 am 
Offline

Joined: Tue Dec 25, 2007 1:06 pm
Posts: 398
I made a small update to HACDDemo.7z.

Changelog:
-> Changed the way colors are displayed (see picture). 'c' can be used to switch color mode.
Attachment:
HACD.jpg
HACD.jpg [ 8.78 KiB | Viewed 11221 times ]
-> Added: params.reduceHullVerticesUsingBtShapeHull (false by default)
(not very useful in most cases: params.maxHullVertices should be used, but in some rare case with a lot of child shapes it may be useful; for example when using HACD full-hull feature (params.maxHullVertices=0)).
-> Now the number of hull vertices per child shape is displayed in the console window.
-> Now the Bullet Collision Shape Viewer accepts files with the .bullet extension (but they are handled exactly as .bcs files: only the first collision shape inside them is shown).

IMPORTANT: BulletOpenGLSupport.lib seems to use btShapeHull to simplify convex hulls before displaying them (most likely for performance reasons).
That means that convex hulls with more than 42 vertices are "cut down" in their visual representation (not in their collision shape).
This could be improved using the information provided by HACD.lib itself (index arrays), but it won't be a very robust solution (when the shape is serialized the index array should be recreated somehow...).
Maybe there's some way to achieve the same result using Bullet code only.
Feel free to modify the source code to solve this issue, if needed.


Top
 Profile  
 
PostPosted: Thu Sep 08, 2011 6:03 pm 
Offline

Joined: Wed Jul 27, 2011 5:18 pm
Posts: 6
Good job Flix, this will surely come handy. Decomposing concave meshes for collision shapes is always tedious :D


Top
 Profile  
 
PostPosted: Tue Jan 03, 2012 3:29 pm 
Offline

Joined: Fri May 13, 2011 1:11 pm
Posts: 65
Are there any plans to decouple the aquisition of the start shape (wavefront obj etc) and the actual algorithm?

And by decoupling I mean, taking a generic btCollisionShape or btBvhTriangleMeshShape and applying the decomposition on it.
This would let users determine how they aquire the initial collision shape (be it googles sketchup, 3dsmax, wavefront objs, osg models etc).

The coupling with wavefront obj's seems to be hardwired in and required at the moment which is not desirable for everyone.

Eitherway cheers for the contributions, it looks incredibly nice!


Top
 Profile  
 
PostPosted: Tue Jan 03, 2012 5:12 pm 
Offline

Joined: Tue Dec 25, 2007 1:06 pm
Posts: 398
Karrok wrote:
Are there any plans to decouple the aquisition of the start shape (wavefront obj etc) and the actual algorithm?
At the moment there are no plans about it.
Karrok wrote:
And by decoupling I mean, taking a generic btCollisionShape or btBvhTriangleMeshShape and applying the decomposition on it.
I'm not sure this should be possible for all the shape types, although it might be possible for btBvhTriangleMeshShapes or GImpactMeshShapes, as long as they expose the inner btStridingMashInterface.
Karrok wrote:
The coupling with wavefront obj's seems to be hardwired in and required at the moment which is not desirable for everyone.
Well:
1) Another format is supported (.OFF).
2) Adding support for other 3d formats shouldn't be too difficult for developers (file "btTriangleMeshEx.cpp"). However I'm not planning to do it ATM (almost every 3D package can export to .obj format).
3) All one really needs to perform what you're asking is the single header file "btHACDCompoundShape.h", a btStridingMeshInterface* (that "probably" could be retrieved back from a btBvhTriangleMeshShape and a GImpactMeshShape) and the decomposition properties (expecially the one used to save the resulting shape). As long as the btStridingMeshInterface* is available, every developer should be able to do it. Please note that the single file "btHACDCompoundShape.h" is not "coupled" at all with any particular 3D formats.
4) When a btCollisionShape is used as an input (and I suppose you mean a .bcs file, but I'm not sure what you used for creating it...), the program must display it to show the serialized shape, and not try to decompose it.

Hope it makes sense :wink:
I suggest you follow point 3 if you're really allergic to the .obj exporters.

PS. At the moment I'm developing an extended version that uses HACD SVN from sourceforge (slightly different than the one in the Bullet/Extra section at the time of writing), that uses btConvexHullComputer to get rid of the limitation introducted by btShapeHull (see the end of the second post) and that can display the real mesh too (so that the user can see how the decomposition is good). I'm deeply extending the GlutDemoApplication class, so I guess it will take a bit long before I'll post it. Anyway here is a development screenshot:
Attachment:
appHACDDemo.jpg
appHACDDemo.jpg [ 33.37 KiB | Viewed 10763 times ]


Top
 Profile  
 
PostPosted: Tue Jan 03, 2012 10:13 pm 
Offline

Joined: Sun Jan 01, 2012 7:37 pm
Posts: 55
Might you put your work on github or bitbucket? (what license would you put your work under)

I would like to fork it and implement an interface similar to how one would generate a concave triangle mesh directly, as an alternate to loading an OBJ or OFF.

I'm really excited about this.

Are you planning to release under something like zlib?


Top
 Profile  
 
PostPosted: Wed Jan 04, 2012 12:54 pm 
Offline

Joined: Tue Dec 25, 2007 1:06 pm
Posts: 398
@ Karrok
Flix wrote:
I suggest you follow point 3 if you're really allergic to the .obj exporters.
You may try this base code (I've only tested btBvhTriangleMeshShape to btHACDCompoundShape and it works):
Code:
#include <BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h>
#include <BulletCollision/CollisionShapes/btScaledBvhTriangleMeshShape.h>
#include <BulletCollision/Gimpact/btGImpactShape.h>
#include "btHACDCompoundShape.h"
/* Creates a decomposed shape of the input collision shape that must be one of the following:
   btBvhTriangleMeshShape
   btScaledBvhTriangleMeshShape
   btGImpactShape
   btCompoundShapes with one nested compatible shape are not supported.
*/
static btHACDCompoundShape* ConvertToHACDCompoundShape(const btCollisionShape* shape,const btHACDCompoundShape::Params& HACDparams=btHACDCompoundShape::Params(),const bool considerShapeLocalScaling=true)   {
         if (!shape) return NULL;
         const btStridingMeshInterface* smi(NULL);
         btHACDCompoundShape::Params params(HACDparams);
         if (shape->getShapeType()==GIMPACT_SHAPE_PROXYTYPE)   {
            const btGImpactShapeInterface* gimi = static_cast < const btGImpactShapeInterface* > (shape);
            if (gimi && gimi->getGImpactShapeType()==CONST_GIMPACT_TRIMESH_SHAPE)   {
               const btGImpactMeshShape* gishape = static_cast < const btGImpactMeshShape* > (gimi);
               smi = gishape->getMeshInterface();
               if (considerShapeLocalScaling) params.decomposeAScaledCopyOfTheMesh*=gishape->getLocalScaling();
            }
         }
         else if (shape->getShapeType()==TRIANGLE_MESH_SHAPE_PROXYTYPE)   {
            const btBvhTriangleMeshShape* bvhShape = static_cast < const btBvhTriangleMeshShape* > (shape);
               smi = bvhShape->getMeshInterface();
               if (considerShapeLocalScaling) params.decomposeAScaledCopyOfTheMesh*=bvhShape->getLocalScaling();
         }
         else if (shape->getShapeType()==SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE)   {
            const btScaledBvhTriangleMeshShape* sbvhShape = static_cast < const btScaledBvhTriangleMeshShape* > (shape);
            if (sbvhShape)   {
               const btBvhTriangleMeshShape* bvhShape = static_cast < const btBvhTriangleMeshShape* > (sbvhShape->getChildShape());
               if (bvhShape)   {
                  smi = bvhShape->getMeshInterface();
                  params.decomposeAScaledCopyOfTheMesh*=sbvhShape->getLocalScaling();      //This must be left outside 'considerShapeLocalScaling'
                  if (considerShapeLocalScaling) params.decomposeAScaledCopyOfTheMesh*=bvhShape->getLocalScaling();
               }   
            }   
         }
         if (!smi) return NULL;
         return new btHACDCompoundShape(smi,params);
}
kloplop321 wrote:
Might you put your work on github or bitbucket? (what license would you put your work under)
I would like to fork it and implement an interface similar to how one would generate a concave triangle mesh directly, as an alternate to loading an OBJ or OFF. I'm really excited about this. Are you planning to release under something like zlib?
No, no, the code I've posted (and the new version) are under the same license of the Bullet library. As you see with the code above one can directly convert Bullet compatible shapes using just one header file (no need for a repository to mantain ATM).


Top
 Profile  
 
PostPosted: Mon Jan 09, 2012 9:31 am 
Offline

Joined: Fri May 13, 2011 1:11 pm
Posts: 65
Thanks Flix! :D


Top
 Profile  
 
PostPosted: Sun Jan 15, 2012 7:38 am 
Offline

Joined: Tue Dec 25, 2007 1:06 pm
Posts: 398
Source code and binary (in the first post) updated.
(Main difference is that now the SVN HACD library is mandatory to compile the code.)

HOW TO COMPILE THE CODE:
Prerequisites:
1) Bullet 2.79 (or upper)
2) HACD (http://sourceforge.net/projects/hacd/). At the time of writing only the HACD SVN version is compatible, NOT the one in the Bullet Extras/HACD folder.

To compile the code using QtDesigner/QtMake a .pro file has been added (needs minimal editing to set the correct folders) and tested on Ubuntu64.
A CMakeList.txt file is present too, but it just shares the same structure as the other Bullet Demos (it's supposed to be included in parent CMakeList files), and it might need some tweaking to make it work.

In case of problems, as a general guideline, the dependencies are similiar to the ones in the appConvexDecompositionDemo, (with the addition of BulletSoftbody), and the HACD include and link folders must be replaced from the Bullet/Extras/HACD folder to the new HACD SVN folders (please read the .pro file for further info).

LAST UPDATE: 15 Jan 2012:
-> btTriangleMeshesEx can be assigned to btCollisionShapes/btCollisionObjects and displayed with the 'r' key (Limitation: only static meshes are supported (display lists are used)).
Now it's possible to compare the decomposed collision shape to the input mesh.
-> btConvexHullComputer is used instead of btShapeHull (better visualization of the decomposed collision shape),
-> CollisionShapes with a striding mesh interface inside them can be directly decomposed (and optionally used as input meshes).
-> Some new decomposition parameters added.

-> Frustum culling added.
-> Free camera mode added ('a' key).
-> Ortho mode modified a bit ('o' key).
-> Raytrace screenshots can be shown/hidden ('R' key (uppercase)).
-> Texture support added.

-> Bug: when meshes are displayed ('r' key), the (btShapeHull based) stencil shadows may produce artifacts, color bleeding and blinking.
The 'g' key can be used to disable them.


Top
 Profile  
 
PostPosted: Wed Apr 24, 2013 8:13 pm 
Offline

Joined: Wed Apr 24, 2013 7:57 pm
Posts: 7
I was trying to compile the HACDDemo against bullet2.81, but I got following compilation errors:

HACDDemo.cpp:110: error: ‘const class btCollisionObject’ has no member named ‘getRootCollisionShape’
HACDDemo.cpp:119: error: ‘const class btCollisionObject’ has no member named ‘getRootCollisionShape’
HACDDemo.cpp: In member function ‘void HACDDemo::initPhysicsForDemoSlidingSledge()’:
/home/anjack/opengl/ogre/HACDDemo/HACDDemo.cpp:768: error: invalid conversion from ‘bool (*)(btManifoldPoint&, const btCollisionObject*, int, int, const btCollisionObject*, int, int)’ to ‘bool (*)(btManifoldPoint&, const btCollisionObjectWrapper*, int, int, const btCollisionObjectWrapper*, int, int)’
make[2]: *** [CMakeFiles/AppHACDDemo.dir/HACDDemo.cpp.o] Error 1

After quick investigation I managed to get rid of the first two errors by replacing getRootCollisionShape with getCollisionShape (no idea how it may affect the final effect). Nonetheless I don't have enough expertise to cope with the third error. Can anyone give some hints on how to resolve the problem, or maybe there is some more up-to-date HACDdemo version available?


Top
 Profile  
 
PostPosted: Thu Apr 25, 2013 9:52 am 
Offline

Joined: Tue Dec 25, 2007 1:06 pm
Posts: 398
ananas wrote:
I got following compilation errors:

Yes, I'm aware about these errors. They should be easy to fix anyway :D , even if I don't have a working version myself (I did HAVE a working version actually, with support of almost every mesh format on input (through the asset import library), but my windows PC crashed just before I was about to post it a few months ago, and I had to reformat my hard drive)

They are caused by a change in the Bullet API :
Basically the method:
Code:
static bool CustomMaterialCombinerCallback(btManifoldPoint& cp,   const btCollisionObject* colObj0,int partId0,int index0,const btCollisionObject* colObj1,int partId1,int index1)
now has a different signature, and should be changed to something like:
Code:
static bool CustomMaterialCombinerCallback(btManifoldPoint& cp,   const btCollisionObjectWrapper* colObj0,int partId0,int index0,const btCollisionObjectWrapper* colObj1,int partId1,int index1)
The 3rd error was just about this delegate mismatch; but to fix the first two you need to retrieve the root collision shape from the btCollisionObjectWrapper (I suggest you look into the btCollisionObjectWrapper fields: there should be the plain btCollisionObject together with other members that could be useful... I don't remember how I fixed it last time...)

The btCollisionObject class has lost its getRootCollisionShape method because of this change.

PS. All the changed parts are just related to the sliding sledge demo: it shouldn't be difficult be to exclude it as a fallback...

> Edited: this replecement should work (in HACDDemo.cpp):
Code:
static bool CustomMaterialCombinerCallback(btManifoldPoint& cp,   const btCollisionObjectWrapper* colObj0Wrapper,int partId0,int index0,const btCollisionObjectWrapper* colObj1Wrapper,int partId1,int index1)
{
    const btCollisionObject* colObj0 = colObj0Wrapper->getCollisionObject();
    const btCollisionObject* colObj1 = colObj1Wrapper->getCollisionObject();

   float friction0 = colObj0->getFriction();
   float friction1 = colObj1->getFriction();
   float restitution0 = colObj0->getRestitution();
   float restitution1 = colObj1->getRestitution();

   if (colObj0->getCollisionFlags() & btCollisionObject::CF_CUSTOM_MATERIAL_CALLBACK)   {
        if (colObj0->getCollisionShape() == gSledgeShape)   {
         #ifndef USE_GIMPACTMESHSHAPE_IN_SLEDGE_DEMO
         if (index0>=0 && index0<gSledgeShapeSubmeshIndexOfChildShapes.size())   GetFrictionRestitutionForTheSledgeShape(colObj0,gSledgeShapeSubmeshIndexOfChildShapes[index0],friction0,restitution0);
         #else //USE_GIMPACTMESHSHAPE_IN_SLEDGE_DEMO
         GetFrictionRestitutionForTheSledgeShape(colObj0,partId0,friction0,restitution0);
         #endif //USE_GIMPACTMESHSHAPE_IN_SLEDGE_DEMO
      }
   }   
   if (colObj1->getCollisionFlags() & btCollisionObject::CF_CUSTOM_MATERIAL_CALLBACK)   {
        if (colObj1->getCollisionShape() == gSledgeShape)   {
         #ifndef USE_GIMPACTMESHSHAPE_IN_SLEDGE_DEMO
         if(index1>=0 && index1<gSledgeShapeSubmeshIndexOfChildShapes.size())   GetFrictionRestitutionForTheSledgeShape(colObj1,gSledgeShapeSubmeshIndexOfChildShapes[index1],friction1,restitution1);
         #else //USE_GIMPACTMESHSHAPE_IN_SLEDGE_DEMO
         GetFrictionRestitutionForTheSledgeShape(colObj1,partId1,friction1,restitution1);
         #endif //USE_GIMPACTMESHSHAPE_IN_SLEDGE_DEMO
      }   
   }   
   
   cp.m_combinedFriction = calculateCombinedFriction(friction0,friction1);
   cp.m_combinedRestitution = calculateCombinedRestitution(restitution0,restitution1);

   //this return value is currently ignored, but to be on the safe side: return false if you don't calculate friction
   return true;
}


Top
 Profile  
 
PostPosted: Tue May 07, 2013 8:38 pm 
Offline

Joined: Wed Apr 24, 2013 7:57 pm
Posts: 7
I did the suggested modifications but the application hangs up in the middle of the first demo. Nevertheless, I realized that bullet2.81 also includes ConvexDecomposition demo. I replaced the mesh shipped with the bullet demo with my "mug" mesh. Unfortunately convex decomposition seems not to work properly for "mug" mesh as it is not empty but has the properties of a cylinder. Here is the result: http://www.youtube.com/watch?v=p0T8sGgw3cc&feature=youtu.be. The left object is created with btBvhTriangleMeshShape and the right one with btCompoundShape comprising btConvexHullShapes obtained from convex decomposition. If I don't create btCompoundShape, the btConvexHullShapes just fall on the ground leaving alone inner cylinder-like object. Is it expected result for the kind of mesh I want to play with? I would use btBvhTriangleMeshShape but when testing with Ogre it sinks into the ground, which doesn't happen in the bullet demo probably because of the constraint defined on the ground surface.


Top
 Profile  
 
PostPosted: Thu May 09, 2013 9:17 am 
Offline

Joined: Tue Dec 25, 2007 1:06 pm
Posts: 398
ananas wrote:
I did the suggested modifications but the application hangs up in the middle of the first demo.
The suggested modification affects only the LAST demo... and I've compiled and run the demo without problems... :)

The other issue you're having refers to the default Bullet ConvexDecomposition demo and not to the topic discussed in this series of posts (and I'm not very familiar with that demo).
Anyway usually there are some parameters you can tune to adjust the convex decomposition result.

P.S. Please note that you can still use "btHACDCompoundShape.h" alone (without the whole HACDDemo) to add HACD support to ANY Bullet program (but you need to compile the HACD SVN library beforehand).


Top
 Profile  
 
PostPosted: Wed May 22, 2013 8:51 am 
Offline

Joined: Tue Dec 25, 2007 1:06 pm
Posts: 398
Today I've added an updated (source code only) version of the HACDDemo.
Please read the first post for further info. :)


Top
 Profile  
 
PostPosted: Mon Jun 03, 2013 9:05 pm 
Offline

Joined: Wed Apr 24, 2013 7:57 pm
Posts: 7
I've manged to successfully compile and launch HACDdemo, but I am struggling with decomposition of a torus-like mesh. I see that BulletDefaultTorusMesh.off gets decomposed properly for all but one objects in the torus demo, but if I replace it with my torus mesh created in blender with "spin" tool, and exported to the waveform format it doesn't have central hole.
Could you give some hints on how to create a torus mesh that would fit the convex decomposition algorithm requirements?


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 18 posts ]  Go to page 1, 2  Next

All times are UTC


Who is online

Users browsing this forum: No registered users and 1 guest


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group