Possible bug/quirk in PxScene::shiftOrigin which degrades performance when stressed

Hi folks!
I’ve been playing with the dynamic origin functionality PhysX provides and noticed my simulation ticks and origin shifts gradually start to take longer if I shift the origin a lot.

I’m calling shiftOrigin every frame - don’t worry I’m not going to do this in my final application, it was just so I could quickly benchmark the performance of the function. If I leave it running for a few minutes then gradually the simulate/fetchResults calls take longer settling at around twice their original time. The call to shiftOrigin also slows down settling at around 7 times it’s original time span - measured using the windows high performance timer.

The scene is reasonably complex so I could get meaningful numbers - there’s 550 cubes arranged in 10 stacks of 55, a rigid body with a compound collision shape, a simple triangle mesh for the ground and some planes to keep everything boxed in. I also run a fully fixed time step (60Hz) for reproduce-ability. I also ran in release!

I’ve also repeated the test with the same scene and logic except I only called shiftOrigin every 5 seconds instead of every frame and left it running for 14+ hours (so it was about the same number of calls to shiftOrigin in total as my stress tests made). The slow down wasn’t present in this test - in-fact the shiftOrigin call got slightly faster over time (but only by 20 microseconds).

I’m not really sure what could be going on that degrades the performance so gradually - I would’ve thought the time taken to complete shiftOrigin would be quite stable when the numbers of objects isn’t changing. Anyway - I thought I’d make a post to get feedback/make people aware of this.

I’m probably being an idiot somewhere in my test, so I’ve extracted my little test class .h and .cpp for anyone to have a go with. All you need to do is initialise it, and call update in a loop passing in time. It will deal with the rest.
.h

#pragma once

#include "DXUT.h"
#include "PxPhysicsAPI.h"


#include <fstream>
#include <iostream>

struct MaterialUserData
{
    int m_someData;
};

class SimplePhysXManager
{
public:
	SimplePhysXManager();
	~SimplePhysXManager();

	bool Initialise( bool sendDebugToFile );
	void Update( double totalGameTime, float elapsedFrameTime );
	void ShutDown();

private:
	void SetupVisualDebugger();
	void CreatePhysicsWorldAndObjects();
    void MakeTheGround();

    void WriteOutFrameTimeUpdate( double gameTime, LARGE_INTEGER timeOut );
    void WriteOutFrameTimeShift( double gameTime, LARGE_INTEGER timeOut );

	physx::PxRigidDynamic* createDynamic( const physx::PxTransform& t, const physx::PxGeometry& geometry, physx::PxU32 collisionGroup, const physx::PxVec3& velocity = physx::PxVec3( 0 ) );
	void SimplePhysXManager::createStack( const physx::PxTransform& t, physx::PxU32 size, physx::PxReal halfExtent );
public:
	static physx::PxDefaultErrorCallback s_defaultPhysXErrorCallback;
	static physx::PxDefaultAllocator s_defaultAllocatorCallback;
	
public:
	physx::PxFoundation*				m_foundationObject;
	physx::PxProfileZoneManager*		m_profileZoneManager;
	physx::PxPhysics*					m_physics;
    physx::PxCooking*                   m_cooking;

	physx::PxDefaultCpuDispatcher*		m_defaultCpuDispatcher;

	physx::PxVisualDebugger*			m_visualDebugger;
	physx::PxVisualDebuggerConnection*	m_visualDebuggerConnection;
	physx::debugger::renderer::PvdUserRenderer* m_debugRenderer;

	physx::PxScene*						m_scene;
	physx::PxMaterial*					m_material;

	physx::PxRigidStatic*				m_groundPlane;

	float								m_accumulatedTime;
	bool								m_sendDebugToFile;

    MaterialUserData                    m_materialUserDatas[ 4 ];

    std::ofstream outputFileUpdate;
    std::ofstream outputFileShift;

    LARGE_INTEGER lastWroteToFile;
    LARGE_INTEGER lastWroteToFileUpdate;
};

.cpp

#include "SimplePhysXManager.h"
#include "physxvisualdebuggersdk/PvdUserRenderer.h"

using namespace physx;

PxDefaultErrorCallback SimplePhysXManager::s_defaultPhysXErrorCallback;
PxDefaultAllocator SimplePhysXManager::s_defaultAllocatorCallback;

