Particle System

Position-based Dynamics (PBD) Particle Systems

A particle system, defined as an instance of the PxPBDParticleSystem, is capable of simulating dynamics that can be represented with a set of particles. The PhysX particle system solver uses position-based dynamics (PBD) for solving particle-to-particle dynamics, and can handle a wide range of dynamics including fluid and deformable objects. Unlike other particle solvers which mostly compute velocity first, PBD finds positions of particles that meet given constraints (such as volume preservation) and then calculates velocities from the optimized positions. This approach makes PBD stable and more robust to larger simulation time-steps.

Simulating particle systems requires a CUDA capable GPU and a CUDA context manager must be provided upon creation of the particle system. A PxCudaContextManager object can be created as follows:

PxCudaContextManagerDesc cudaContextManagerDesc;
PxCudaContextManager* cudaContextManager = PxCreateCudaContextManager(*gFoundation, cudaContextManagerDesc, PxGetProfilerCallback());

As a first step to make a PBD simulation in PhysX, an instance of PxPBDParticleSystem can be created as shown below:

PxPBDParticleSystem* particleSystem = gPhysics->createPBDParticleSystem(*cudaContextManager, 96);

The maximum number of neighbors is set to 96, which upper-bounds the number of particles considered in neighborhood-based particle interaction calculations (e.g. fluid density constraints).

User-visible particle system state is stored in particle buffers, which are independent of the particle system and scene and can be added/removed on demand. This section first introduces some general concepts before diving into the details of the position-based dynamics particle system.

Several snippets demonstrate the setup of PBD particle systems:

  • SnippetPBF demonstrates fluid simulation

  • SnippetPBFMultiMat demonstrates fluid simulation with multiple materials

  • SnippetPBDCloth demonstrates cloth simulation

  • SnippetPBDInflatable demonstrates inflatable simulation

Particle Buffers

Particle system simulation state is represented using one or multiple PxParticleBuffers that are associated with a particle system. A PxParticleBuffer object can be created as follows:

PxParticleBuffer* particleBuffer = gPhysics->createParticleBuffer(maxParticles, maxVolumes, *cudaContextManager);

This buffer has the capacity to hold maxParticles particles and maxVolumes particle volumes. Note that a CUDA context manager needs to be provided because creating a particle buffer will allocate GPU memory to hold the particle data for this buffer. The CUDA context manager provided here needs to be the same as the context manager provided to PxPhysics::createPBDParticleSystem().

Once a particle buffer is created, it can be initialized with initial positions, velocities and phases for simulation. Because the memory allocated during the creation of the particle buffer resides on the GPU, all of the responsibility of copying data to and from the GPU is left to the user. All pointers returned by functions of PxParticleBuffer are device pointers, i.e. point to GPU memory.

Given a list of initial positions for 1000 particles residing in host memory, the positions are initialized as follows:

PxVec4* bufferPos = particleBuffer->getPositionInvMasses();
cudaContext->memcpyHtoDAsync(bufferPos, positionsHost, 1000 * sizeof(PxVec4), 0);
particleBuffer->raiseFlags(PxParticleBufferFlag::eUPDATE_POSITION);

Whenever GPU state is changed by the user, the corresponding PxParticleBufferFlag has to be raised to notify the particle system of the changes. If a particle buffer is added to a particle system, all the PxParticleBufferFlags will be raised implicitly. Setting up intial velocities follows the same pattern, using PxParticleBuffer::getVelocities() and PxParticleBufferFlag::eUPDATE_VELOCITY.

After setting up initial state, the number of active particles in the buffer has to be set, and the buffer needs to be added to a particle system:

particleBuffer->setNbActiveParticles(1000);
particleSystem->addParticleBuffer(particleBuffer);

Note that the number of active particles needs to be <= PxParticleBuffer::getMaxParticles().

The results of each simulation step in a particle system simulation are ready after PxScene::fetchResultsParticleSystem() completes. At this point in time, the updated positions/velocities are ready in the GPU buffers returned by PxParticleBuffer::getPositionInvMasses()/ PxParticleBuffer::getVelocities() and it is the user’s responsibility to copy the data to the host if needed.

Because different types of effects that can be simulated with particle systems require different amounts of simulation state, several types of PxParticleBuffers exist:

Particle Phases

Every particle in a particle system is associated with a particle phase that defines collision properties, particle groups and material bindings. A particle phase is a created as follows:

