Game Development Community

SpherePointGen [T3D]

by James Ford · 08/30/2009 (3:31 pm) · 11 comments

This is a SceneObject derived class that is intended as a helper for generating uniformly random distributed points within a sphere, allowing you to preview and tweak some parameters from within the WorldEditor, and then dump those points to file as a hlsl-style float3 array.

I created this as a helper for generating the sample points used by the T3D SSAO PostEffect.

Installation:
Place both source files in the engine/source/T3D folder and rebuild.

Usage:
Start T3D, open a level, open the WorldEditor, then type genericCreateObject("SpherePointGen"); in the console. Select it and tweak values from the Inspector. To dump the points to disk give your object a name and call the viewPoints method from the console.

Extensions:
It could be useful to extend this class to not ONLY do uniformly random distributions but also, for example, poisson distribution or even just applying a little bias.


spherePointGen.h

#ifndef _SPHEREPOINTGEN_H_
#define _SPHEREPOINTGEN_H_

#ifndef _SCENEOBJECT_H_
#include "sceneGraph/sceneObject.h"
#endif
#ifndef _GFXSTATEBLOCK_H_
#include "gfx/gfxStateBlock.h"
#endif
#ifndef _GFXVERTEXBUFFER_H_
#include "gfx/gfxVertexBuffer.h"
#endif
#ifndef _GFXPRIMITIVEBUFFER_H_
#include "gfx/gfxPrimitiveBuffer.h"
#endif
#ifndef _TVECTOR_H_
#include "core/util/tVector.h"
#endif


class SpherePointGen : public SceneObject
{
   typedef SceneObject Parent;

   enum MaskBits 
   {
      TransformMask = Parent::NextFreeMask << 0,
      UpdateMask    = Parent::NextFreeMask << 1,
      NextFreeMask  = Parent::NextFreeMask << 2
   };

   Vector<Point3F> mPoints;
   F32 mRadius;
   S32 mCount;
   U32 mSeed;
   F32 mPointRenderSize;
   bool mShowBoundingSphere;

public:

   SpherePointGen();
   virtual ~SpherePointGen();

   DECLARE_CONOBJECT(SpherePointGen);

   static void initPersistFields();
   virtual void inspectPostApply();

   virtual bool onAdd();
   virtual void onRemove();
   virtual void setTransform( const MatrixF &mat );
   virtual void setScale( const VectorF & scale );
   virtual U32 packUpdate( NetConnection *conn, U32 mask, BitStream *stream );
   virtual void unpackUpdate( NetConnection *conn, BitStream *stream );

   void createPoints();

   bool prepRenderImage( SceneState *state, const U32 stateKey, 
      const U32 startZone, const bool modifyBaseZoneState = false);

   void render( ObjectRenderInst *ri, SceneState *state, BaseMatInstance *overrideMat );

   void viewPoints();  
};

#endif // _SPHEREPOINTGEN_H_

spherePointGen.cpp

#include "platform/platform.h"
#include "T3D/spherePointGen.h"

#include "console/consoleTypes.h"
#include "math/mathIO.h"
#include "sceneGraph/sceneState.h"
#include "core/stream/bitStream.h"
#include "gfx/gfxDrawUtil.h"
#include "renderInstance/renderPassManager.h"
#include "core/stream/fileStream.h"

IMPLEMENT_CO_NETOBJECT_V1(SpherePointGen);


SpherePointGen::SpherePointGen()
 : mRadius( 1.0f ),
   mCount( 50 ),
   mSeed( 100 ),
   mPointRenderSize( 0.03f ),
   mShowBoundingSphere( true )
{
   mNetFlags.set( Ghostable | ScopeAlways );
   mTypeMask |= StaticObjectType;
}

SpherePointGen::~SpherePointGen()
{
}

void SpherePointGen::initPersistFields()
{
   addField( "radius", TypeF32, Offset( mRadius, SpherePointGen ) );
   addField( "count", TypeS32, Offset( mCount, SpherePointGen ) );
   addField( "seed", TypeS32, Offset( mSeed, SpherePointGen ) );
   addField( "pointRenderSize", TypeF32, Offset( mPointRenderSize, SpherePointGen ) );
   addField( "showBoundingSphere", TypeBool, Offset( mShowBoundingSphere, SpherePointGen ) );  
   
   Parent::initPersistFields();
}


void SpherePointGen::inspectPostApply()
{   
   mRadius = getMax( mRadius, 0.01f );
   mCount = mClamp( mCount, 1, 500 );
   mPointRenderSize = mClampF( mPointRenderSize, 0.001f, 100.0f );

   setMaskBits( UpdateMask );
   Parent::inspectPostApply();
}