SimplePhysXManager::SimplePhysXManager(  )
	: m_foundationObject( nullptr )
	, m_profileZoneManager( nullptr )
	, m_physics( nullptr )
    , m_cooking( nullptr )
	, m_defaultCpuDispatcher( nullptr )
	, m_visualDebugger( nullptr )
	, m_visualDebuggerConnection( nullptr )
	, m_debugRenderer( nullptr )
	, m_scene( nullptr )
	, m_material( nullptr )
	, m_groundPlane( nullptr )
	, m_accumulatedTime( 0.0f )
	, m_sendDebugToFile( false )
{
}

SimplePhysXManager::~SimplePhysXManager(void)
{
}

bool SimplePhysXManager::Initialise( bool sendDebugToFile )
{
	bool succesfullyInitialised = true;

	m_sendDebugToFile = sendDebugToFile;

	m_foundationObject = PxCreateFoundation( PX_PHYSICS_VERSION, s_defaultAllocatorCallback, s_defaultPhysXErrorCallback );

	if( m_foundationObject )
	{
		m_profileZoneManager = &PxProfileZoneManager::createProfileZoneManager( m_foundationObject );

		bool recordMemoryAllocations = true;
		
		m_physics = PxCreatePhysics( PX_PHYSICS_VERSION, *m_foundationObject, PxTolerancesScale(), recordMemoryAllocations, m_profileZoneManager );

        m_cooking = PxCreateCooking( PX_PHYSICS_VERSION, *m_foundationObject, PxCookingParams( m_physics->getTolerancesScale() ) );

		SetupVisualDebugger();

		CreatePhysicsWorldAndObjects();
	}

    outputFileUpdate.open( "C:\\PhysXExperiment\\test-update.csv", std::ios::app );
    outputFileShift.open( "C:\\PhysXExperiment\\test-shift_real.csv", std::ios::app );

    QueryPerformanceCounter(&lastWroteToFile);
    QueryPerformanceCounter(&lastWroteToFileUpdate);

	return m_foundationObject
		&& m_profileZoneManager
		&& m_physics
		&& m_scene
		//&& m_visualDebugger
		//&& m_visualDebuggerConnection
		&& m_material
		&& m_defaultCpuDispatcher
		&& m_groundPlane;
		//&& ( m_debugRenderer || m_sendDebugToFile );
}

void SimplePhysXManager::WriteOutFrameTimeUpdate( double gameTime, LARGE_INTEGER timeOut )
{
    outputFileUpdate << "\n" << gameTime << "," << timeOut.QuadPart;
    outputFileUpdate.flush();
}

void SimplePhysXManager::WriteOutFrameTimeShift( double gameTime, LARGE_INTEGER timeOut )
{
    outputFileShift << "\n" << gameTime << "," << timeOut.QuadPart;
    outputFileShift.flush();
}