const PxU32 particlePhase = particleSystem->createPhase(defaultMat, PxParticlePhaseFlags(PxParticlePhaseFlag::eParticlePhaseFluid | PxParticlePhaseFlag::eParticlePhaseSelfCollide));

This creation function takes a PxPBDMaterial and a set of PxParticlePhaseFlags. After creating one or multiple particle phases, phases are assigned to particles by inserting the PxU32 returned by the creation method in the buffer returned by PxParticleBuffer::getPhases() at the same index as the position/velocity for said particle.

The following properties are important when working with particle phases:

  • particles with the same phase always use the same material

  • particles with different phases will collide with each other

  • particles with the same phase will collide if PxParticlePhaseFlag::eParticlePhaseSelfCollide is set

  • particles with the same phase will generate fluid density constraints for overlapping neighbors if PxParticlePhaseFlag::eParticlePhaseFluid is set

  • particles with the same phase will not generate collisions with particles closer than the rest offset if eParticlePhaseSelfCollideFilter is set and valid rest positions are set.

Particle Volumes

PxParticleVolume objects are used to accelerate scene queries, for example to support picking. It is required to specify the maximum number of particle volumes when creating a particle buffer. A PxParticleBuffer can contain multiple volumes, however the particles contributing to the same volume need to be consecutive within this buffer.

Note that the PxParticleVolume only provides an up-to-date axis-aligned bounding box for the particles in the volume. Actual geometric queries need to be implemented by the application. The bounding box can be used to evaluate which underlying particle based geometry needs to be tested.

Particle System Configuration

Next the particle properties such as particle radius and mass need to specified. The following example code sets up fluid particles:

const PxReal particleSpacing = 0.2f;
const PxReal fluidDensity = 1000.f;
const PxReal restOffset = 0.5f * particleSpacing / 0.6f;
const PxReal solidRestOffset = restOffset;
const PxReal fluidRestOffset = restOffset * 0.6f;
const PxReal renderRadius = fluidRestOffset;
const PxReal particleMass = fluidDensity * 1.333f * 3.14159f * renderRadius * renderRadius * renderRadius;
particleSystem->setRestOffset(restOffset);
particleSystem->setContactOffset(restOffset + 0.01f);
particleSystem->setParticleContactOffset(PxMax(solidRestOffset + 0.01f, fluidRestOffset / 0.6f));
particleSystem->setSolidRestOffset(solidRestOffset);
particleSystem->setFluidRestOffset(fluidRestOffset);

The following list describes each parameter which was set above:

Once configured, the new particle system can be added to the scene:

gScene->addParticleSystem(*particleSystem);

Cloth Simulation

Particles in a particle system can not only behave like fluid or granular material. It is possible to insert additional constraints into the particle systems to connect a group of particles to act like a piece of cloth. To set up a cloth, a PxParticleClothBuffer is required to define the connections between the particles:

PxParticleClothBuffer* clothBuffer = gPhysics->createParticleClothBuffer(maxParticles, maxVolumes, nbCloths, nbTriangles, nbSprings, *cudaContextManager);

In addition to setting up the positions, velocities and phases as descibed in the section on particle buffers, the user is required to setup and preprocess the PxParticleSprings and PxParticleTriangles. This is done by creating two arrays for springs and triangles and defining the connections:

// Define springs and triangles
PxArray<PxParticleSpring> springs;
springs.reserve(numSprings);
PxArray<PxU32> triangles;
triangles.reserve(numTriangles * 3);

...

//Places a spring between particles at index i and j in the particle buffer
PxParticleSpring spring = { i, j, particleSpacing, stretchStiffness, springDamping, 0 };
springs.pushBack(spring);

//Adds a triangle between vertices at indices a, b and c in the particle buffer. Triangles are used to compute approximated aerodynamic forces for cloth
triangles.pushBack(a);
triangles.pushBack(b);
triangles.pushBack(c);

For every piece of cloth in the buffer, an additional set of parameters needs to be specified, using a PxParticleCloth struct. After setting up springs and triangles, a PxParticleClothDesc needs to be created:

PxParticleClothDesc clothDesc;
clothDesc.cloths = cloths.begin(); // list of PxParticleCloth
clothDesc.triangles = triangles.begin();
clothDesc.springs = springs.begin();
clothDesc.restPositions = restPositions.begin();
clothDesc.nbCloths = nbCloth;
clothDesc.nbSprings = nbSprings;
clothDesc.nbTriangles = nbTriangles;
clothDesc.nbParticles = nbParticles;