bool SpherePointGen::onAdd()
{
   if ( !Parent::onAdd() )
      return false;

   mObjBox.set( Point3F( -0.5f, -0.5f, -0.5f ),
      Point3F(  0.5f,  0.5f,  0.5f ) );

   resetWorldBox();

   addToScene();

   if ( isClientObject() )
      createPoints();

   return true;
}

void SpherePointGen::onRemove()
{
   removeFromScene();

   Parent::onRemove();
}

void SpherePointGen::setTransform(const MatrixF & mat)
{
   Parent::setTransform( mat );

   setMaskBits( TransformMask );
}

void SpherePointGen::setScale( const VectorF & scale )
{  
   // Scale always equals the radius!
   VectorF overrideScale( mRadius, mRadius, mRadius );
   Parent::setScale( overrideScale );   
}

U32 SpherePointGen::packUpdate( NetConnection *conn, U32 mask, BitStream *stream )
{
   U32 retMask = Parent::packUpdate( conn, mask, stream );

   if ( stream->writeFlag( mask & TransformMask ) )
   {
      mathWrite(*stream, getTransform());
      mathWrite(*stream, getScale());
   }

   if ( stream->writeFlag( mask & UpdateMask ) )
   {
      stream->write( mRadius );
      stream->write( mCount );
      stream->write( mSeed );
      stream->write( mPointRenderSize );
      stream->writeFlag( mShowBoundingSphere );
   }

   return retMask;
}

void SpherePointGen::unpackUpdate(NetConnection *conn, BitStream *stream)
{
   Parent::unpackUpdate(conn, stream);

   if ( stream->readFlag() )  // TransformMask
   {
      mathRead(*stream, &mObjToWorld);
      mathRead(*stream, &mObjScale);

      setTransform( mObjToWorld );
   }

   if ( stream->readFlag() )   // UpdateMask
   {
      stream->read( &mRadius );
      stream->read( &mCount );
      stream->read( &mSeed );
      stream->read( &mPointRenderSize );
      mShowBoundingSphere = stream->readFlag();

      if ( isProperlyAdded() )
      {
         createPoints();
      }
   }
}


void SpherePointGen::createPoints()
{
   mPoints.clear();

   // Random distribution

   MRandomLCG rand( mSeed );

   for ( U32 i = 0; i < mCount; i++ )
   {
      Point3F pnt;

      U32 realityCheck = 0;
      while ( true )
      {
         pnt.set( rand.randF(-mRadius ,mRadius ), rand.randF(-mRadius ,mRadius ), rand.randF(-mRadius ,mRadius ) );         

         if ( pnt.len() <= mRadius )
            break;

         AssertFatal( ++realityCheck < 100, "SpherePointGen::createPoints - Something is wrong here!" );
      }

      mPoints.push_back( pnt );
   }
}

bool SpherePointGen::prepRenderImage( SceneState *state, const U32 stateKey, 
                                          const U32 startZone, const bool modifyBaseZoneState)
{
   if ( isLastState( state, stateKey ) )
      return false;

   setLastState(state, stateKey);

   if ( state->isObjectRendered( this ) ) 
   {
      ObjectRenderInst *ri = state->getRenderPass()->allocInst<ObjectRenderInst>();
      ri->renderDelegate.bind( this, &SpherePointGen::render );
      ri->type = RenderPassManager::RIT_Object;
      ri->defaultKey = 0;
      ri->defaultKey2 = 0;
      state->getRenderPass()->addInst( ri );
   }

   return false;
}

void SpherePointGen::render( ObjectRenderInst *ri, SceneState *state, BaseMatInstance *overrideMat )
{
   if ( overrideMat )
      return;      

   GFXDrawUtil *drawer = GFX->getDrawUtil();

   const MatrixF &objectMat = getRenderTransform();
   const Point3F &objectPos = objectMat.getPosition();

   GFXStateBlockDesc defaultDesc; 

   GFXStateBlockDesc sphereDesc;
   sphereDesc.setBlend( true );
   sphereDesc.setZReadWrite( true, false );
   sphereDesc.setCullMode( GFXCullNone );

   if ( mShowBoundingSphere )   
      drawer->drawSphere( sphereDesc, mRadius, objectPos, ColorI(0,0,150,30) );

   const U32 numPoints = mPoints.size();

   if ( numPoints == 0 )
      return;      
              
   ColorI pntColor;
   const Point3F pntSize( mPointRenderSize, mPointRenderSize, mPointRenderSize );

   for ( U32 i = 0; i < numPoints; i++ )
   {
      Point3F pnt = mPoints[i];
      objectMat.mulV( pnt );
      
      pntColor = ColorI::WHITE * ( 1.0f - pnt.len() / mRadius );

      drawer->drawCube( defaultDesc, pntSize, pnt + objectPos, pntColor );         
   }
}