void SimplePhysXManager::Update( double totalGameTime, float elapsedFrameTime )
{
    LARGE_INTEGER ElapsedSimulationTime;
    LARGE_INTEGER ElapsedShiftTime;
    ElapsedSimulationTime.QuadPart = 0;
    ElapsedShiftTime.QuadPart = 0;


    float maximumFrameTime = 1.0f / 60.0f;

	m_accumulatedTime += elapsedFrameTime;

	// work out how much time we're going to consume
	int numberOfIterationsNeeded = ( int )( m_accumulatedTime / maximumFrameTime );

	// remove that from the accumulator
	m_accumulatedTime -= ( ( float )numberOfIterationsNeeded * maximumFrameTime );

	// consume the time
	for( int i = 0; i < numberOfIterationsNeeded; ++i )
	{
        LARGE_INTEGER Frequency;
        QueryPerformanceFrequency(&Frequency); 


        //////////////////////////////////////////////////////////////////////////
        // Benchmark the frame update

        LARGE_INTEGER StartingTime, EndingTime;//, ElapsedMicroseconds;
        QueryPerformanceCounter(&StartingTime);

        //////////////////////////////////////////////////////////////////////////
		m_scene->simulate( maximumFrameTime );
		m_scene->fetchResults( true );
        //////////////////////////////////////////////////////////////////////////


        QueryPerformanceCounter(&EndingTime);
        ElapsedSimulationTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;

        ElapsedSimulationTime.QuadPart *= 1000000;
        ElapsedSimulationTime.QuadPart /= Frequency.QuadPart;

        if( ElapsedSimulationTime.QuadPart != 0 )
        {
            WCHAR message[256];
            swprintf_s(message, L"Frame update time: %d\n", ElapsedSimulationTime.QuadPart );
        
            OutputDebugString( message );
        }



        //////////////////////////////////////////////////////////////////////////
        // Benchmark the position update



		// LONGTERMTIMER
        //LARGE_INTEGER CurrentTime;
        //LARGE_INTEGER ElapsedMicrosecondsFrame;
        //QueryPerformanceCounter(&CurrentTime);
        //
        //ElapsedMicrosecondsFrame.QuadPart = CurrentTime.QuadPart - lastWroteToFile.QuadPart;
        //
        //ElapsedMicrosecondsFrame.QuadPart *= 1000000;
        //ElapsedMicrosecondsFrame.QuadPart /= Frequency.QuadPart;
        //
        //bool shiftOrigin = false;
        //
        //if( ElapsedMicrosecondsFrame.QuadPart > 5000000 )
        //{
        //    lastWroteToFile.QuadPart = CurrentTime.QuadPart;
        //    shiftOrigin = true;
        //}
        //
        //ElapsedMicrosecondsFrame.QuadPart = CurrentTime.QuadPart - lastWroteToFileUpdate.QuadPart;
        //
        //ElapsedMicrosecondsFrame.QuadPart *= 1000000;
        //ElapsedMicrosecondsFrame.QuadPart /= Frequency.QuadPart;
        //
        //if( ElapsedMicrosecondsFrame.QuadPart > 1000000 )
        //{
        //    lastWroteToFileUpdate.QuadPart = CurrentTime.QuadPart;
        //    WriteOutFrameTimeUpdate( totalGameTime, ElapsedSimulationTime );
        //}








        QueryPerformanceCounter(&StartingTime);

		// LONGTERMTIMER
        //if( shiftOrigin )
        {
            m_scene->shiftOrigin( PxVec3( 0.0f, 0.0f, 0.0f ) );
        }
        //////////////////////////////////////////////////////////////////////////

        QueryPerformanceCounter(&EndingTime);
        ElapsedShiftTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;

        ElapsedShiftTime.QuadPart *= 1000000;
        ElapsedShiftTime.QuadPart /= Frequency.QuadPart;




		// LONGTERMTIMER
        //if( shiftOrigin )
        //{
        //    WriteOutFrameTimeShift( totalGameTime, ElapsedShiftTime );
        //}

        if( ElapsedShiftTime.QuadPart != 0 )
        {
            WCHAR message[256];
            swprintf_s(message, L"Origin shift time: %d\n", ElapsedShiftTime.QuadPart );
        
            OutputDebugString( message );
        }
	}
}

void SimplePhysXManager::ShutDown()
{
    outputFileUpdate.flush();
    outputFileUpdate.close();

    outputFileShift.flush();
    outputFileShift.close();

	if( m_visualDebuggerConnection )
	{
		m_visualDebuggerConnection->release();
	}
	
	if( m_physics )
	{
		m_physics->release();
	}

	if( m_profileZoneManager )
	{
        m_profileZoneManager->release();
    }

	if( m_foundationObject )
	{
		m_foundationObject->release();
	}	
}

void SimplePhysXManager::SetupVisualDebugger()
{
	if( m_physics && m_physics->getPvdConnectionManager() )
	{
		m_visualDebugger = m_physics->getVisualDebugger();

		m_visualDebugger->setVisualizeConstraints( true );
		m_visualDebugger->setVisualDebuggerFlag( PxVisualDebuggerFlag::eTRANSMIT_SCENEQUERIES, true );

		if( m_sendDebugToFile )
		{
			m_visualDebuggerConnection = PxVisualDebuggerExt::createConnection( m_physics->getPvdConnectionManager(), "C:\\PhysXOutput\\myTestOutput.pxd2" );
		}
		else
		{
			m_visualDebuggerConnection = PxVisualDebuggerExt::createConnection( m_physics->getPvdConnectionManager(), "127.0.0.1", 5425, 10 );

            if( m_visualDebuggerConnection )
            {
			    m_debugRenderer = &m_visualDebuggerConnection->createRenderer();
            }
		}
	}
}