At this point, the cloth buffer is ready to be preprocessed. This is done as follows:

PxParticleClothPreProcessor* processor = PxCreateParticleClothPreProcessor(cudaContextManager);
PxPartitionedParticleCloth output;
processor->partitionSprings(clothDesc, output);
processor->release();

Cloth preprocessing will partition the spring constraints to maximize parallelism in the solver. Note that changing the spring or triangle lists will require a repartitioning. After preprocessing, all the data needs to be copied into the PxParticleClothBuffer and the buffer can be added to the particle system:

cudaContext->memcpyHtoDAsync(clothBuffer->getPositionsInvMasses(), hostPositions, nbParticles * sizeof(PxVec4), 0);
cudaContext->memcpyHtoDAsync(clothBuffer->getVelocities(), hostVelocities, nbParticles * sizeof(PxVec4), 0);
cudaContext->memcpyHtoDAsync(clothBuffer->getPhases(), hostPhases, nbParticles * sizeof(PxU32), 0);
cudaContext->memcpyHtoDAsync(clothBuffer->getVolumes(), hostVolumes, numVolumes * sizeof(PxParticleVolume), 0);
cudaContext->memcpyHtoDAsync(clothBuffer->getTriangles(), triangles.begin(), nbTriangles * sizeof(PxU32) * 3, 0);
cudaContext->memcpyHtoDAsync(clothBuffer->getRestPositions(), hostRestPositions, nbParticles * sizeof(PxVec4), 0);

clothBuffer->setNbActiveParticles(nbParticles);
clothBuffer->setNbParticleVolumes(nbVolumes);
clothBuffer->setNbTriangles(nbTriangles);

clothBuffer->setCloths(output); // result of the preprocessing

particleSystem->addParticleBuffer(clothBuffer);

Cloth Cooking

Setting up springs and triangles the right way can be tricky. PhysX provides a PxParticleClothCooker to help with setting up springs and triangles from a triangle mesh. The following example shows how to cook particle cloth constraints from a triangle mesh defined by a list of vertices and a list of triangle indices:

PxParticleClothCooker* cooker = PxCreateParticleClothCooker(vertices.size(), vertices.begin(), indices.size(), indices.begin(),
    PxParticleClothConstraint::eTYPE_HORIZONTAL_CONSTRAINT | PxParticleClothConstraint::eTYPE_VERTICAL_CONSTRAINT | PxParticleClothConstraint::eTYPE_DIAGONAL_CONSTRAINT);
cooker->cookConstraints();
cooker->calculateMeshVolume();

PxArray<PxU32> triangles;
PxArray<PxParticleSpring> springs;

PxU32 cookedTriangleIndicesCount = cooker->getTriangleIndicesCount();
PxU32* cookedTriangleIndices = cooker->getTriangleIndices();
for (PxU32 t = 0; t < cookedTriangleIndicesCount; t += 3)
{
    triangles.push_back(cookedTriangleIndices[t + 0]);
    triangles.push_back(cookedTriangleIndices[t + 1]);
    triangles.push_bacl(cookedTriangleIndices[t + 2]);
}

PxU32 constraintCount = cooker->getConstraintCount();
PxClothConstraint* constraintBuffer = cooker->getConstraints();
for (PxU32 i = 0; i < constraintCount; i++)
{
    PxParticleSpring spring;
    spring.ind0 = c.mParticleIndexA;
    spring.ind1 = c.mParticleIndexB;
    spring.stiffness = stiffness;
    spring.length =  c.mLength;
    spring.damping = damping;
    springs.pushBack(spring);
}

The resulting arrays triangles and springs can then be used as an input for cloth preprocessing.

Inflatables

A group of particles that is configured as cloth can be used to create an inflatable cloth if the triangle mesh used to create the inflatable is a non-overlapping, watertight mesh with a well defined volume. In contrast to a simple cloth, an inflatable body will simulate pressure inside the object. The pressure factor to control the behavior defines the volume the inflatable body tries to maintain during simulation. If the pressure is set to a value larger than one, the inflatable will show a behavior like a balloon. The setup is identical to cloth with the difference that the PxParticleCloth::pressure parameter needs to be nonzero.