Hi Gerrit,
I'm trying to port the latest version of the ClipPlaneCaps tutorial
Johannes Brunen contributed to 2.0, but runnning into a small problem:
there is no StateSortingGroup any more.
I thought I could just replace them with Stages (using SimpleStages with
PassiveBackground I can have them both render into the framebuffer), but
that has the problem that a stage seems to cause a full state reset,
i.e. you start with no materials in the origin of the world coordinate
system. Is there a way to have a stage inherit the state so far, or is
there something that would be a better fit? Should we introduce
something like a "grouping stage" that essentially just groups a subtree
into a pass?
Thanks,
Carsten
PS: attached is the code so far
// OpenSG example: ClipPlaneCaps
//
// Demonstrates the use of the ClipPlaneChunk, StencilChunk for capping of
// clipped geometry
//
// This examples allows creation of a box and a torus. Additionally, two clip
// planes can be set up. On clipping the geometry caps are used to repair the
// clipped geometry.
//
// User interface:
// a) mouse => standard navigator
// b) keyboard =>
// '1': enable/disable clip plane 0
// '2': enable/disable clip plane 1
// '3': enable/disable box geometry
// '4': enable/disable torus geometry
// 'n': move clip plane 0 opposite to the normal direction of the plane
// 'm': move clip plane 0 in the normal direction of the plane
// ',': move clip plane 1 opposite to the normal direction of the plane
// '.': move clip plane 1 in the normal direction of the plane
// 'q': move box in -x direction
// 'w': move box in +x direction
// 'a': move box in -y direction
// 's': move box in +y direction
// 'y': move box in -z direction
// 'x': move box in +z direction
// 'e': move torus in -x direction
// 'r': move torus in +x direction
// 'd': move torus in -y direction
// 'f': move torus in +y direction
// 'c': move torus in -z direction
// 'v': move torus in +z direction
//
// This example was contributed by Johannes Brunen
#include <vector>
#ifdef OSG_BUILD_INTEGRATED
#include <OSGGLUT.h>
#include <OSGConfig.h>
#include <OSGSimpleGeometry.h>
#include <OSGPassiveWindow.h>
#include <OSGSimpleSceneManager.h>
#include <OSGSceneFileHandler.h>
#include <OSGMultiSwitch.h>
#include <OSGMaterialGroup.h>
#include <OSGSmipleStage.h>
#include <OSGChunkMaterial.h>
#include <OSGSimpleMaterial.h>
#include <OSGClipPlaneChunk.h>
#include <OSGStencilChunk.h>
#include <OSGInverseTransform.h>
#include <OSGPassiveBackground.h>
#include <OSGFieldContainerUtils.h>
#else // OSG_BUILD_INTEGRATED
#include <OpenSG/OSGGLUT.h>
#include <OpenSG/OSGConfig.h>
#include <OpenSG/OSGSimpleGeometry.h>
#include <OpenSG/OSGPassiveWindow.h>
#include <OpenSG/OSGSimpleSceneManager.h>
#include <OpenSG/OSGSceneFileHandler.h>
#include <OpenSG/OSGMultiSwitch.h>
#include <OpenSG/OSGMaterialGroup.h>
#include <OpenSG/OSGSimpleStage.h>
#include <OpenSG/OSGChunkMaterial.h>
#include <OpenSG/OSGSimpleMaterial.h>
#include <OpenSG/OSGClipPlaneChunk.h>
#include <OpenSG/OSGStencilChunk.h>
#include <OpenSG/OSGInverseTransform.h>
#include <OpenSG/OSGPassiveBackground.h>
#include <OpenSG/OSGFieldContainerUtils.h>
#endif // OSG_BUILD_INTEGRATED
OSG_USING_NAMESPACE
typedef std::vector<NodeRefPtr> VecNodesT; // convenience type
//
// transport container for the actual clip plane data
//
struct ClipPlaneData
{
Vec4f _equation;
bool _enabled;
};
typedef std::vector<ClipPlaneData> VecClipPlaneData;
//
// for each clip plane these data are necessary
//
struct ClipPlaneDetails
{
ClipPlaneChunkRefPtr _clipPlaneChunk;
GeometryRefPtr _planeGeometryCore;
TransformRefPtr _planeTrafoCore;
NodeRefPtr _planeBeaconNode;
Color3f _planeColor;
};
typedef std::vector<ClipPlaneDetails> VecClipPlaneDetailsT;
//
// global state
//
VecClipPlaneData vecClipPlaneData; // transport clip plane info
VecClipPlaneDetailsT vecClipPlaneDetails; // opensg clip plane state
VecNodesT vecGeometries; // box and torus
SimpleSceneManager *mgr;
NodeRefPtr scene;
//
// the number of clipping planes supported by the demo. Define a plane color
// for each.
//
const int iNumClipPlanes = 2;
Color3f planeCol[iNumClipPlanes] = { Color3f(0,1,0), Color3f(0,0,1) };
//
// Build the global clip plane state
//
void createClipPlaneDetails(void)
{
for(int i = 0; i < iNumClipPlanes; ++i)
{
ClipPlaneDetails details;
//
// Create clip plane chunk
//
details._planeBeaconNode = Node::create();
details._clipPlaneChunk = ClipPlaneChunk::create();
details._clipPlaneChunk->setEquation(Vec4f(1,0,0,0));
details._clipPlaneChunk->setEnable(false);
details._clipPlaneChunk->setBeacon(details._planeBeaconNode);
//
// Create plane geometry
//
details._planeGeometryCore = makePlaneGeo(100.f, 100.f, 128, 128);
//
// Create plane transformation core
//
Matrix mat;
mat.setIdentity();
details._planeTrafoCore = Transform::create();
details._planeTrafoCore->setMatrix(mat);
//
// Define plane color
//
details._planeColor = planeCol[i];
vecClipPlaneDetails.push_back(details);
}
}
//
// Clean up code
//
void cleanup(void)
{
vecClipPlaneDetails.clear();
vecGeometries .clear();
delete mgr;
mgr = NULL;
scene = NULL;
}
//
// In case the clip plane data change this function is called
//
void updateClipPlanes(const VecClipPlaneData& vec)
{
int sz = vec.size();
for(int i = 0; i < iNumClipPlanes; ++i)
{
ClipPlaneChunk *clipPlaneChunk = vecClipPlaneDetails[i]._clipPlaneChunk;
clipPlaneChunk->setEnable(false);
if(i < sz)
{
const ClipPlaneData& data = vec[i];
//
// Update the clip plane chunk
//
clipPlaneChunk->setEquation(data._equation);
clipPlaneChunk->setEnable (data._enabled );
//
// and the plane transform core
//
Matrix rotMat;
Vec4f v1(0.f, 0.f, -1.f, 0.f);
Quaternion q(Vec3f(v1), Vec3f(data._equation));
rotMat.setTransform(q);
Matrix mat;
Vec3f v2(0.0f, 0.0f, data._equation[3]);
mat.setTranslate(v2);
mat.multLeft(rotMat);
vecClipPlaneDetails[i]._planeTrafoCore->setMatrix(mat);
}
}
}
//
// build geometry scenegraph Tree
//
//
// We need 3 material groups for the clip plane capping trick:
//
// scene
// |
// +--------------------+--------------------+
// | | |
// group1 (mat1) group2 (mat2) group3 (mat3)
// | | |
// geometry (geo1) geometry (geo2) geometry (geo1)
//
// geo1 : actual geometry to draw
// geo2 : plane geometry coincident with the clip plane
//
// mat1 : has a stencil chunk that clears the stencil buffer first, than
// does the inversion, and has a clip plane chunk that enables one
// clip plane. Sortkey 0.
// mat2 : has a stencil chunk and settings for drawing the clip plane
// geometry. All clip planes but the one coincident with the plane
// are activated. Sortkey 1.
// mat3 : the material used for the actual geometry. All clip planes are
// activated. Sortkey 3.
//
// For each active clip plane copies of the left two branches need to be
// added.
//
NodeTransitPtr buildGeoTree(Node *scene, Geometry *geo1, const Matrix& matrix)
{
// if using a sort key, for each geometry we must ensure that the sortkeys
// are independent. However, using a StateSortingGroup parent node does
// make the use of sortkeys not necessary.
static int k = 0;
//
// Parent nodes for the left two branches
//
VecNodesT vecMaterialNodes1;
VecNodesT vecMaterialNodes2;
for(int i = 0; i < iNumClipPlanes; ++i) // foreach clip plane
{
//
// Branch 1: Imprint the geometry clip plane intersection into the
// stencil buffer.
//
NodeRefPtr geomNode = Node::create();
geomNode->setCore(geo1);
NodeRefPtr materialNode1 = Node::create();
//
// Create stencil material core
//
StencilChunkRefPtr stencilChunk1 = StencilChunk::create();
stencilChunk1->setClearBuffer(1);
stencilChunk1->setStencilFunc(GL_NEVER);
stencilChunk1->setStencilValue(1);
stencilChunk1->setStencilMask(1);
stencilChunk1->setStencilOpFail(GL_INVERT);
stencilChunk1->setStencilOpZFail(GL_INVERT);
stencilChunk1->setStencilOpZPass(GL_INVERT);
ChunkMaterialRefPtr mat1 = ChunkMaterial::create();
mat1->addChunk(stencilChunk1);
mat1->addChunk(vecClipPlaneDetails[i]._clipPlaneChunk);
mat1->setSortKey(2 * i + 0);
//mat1->setSortKey(k*(2*iNumClipPlanes + 1) + 2*i + 0);
MaterialGroupRefPtr mgrp1 = MaterialGroup::create();
mgrp1->setMaterial(mat1);
materialNode1->setCore(mgrp1);
materialNode1->addChild(geomNode); // the primary geometry
vecMaterialNodes1.push_back(materialNode1);
//
// Branch 2: Draw plane at places were the stencil buffer is set
//
NodeRefPtr materialNode2 = Node ::create();
StencilChunkRefPtr stencilChunk2 = StencilChunk::create();
stencilChunk2->setStencilFunc(GL_EQUAL);
stencilChunk2->setStencilValue(1);
stencilChunk2->setStencilMask(1);
stencilChunk2->setStencilOpFail(GL_KEEP);
stencilChunk2->setStencilOpZFail(GL_ZERO);
stencilChunk2->setStencilOpZPass(GL_ZERO);
SimpleMaterialRefPtr mat2 = SimpleMaterial::create();
mat2->setDiffuse(vecClipPlaneDetails[i]._planeColor);
mat2->setSpecular(Color3f(1,1,1));
mat2->setLit(true);
//
// Do clip the plane with all clip planes but the one coincident
// with the plane.
//
for(int j = 0; j < iNumClipPlanes; ++j)
{
if(i != j)
{
mat2->addChunk(vecClipPlaneDetails[j]._clipPlaneChunk);
}
}
mat2->addChunk(stencilChunk2);
mat2->setSortKey(2 * i + 1);
//mat2->setSortKey(k*(2*iNumClipPlanes + 1) + 2*i + 1);
NodeRefPtr planeGeoNode = Node::create();
planeGeoNode->setCore(vecClipPlaneDetails[i]._planeGeometryCore);
NodeRefPtr planeTrafoNode = Node::create();
planeTrafoNode->setCore(vecClipPlaneDetails[i]._planeTrafoCore);
planeTrafoNode->addChild(planeGeoNode);
//
// Neutralize the summed up transformation at this point in the
// scenegraph since we are describing the plane in the same frame
// as the clip planes, i.e. world coordinates.
//
NodeRefPtr planeRootNode = Node::create();
planeRootNode->setCore(InverseTransform::create());
planeRootNode->addChild(planeTrafoNode);
MaterialGroupRefPtr mgrp2 = MaterialGroup::create();
mgrp2->setMaterial(mat2);
materialNode2->setCore(mgrp2);
materialNode2->addChild(planeRootNode); // plane geometry
vecMaterialNodes2.push_back(materialNode2);
}
//
// Finally, set up a branch for drawing the primary geometry
//
NodeRefPtr materialNode3 = Node ::create();
SimpleMaterialRefPtr mat3 = SimpleMaterial::create();
mat3->setDiffuse(Color3f(1,0,0));
mat3->setSpecular(Color3f(1,1,1));
mat3->setLit(true);
//
// Clip the geometry with each clip plane
//
for(int i = 0; i < iNumClipPlanes; ++i)\
{
mat3->addChunk(vecClipPlaneDetails[i]._clipPlaneChunk);
}
mat3->setSortKey(2 * iNumClipPlanes);
//mat3->setSortKey(k*(2*iNumClipPlanes + 1) + iNumClipPlanes * 2);
MaterialGroupRefPtr mgrp3 = MaterialGroup::create();
mgrp3->setMaterial(mat3);
NodeRefPtr geometryNode = Node::create();
geometryNode->setCore(geo1);
materialNode3->setCore (mgrp3);
materialNode3->addChild(geometryNode);
//
// The multi switch core is not actually used in this
// example. However it could be used to define multiple
// render branches and selectively activate and deactivate
// them in a given context.
//
MultiSwitchRefPtr selectCore = MultiSwitch::create();
selectCore->setSwitchMode(MultiSwitch::ALL);
//
// Add the branches to some parent node.
//
NodeRefPtr selectNode = Node::create();
selectNode->setCore(selectCore);
for(int i = 0; i < iNumClipPlanes; ++i)
{
selectNode->addChild(vecMaterialNodes1[i]);
selectNode->addChild(vecMaterialNodes2[i]);
}
selectNode->addChild(materialNode3);
//
// In order to avoid sort keys which grow with the number of primary
// geometry nodes, a node carrying a Stage core is inserted
// into the tree.
//
PassiveBackgroundRefPtr passBkg = PassiveBackground::create();
SimpleStageRefPtr stageCore;
NodeRefPtr stageNode = makeCoredNode<SimpleStage>(&stageCore);
stageCore->setInheritedTarget(true);
stageCore->setCamera (mgr->getCamera());
stageCore->setBackground(passBkg );
stageNode->addChild(selectNode);
//
// Finally, the geometry should be transformable
//
TransformRefPtr transfCore;
NodeRefPtr transfNode = makeCoredNode<Transform>(&transfCore);
transfCore->setMatrix(matrix);
transfNode->addChild(stageNode);
//trafoNode->addChild(selectNode); // if using sort keys use this
// instead of the former line.
k++;
return NodeTransitPtr(transfNode);
}
//
// redraw the window
//
void display(void)
{
// render
mgr->redraw();
// all done, swap
glutSwapBuffers();
}
//
// react to size changes
//
void reshape(int w, int h)
{
mgr->resize(w,h);
glutPostRedisplay();
}
// react to mouse button presses
void mouse(int button, int state, int x, int y)
{
if (state)
mgr->mouseButtonRelease(button, x, y);
else
mgr->mouseButtonPress(button, x, y);
glutPostRedisplay();
}
//
// react to mouse motions with pressed buttons
//
void motion(int x, int y)
{
mgr->mouseMove(x, y);
glutPostRedisplay();
}
//
// react to keys
//
void keyboard(unsigned char k, int, int)
{
static Real32 val0 = 0.f;
static Real32 val1 = 0.f;
static Real32 x1 = 0.f;
static Real32 y1 = 0.f;
static Real32 z1 = 0.f;
static Real32 x2 = 0.f;
static Real32 y2 = 0.f;
static Real32 z2 = 0.f;
switch(k)
{
case ' ':
{
SceneGraphPrinter sgp(mgr->getRoot());
sgp.printDownTree(std::cout);
}
break;
case '1': // enable/disable clip plane 0
{
vecClipPlaneData[0]._enabled = !vecClipPlaneData[0]._enabled;
updateClipPlanes(vecClipPlaneData);
}
break;
case '2': // enable/disable clip plane 1
{
vecClipPlaneData[1]._enabled = !vecClipPlaneData[1]._enabled;
updateClipPlanes(vecClipPlaneData);
}
break;
case '3': // enable/disable box geometry
{
if(vecGeometries[0] == NULL)
{
Matrix matrix;
Vec3f v(10.f, 0.f, 15.f);
matrix.setTranslate(v);
GeometryRefPtr boxGeo = makeBoxGeo(15, 15, 15, 1, 1, 1);
NodeRefPtr boxTree = buildGeoTree(scene, boxGeo, matrix);
vecGeometries[0] = boxTree;
scene->addChild(boxTree);
}
else
{
scene->subChild(vecGeometries[0]);
vecGeometries[0] = NULL;
}
// mgr->showAll();
// mgr->redraw();
}
break;
case '4': // enable/disable torus geometry
{
if (vecGeometries[1] == NULL)
{
Matrix matrix;
Vec3f v( 0.f, 10.f, 0.f);
matrix.setTranslate(v);
GeometryRefPtr torusGeo = makeTorusGeo(2, 6, 8, 16);
NodeRefPtr torusTree = buildGeoTree(scene, torusGeo,
matrix);
vecGeometries[1] = torusTree;
scene->addChild(torusTree);
}
else
{
scene->subChild(vecGeometries[1]);
vecGeometries[1] = NULL;
}
// mgr->showAll();
// mgr->redraw();
}
break;
case 'n': // move clip plane 0 opposite to the normal direction of the
plane
{
val0 -= 0.2;
vecClipPlaneData[0]._equation[3] = val0;
updateClipPlanes(vecClipPlaneData);
}
break;
case 'm': // move clip plane 0 in the normal direction of the plane
{
val0 += 0.2;
vecClipPlaneData[0]._equation[3] = val0;
updateClipPlanes(vecClipPlaneData);
}
break;
case ',': // move clip plane 1 opposite to the normal direction of the
plane
{
val1 -= 0.2;
vecClipPlaneData[1]._equation[3] = val1;
updateClipPlanes(vecClipPlaneData);
}
break;
case '.': // move clip plane 1 in the normal direction of the plane
{
val1 += 0.2;
vecClipPlaneData[1]._equation[3] = val1;
updateClipPlanes(vecClipPlaneData);
}
break;
case 'q': // move box in -x direction
{
x1 -= 0.2f;
Matrix matrix;
Vec3f v(10.f + x1, 0.f + y1, 15.f + z1);
matrix.setTranslate(v);
if(vecGeometries[0] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[0]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'w': // move box in +x direction
{
x1 += 0.2f;
Matrix matrix;
Vec3f v(10.f + x1, 0.f + y1, 15.f + z1);
matrix.setTranslate(v);
if(vecGeometries[0] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[0]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'a': // move box in -y direction
{
y1 -= 0.2f;
Matrix matrix;
Vec3f v(10.f + x1, 0.f + y1, 15.f + z1);
matrix.setTranslate(v);
if(vecGeometries[0] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[0]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 's': // move box in +y direction
{
y1 += 0.2f;
Matrix matrix;
Vec3f v(10.f + x1, 0.f + y1, 15.f + z1);
matrix.setTranslate(v);
if(vecGeometries[0] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[0]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'y': // move box in -z direction
{
z1 -= 0.2f;
Matrix matrix;
Vec3f v(10.f + x1, 0.f + y1, 15.f + z1);
matrix.setTranslate(v);
if(vecGeometries[0] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[0]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'x': // move box in +z direction
{
z1 += 0.2f;
Matrix matrix;
Vec3f v(10.f + x1, 0.f + y1, 15.f + z1);
matrix.setTranslate(v);
if(vecGeometries[0] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[0]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'e': // move torus in -x direction
{
x2 -= 0.2f;
Matrix matrix;
Vec3f v( 0.f + x2, 10.f + y2, 0.f + z2);
matrix.setTranslate(v);
if(vecGeometries[1] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[1]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'r': // move torus in +x direction
{
x2 += 0.2f;
Matrix matrix;
Vec3f v( 0.f + x2, 10.f + y2, 0.f + z2);
matrix.setTranslate(v);
if(vecGeometries[1] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[1]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'd': // move torus in -y direction
{
y2 -= 0.2f;
Matrix matrix;
Vec3f v( 0.f + x2, 10.f + y2, 0.f + z2);
matrix.setTranslate(v);
if(vecGeometries[1] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[1]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'f': // move torus in +y direction
{
y2 += 0.2f;
Matrix matrix;
Vec3f v( 0.f + x2, 10.f + y2, 0.f + z2);
matrix.setTranslate(v);
if(vecGeometries[1] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[1]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'c': // move torus in -z direction
{
z2 -= 0.2f;
Matrix matrix;
Vec3f v( 0.f + x2, 10.f + y2, 0.f + z2);
matrix.setTranslate(v);
if(vecGeometries[1] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[1]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 'v': // move torus in +z direction
{
z2 += 0.2f;
Matrix matrix;
Vec3f v( 0.f + x2, 10.f + y2, 0.f + z2);
matrix.setTranslate(v);
if(vecGeometries[1] != NULL)
{
TransformRefPtr transformCore =
dynamic_cast<Transform *>(vecGeometries[1]->getCore());
transformCore->setMatrix(matrix);
}
}
break;
case 27:
{
cleanup();
osgExit();
exit(0);
}
break;
}
glutPostRedisplay();
}
int doMain(int argc, char **argv)
{
osgInit(argc,argv);
// GLUT init
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_STENCIL | GLUT_DOUBLE);
glutCreateWindow("OpenSG");
glutReshapeFunc(reshape);
glutDisplayFunc(display);
glutIdleFunc(display);
glutMouseFunc(mouse);
glutMotionFunc(motion);
glutKeyboardFunc(keyboard);
PassiveWindowRefPtr pwin=PassiveWindow::create();
pwin->init();
// create the SimpleSceneManager helper
mgr = new SimpleSceneManager;
// create the window and initial camera/viewport
mgr->setWindow(pwin);
//
// Implementation details:
// For each clip plane we provide a ClipPlaneChunk, the plane geometry,
// the plane transform core and at least a plane color conveniently in
// a vector of type VecClipPlaneDetailsT. The next function call
// initializes this data structure.
//
createClipPlaneDetails();
//
// The scene
//
scene = Node::create();
scene->setCore(Group::create());
//
// A place for accessing the box and torus.
//
vecGeometries.push_back(NULL);
vecGeometries.push_back(NULL);
//
// Build concrete clipping planes and update the clip plane details.
//
ClipPlaneData data1;
ClipPlaneData data2;
data1._equation = Vec4f(0,0,1,0);
data1._enabled = true;
data2._equation = Vec4f(1,0,0,0);
data2._enabled = false;
vecClipPlaneData.push_back(data1);
vecClipPlaneData.push_back(data2);
updateClipPlanes(vecClipPlaneData);
keyboard('3',-1,-1);
keyboard('4',-1,-1);
// tell the manager what to manage
mgr->setRoot(scene);
// show the whole scene
mgr->showAll();
mgr->redraw();
pwin->dumpExtensions();
}
int main(int argc, char *argv[])
{
doMain(argc, argv);
// GLUT main loop
glutMainLoop();
//
// Clean up the global held data
//
cleanup();
osgExit();
return 0;
}
------------------------------------------------------------------------------
This SF.net email is sponsored by:
SourcForge Community
SourceForge wants to tell your story.
http://p.sf.net/sfu/sf-spreadtheword
_______________________________________________
Opensg-core mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/opensg-core