void SimplePhysXManager::CreatePhysicsWorldAndObjects()
{
	/************************************************************************/
	/* Make the scene                                                       */
	/************************************************************************/
	PxSceneDesc sceneDescription( m_physics->getTolerancesScale() );
	sceneDescription.gravity = PxVec3( 0.0f, -9.81f, 0.0f );
	m_defaultCpuDispatcher = PxDefaultCpuDispatcherCreate( 6 );
	sceneDescription.cpuDispatcher = m_defaultCpuDispatcher;
	
	sceneDescription.filterShader = PxDefaultSimulationFilterShader;

	m_scene = m_physics->createScene( sceneDescription );

	/************************************************************************/
	/* Make a material                                                      */
	/************************************************************************/
	m_material = m_physics->createMaterial( 0.5f, 0.5f, 0.6f );

	
	if( m_material && m_scene )
	{
        /************************************************************************/
        /* Make a ground object                                                 */
        /************************************************************************/
		MakeTheGround();

		/************************************************************************/
		/* Make some stacks and a projectile to knock them over                 */
		/************************************************************************/
		PxReal stackZ = 10.0f;

		for( PxU32 i = 0; i < 10; i++ )
		{
			createStack( PxTransform( PxVec3( 0, 0, stackZ -= 10.0f ) ), 10, 2.0f );
		}

		PxRigidDynamic* rigidBody = createDynamic( PxTransform( PxVec3( 0, 11.5f, stackZ -= 50.0f ) ), PxSphereGeometry( 5.0f ), 1, PxVec3( 0, 0, 125.0f) );
        rigidBody->setName( "Projectile rigid body" );
		
		rigidBody->setAngularVelocity( PxVec3( 5.0f, 5.0f, 5.0f ) );

        PxFilterData filterData( 2, 0, 0, 0 );

		PxShape* nextShape = rigidBody->createShape( PxSphereGeometry( 3.0f ), *m_material, PxTransform( PxVec3( 0.0f, 5.0f, 0.0f ) ) );
        nextShape->setSimulationFilterData( filterData );
        nextShape->setName( "Y+" );
		nextShape = rigidBody->createShape( PxSphereGeometry( 3.0f ), *m_material, PxTransform( PxVec3( 0.0f, -5.0f, 0.0f ) ) );
        nextShape->setSimulationFilterData( filterData );
        nextShape->setName( "Y-" );
		nextShape = rigidBody->createShape( PxSphereGeometry( 3.0f ), *m_material, PxTransform( PxVec3( 5.0f, 0.0f, 0.0f ) ) );
        nextShape->setSimulationFilterData( filterData );
        nextShape->setName( "X+" );
		nextShape = rigidBody->createShape( PxSphereGeometry( 3.0f ), *m_material, PxTransform( PxVec3( -5.0f, 0.0f, 0.0f ) ) );
        nextShape->setSimulationFilterData( filterData );
        nextShape->setName( "X-" );
		nextShape = rigidBody->createShape( PxSphereGeometry( 3.0f ), *m_material, PxTransform( PxVec3( 0.0f, 0.0f, 5.0f ) ) );
        nextShape->setSimulationFilterData( filterData );
        nextShape->setName( "Z+" );

        filterData.word0 = 1;
		nextShape = rigidBody->createShape( PxSphereGeometry( 3.0f ), *m_material, PxTransform( PxVec3( 0.0f, 0.0f, -5.0f ) ) );
        nextShape->setSimulationFilterData( filterData );
        nextShape->setName( "Z-" );
	}
}