void SpherePointGen::viewPoints()
{
   // Hack: if this gets called on the server object by accident
   // redirect to the local client object since its the
   // one that actually generates points.
   if ( isServerObject() )
   {
      if ( getClientObject() )
      {
         static_cast< SpherePointGen* >( getClientObject() )->viewPoints();
         return;
      }
   }

   String filename = Torque::FS::MakeUniquePath( "", "SpherePoints", "txt" );

   FileStream *fstream = FileStream::createAndOpen( filename, Torque::FS::File::Write );
   if ( !fstream )         
      return;
   
   char buff[256];
   dSprintf( buff, 256, "float3 spherePts[%d] =n{n", mPoints.size() );
   fstream->writeText( buff );
     
   for ( U32 i = 0; i < mPoints.size(); i++ )
   {
      const Point3F &pnt = mPoints[i];

      if ( i != mPoints.size() - 1 )
         dSprintf( buff, 256, "tfloat3( %f, %f, %f ),n", pnt.x, pnt.y, pnt.z );
      else
         dSprintf( buff, 256, "tfloat3( %f, %f, %f )n", pnt.x, pnt.y, pnt.z );  

      fstream->writeText( buff );
   }
   
   fstream->writeText( "};n" );

   fstream->close();
   delete fstream;  

   Platform::openFile( filename );
}

ConsoleMethod( SpherePointGen, viewPoints, void, 2, 2, "" )
{
   object->viewPoints();
}

#1
08/30/2009 (5:17 pm)
Awesome James, your code is going to be very useful for lots of things.
#2
08/31/2009 (12:27 am)
can be useful for this?
#3
09/02/2009 (4:21 pm)
Hmm... I see some interesting possibilities for this. Thumbs up!
#4
10/27/2009 (2:26 am)
Interesting code James.
I happened to get interested in distributing random points over the surface of a sphere or a disk a while ago. The difficult part is both getting the distribution to be uniform and to execute in linear time or even with guaranteed termination. ie, avoiding the test-and-reject core loop.
for sphere-surfaces and disks i believe this is possible by simply choosing a random theta uniformly on [-pi, pi] and then choosing radius as the square-root of a uniform variable on [0, 1]. there's some interactive visualizations of this approach here. i would hazard a guess that for solid spheres something similar would work. ie, theta = rand(-pi, pi), phi = sqrt(-pi/2, pi/2), rho = sqrt(0, 1).
#5
10/27/2009 (2:30 am)
doh accidental post. ignore.
#6
10/28/2009 (5:06 pm)
I believe that by generating the x, y, z as independent randoms and then rejecting outside the radius guarantees a uniformly random distribution, whereas, generating a random angle and a random distance (0-radius) results in a clustering around the center.

With the second approach, think of the odds you will get a point around the center... the first random can be any angle so long as the second random is close to zero. But to get a point near the radius on a particular side you need to generate a particular angle with the first random and a distance close to radius.

So you end up with the points uniformly distributed among all angles, but as distance from the center increases the density of the points spreads out.
#7
10/28/2009 (5:11 pm)
When I describe this as "uniformly random" I mean that the chance of a point being placed anywhere within the volume is uniform, not that the actual distribution is guaranteed to have any properties like all points equal distance from one another etc. If you need a distribution like that you might want to investigate "Poisson disks/distribution".

There was a paper around somewhere that described a very simple algorithm for generating Poisson distributions ( I'm thinking it was actually made into a java app or something by the Luma arcade guys of all random things ).

You do some spacial partitioning and then place points into random grid elements, then repartition in between placing.
#8
10/28/2009 (5:38 pm)
> I believe that by generating the x, y, z as independent randoms and then rejecting outside the radius guarantees a uniformly random distribution

yup, it does.

> generating a random angle and a random distance (0-radius) results in a clustering around the center.

yup. but that's not what i propose.

i propose that for a disk, the following results in a uniform distribution without increased density near the pole:

theta = random(-pi, pi)
radius = sqrt(random(0, 1))

check out pics & app here: elenzil.com/progs/randompoints/index.html




#9
10/28/2009 (7:40 pm)
Very cool little app / screenshots.

Those results look a lot better than without the square root, but without understanding why that would mathematically work I feel very skeptical... hehe.

#10
10/28/2009 (8:07 pm)
yeah, i never quite got around to proving it works.
i tried a couple times but i've been out of school so long it was rough going.. heh.
it sure seems correct to the eye tho!
#11
01/14/2010 (1:59 am)
Thanks, integrated into my project nicely.