void SimplePhysXManager::MakeTheGround()
{
    PxFilterData filterData( 1, 0, 0, 0 );
    float scale = 100.0f;

    PxShape* shapes[ 5 ];
    shapes[ 0 ] = nullptr;
    shapes[ 1 ] = nullptr;
    shapes[ 2 ] = nullptr;
    shapes[ 3 ] = nullptr;
    shapes[ 4 ] = nullptr;

    // create some boundaries
    PxRigidStatic* positiveXSide = PxCreatePlane(*m_physics, PxPlane( PxVec3( 1.0f, 0.0f, 0.0f ), scale ), *m_material );
    m_scene->addActor( *positiveXSide );
    positiveXSide->getShapes( shapes, 5 );
    shapes[ 0 ]->setSimulationFilterData( filterData );

    PxRigidStatic* negativeXSide = PxCreatePlane(*m_physics, PxPlane( PxVec3( -1.0f, 0.0f, 0.0f ), scale ), *m_material );
    m_scene->addActor( *negativeXSide );
    negativeXSide->getShapes( shapes, 5 );
    shapes[ 0 ]->setSimulationFilterData( filterData );

    PxRigidStatic* positiveZSide = PxCreatePlane(*m_physics, PxPlane( PxVec3( 0.0f, 0.0f, 1.0f ), scale ), *m_material );
    m_scene->addActor( *positiveZSide );
    positiveZSide->getShapes( shapes, 5 );
    shapes[ 0 ]->setSimulationFilterData( filterData );

    PxRigidStatic* negativeZSide = PxCreatePlane(*m_physics, PxPlane( PxVec3( 0.0f, 0.0f, -1.0f ), scale ), *m_material );
    m_scene->addActor( *negativeZSide );
    negativeZSide->getShapes( shapes, 5 );
    shapes[ 0 ]->setSimulationFilterData( filterData );

    PxRigidStatic* negativeYSide = PxCreatePlane(*m_physics, PxPlane( PxVec3( 0.0f, -1.0f, 0.0f ), scale ), *m_material );
    m_scene->addActor( *negativeYSide );
    negativeYSide->getShapes( shapes, 5 );
    shapes[ 0 ]->setSimulationFilterData( filterData );

    // create the ground geometry
    PxVec3 meshVertices[ 9 ];

    meshVertices[ 0 ].x = 0.0f * scale;
    meshVertices[ 0 ].y = 0.0f * scale;
    meshVertices[ 0 ].z = 0.0f * scale;
                               
    meshVertices[ 1 ].x = 1.0f * scale;
    meshVertices[ 1 ].y = 0.0f * scale;
    meshVertices[ 1 ].z = 0.0f * scale;
                               
    meshVertices[ 2 ].x = 2.0f * scale;
    meshVertices[ 2 ].y = 0.0f * scale;
    meshVertices[ 2 ].z = 0.0f * scale;
                               
    meshVertices[ 3 ].x = 0.0f * scale;
    meshVertices[ 3 ].y = 0.0f * scale;
    meshVertices[ 3 ].z = 1.0f * scale;
                               
    meshVertices[ 4 ].x = 1.0f * scale;
    meshVertices[ 4 ].y = -0.5f * scale;
    meshVertices[ 4 ].z = 1.0f * scale;
                               
    meshVertices[ 5 ].x = 2.0f * scale;
    meshVertices[ 5 ].y = 0.0f * scale;
    meshVertices[ 5 ].z = 1.0f * scale;
                               
    meshVertices[ 6 ].x = 0.0f * scale;
    meshVertices[ 6 ].y = 0.0f * scale;
    meshVertices[ 6 ].z = 2.0f * scale;
                               
    meshVertices[ 7 ].x = 1.0f * scale;
    meshVertices[ 7 ].y = 0.0f * scale;
    meshVertices[ 7 ].z = 2.0f * scale;
                               
    meshVertices[ 8 ].x = 2.0f * scale;
    meshVertices[ 8 ].y = 0.0f * scale;
    meshVertices[ 8 ].z = 2.0f * scale;

    PxU32 triangleIndices[ 24 ];

    triangleIndices[ 0 ] = 3;
    triangleIndices[ 1 ] = 1;
    triangleIndices[ 2 ] = 0;

    triangleIndices[ 3 ] = 4;
    triangleIndices[ 4 ] = 1;
    triangleIndices[ 5 ] = 3;

    triangleIndices[ 6 ] = 4;
    triangleIndices[ 7 ] = 2;
    triangleIndices[ 8 ] = 1;

    triangleIndices[ 9 ] = 5;
    triangleIndices[ 10 ] = 2;
    triangleIndices[ 11 ] = 4;

    triangleIndices[ 12 ] = 6;
    triangleIndices[ 13 ] = 4;
    triangleIndices[ 14 ] = 3;

    triangleIndices[ 15 ] = 7;
    triangleIndices[ 16 ] = 4;
    triangleIndices[ 17 ] = 6;

    triangleIndices[ 18 ] = 7;
    triangleIndices[ 19 ] = 5;
    triangleIndices[ 20 ] = 4;

    triangleIndices[ 21 ] = 8;
    triangleIndices[ 22 ] = 5;
    triangleIndices[ 23 ] = 7;

    PxMaterialTableIndex materialIndices[ 8 ];

    materialIndices[ 0 ] = 0;
    materialIndices[ 1 ] = 0;
    materialIndices[ 2 ] = 1;
    materialIndices[ 3 ] = 1;
    materialIndices[ 4 ] = 2;
    materialIndices[ 5 ] = 2;
    materialIndices[ 6 ] = 3;
    materialIndices[ 7 ] = 3;

    m_materialUserDatas[ 0 ].m_someData = 1;
    m_materialUserDatas[ 1 ].m_someData = 2;
    m_materialUserDatas[ 2 ].m_someData = 3;
    m_materialUserDatas[ 3 ].m_someData = 4;

    PxMaterial* materialList[ 4 ];
    materialList[ 0 ] = m_physics->createMaterial( 0.1f, 0.1f, 0.0f );
    materialList[ 0 ]->userData = &m_materialUserDatas[ 0 ];

    materialList[ 1 ] = m_physics->createMaterial( 0.2f, 0.2f, 0.33f );
    materialList[ 1 ]->userData = &m_materialUserDatas[ 1 ];

    materialList[ 2 ] = m_physics->createMaterial( 0.3f, 0.3f, 0.66f );
    materialList[ 2 ]->userData = &m_materialUserDatas[ 2 ];

    materialList[ 3 ] = m_physics->createMaterial( 0.4f, 0.4f, 1.0f );
    materialList[ 3 ]->userData = &m_materialUserDatas[ 3 ];

    PxTriangleMeshDesc meshDescription;
    meshDescription.points.data = meshVertices;
    meshDescription.points.count = 9;
    meshDescription.points.stride = sizeof( PxVec3 );

    meshDescription.triangles.data = triangleIndices;
    meshDescription.triangles.count = 8;
    meshDescription.triangles.stride = 3 * sizeof( PxU32 );

    meshDescription.materialIndices.data = materialIndices;
    meshDescription.materialIndices.stride = sizeof( PxMaterialTableIndex );

    PxTriangleMesh* groundTriangleMesh = m_cooking->createTriangleMesh( meshDescription, m_physics->getPhysicsInsertionCallback() );

    m_groundPlane = m_physics->createRigidStatic( PxTransform( PxVec3( -scale, 0.0f, -scale ) ) );
    m_scene->addActor( *m_groundPlane );

    PxTriangleMeshGeometry triangleMeshGeometry( groundTriangleMesh );

    PxShape* shape = m_groundPlane->createShape( triangleMeshGeometry, materialList, 4, PxTransform( PxIdentity ) );
    shape->setSimulationFilterData( filterData );
}

PxRigidDynamic* SimplePhysXManager::createDynamic( const PxTransform& t, const PxGeometry& geometry, PxU32 collisionGroup, const PxVec3& velocity )
{
    PxRigidDynamic* dynamic = m_physics->createRigidDynamic( t );
    dynamic->setAngularDamping( 0.5f );
    dynamic->setLinearVelocity( velocity );
    dynamic->setMass( 500.0f );

    PxFilterData filterData( collisionGroup, 0, 0, 0 );
    PxShape* shape = dynamic->createShape( geometry, *m_material, PxTransform( PxIdentity ) );
    shape->setSimulationFilterData( filterData );

	m_scene->addActor( *dynamic );

	return dynamic;
}

void SimplePhysXManager::createStack( const PxTransform& t, PxU32 size, PxReal halfExtent )
{
	PxShape* shape = m_physics->createShape( PxBoxGeometry( halfExtent, halfExtent, halfExtent ), *m_material );

    PxFilterData filterData( 1, 0, 0, 0 );
    shape->setSimulationFilterData( filterData );

	for( PxU32 i = 0; i < size; i++ )
	{
		for( PxU32 j = 0; j < size - i; j++ )
		{
			PxTransform localTm( PxVec3( PxReal( j * 2 ) - PxReal( size - i ), PxReal( i * 2 + 1 ), 0 ) * halfExtent );
			PxRigidDynamic* body = m_physics->createRigidDynamic( t.transform( localTm ) );
			body->setMass( 10.0f );
			body->attachShape( *shape );
			PxRigidBodyExt::updateMassAndInertia( *body, 10.0f );
			m_scene->addActor( *body );
		}
	}

	shape->release();
}