In this section, we describe how to set up NvCloth and guide you through some common features.
Some setup is required before we can start simulating. Here we give an overview of all the components needed. The sections below will provide more details about each component.
NvCloth uses user defined callbacks for memory allocation, error reporting, assert handling, and profile timings. The callbacks need to be passed to the library using nv::cloth::InitializeNvCloth() before the rest of the library can be used. The callbacks are straightforward to implement by providing classes with implementations for all the functions of physx::PxAllocatorCallback, physx::PxErrorCallback, physx::PxAssertHandler, and optionally physx::PxProfilerCallback. Note that the allocations returned by the PxAllocatorCallback need to be 16 byte aligned.
The library doesn’t need any deinitialization.
The Factory object lets you construct all the other components necessary for the simulation. There are different factories, one for each platform (e.g. CPU, CUDA, etc.) Components created with different platforms cannot be used together (e.g. a CPU cloth cannot be added to a GPU solver), but multiple factories (for different platforms) can be used at the same time. A factory is created as follows:
#include <NvCloth/Factory.h>
...
nv::cloth::Factory* factory = NvClothCreateFactoryCPU();
if(factory==nullptr)
{
//error
}
...
//At cleanup:
NvClothDestroyFactory(factory); //This works for all different factories.
Different functions instead of NvClothCreateFactoryCPU() can be used to create factories for a different platform. Some platforms may need additional arguments when creating the factory, like CUDA:
//// CUDA
#include <NvCloth/Factory.h>
#include <cuda.h>
...
CUcontext cudaContext;
int deviceCount = 0;
CUresult result = cuDeviceGetCount(&deviceCount);
ASSERT(CUDA_SUCCESS == result);
ASSERT(deviceCount >= 1);
result = cuCtxCreate(&cudaContext, 0, 0); //Pick first device
ASSERT(CUDA_SUCCESS == result);
nv::cloth::Factory* factory = NvClothCreateFactoryCUDA(cudaContext);
//We need to call cuCtxDestroy(cudaContext); after destroying the factory.
And DX11:
//// DX11
#include <NvCloth/Factory.h>
#include <d3d11.h>
...
//Setup DX11 context
ID3D11Device* DXDevice;
ID3D11DeviceContext* DXDeviceContext;
nv::cloth::DxContextManagerCallback* GraphicsContextManager;
D3D_FEATURE_LEVEL featureLevels[] = {D3D_FEATURE_LEVEL_11_0};
D3D_FEATURE_LEVEL featureLevelResult;
HRESULT result = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, featureLevels, 1, D3D11_SDK_VERSION, &DXDevice, &featureLevelResult, &DXDeviceContext);
ASSERT(result == S_OK);
ASSERT(featureLevelResult == D3D_FEATURE_LEVEL_11_0);
GraphicsContextManager = new DxContextManagerCallbackImpl(DXDevice);
ASSERT(GraphicsContextManager != nullptr);
nv::cloth::Factory* factory = NvClothCreateFactoryDX11(GraphicsContextManager);
//We need to release all DX objects after destroying the factory.
The data used for cloth simulation is divided into two objects. The fabric object contains all reusable data like the constraint lengths and connections. The cloth object contains all instance data like particle positions. This way multiple cloths can share the same fabric data, reducing memory usage.
Creating the fabric is the most complicated part of the setup. We have included helper functions as extension to simplify this step. We can fill the nv::cloth::ClothMeshDesc meshDesc struct and pass it to the NvClothCookFabricFromMesh function if we use the cooking extension:
#include <NvClothExt/ClothFabricCooker.h>
...
nv::cloth::ClothMeshDesc meshDesc;
//Fill meshDesc with data
meshDesc.setToDefault();
meshDesc.points.data = vertexArray;
meshDesc.points.stride = sizeof(vertexArray[0]);
meshDesc.points.count = vertexCount;
//etc. for quads, triangles and invMasses
physx::PxVec3 gravity(0.0f, -9.8f, 0.0f);
nv::cloth::Vector<int32_t>::Type phaseTypeInfo;
nv::cloth::Fabric* fabric = NvClothCookFabricFromMesh(factory, meshDesc, gravity, &phaseTypeInfo);
...
fabric->decRefCount();
phaseTypeInfo contains information used later for setting up the constraint phases of the cloth. We provide the gravity vector so the cooker can differentiate between horizontal and vertical constraints.
You have to release the fabric using the decRefCount() function. Cloths that depend on the fabric also keep a reference, so it is safe to decrease the refcount when the fabric is still in use. The fabric returned by NvClothCookFabricFromMesh (or by *Factory::createFabric) already has a reference count of 1. The fabric will destroy itself when its reference count reaches 0. All fabrics created by a factory need to be destroyed before that factory is destroyed (meaning indirectly that all cloths, which hold references to these fabrics, also need to be destroyed).
You can also manually provide all the cooked data using the Factory::createFabric function directly in case you have your own cooking code or if you do not cook at runtime.
We can now create a cloth instance from the fabric created in the previous step. The cloth instance does not need to begin with the same particle positions the fabric was cooked in, so we need to provide the initial position of the particles:
physx::PxVec4* particlePositions = ...; // The w component is the inverse mass of the particle
// and can be to 0 to lock the particle / make it static.
nv::cloth::Cloth* cloth = factory->createCloth(nv::cloth::Range<physx::PxVec4>(particlePositions, particlePositions + particleCount), *fabric);
// particlePositions can be freed here.
...
NV_CLOTH_DELETE(cloth);
Now we need to setup the phase configurations. Phase configurations control the order in which the constraints are solved (e.g. first horizontal then vertical etc.) and the constrain properties like stiffness:
nv::cloth::PhaseConfig* phases = new nv::cloth::PhaseConfig[fabric->getNumPhases()];
for(int i = 0; i < fabric->getNumPhases(); i++)
{
phases[i].mPhaseIndex = i; // Set index to the corresponding set (constraint group)
//Give phases different configs depending on type
switch(phaseTypeInfo[i])
{
case nv::cloth::ClothFabricPhaseType::eINVALID:
//ERROR
break;
case nv::cloth::ClothFabricPhaseType::eVERTICAL:
break;
case nv::cloth::ClothFabricPhaseType::eHORIZONTAL:
break;
case nv::cloth::ClothFabricPhaseType::eBENDING:
break;
case nv::cloth::ClothFabricPhaseType::eSHEARING:
break;
}
//For this example we give very phase the same config
phases[i].mStiffness = 1.0f;
phases[i].mStiffnessMultiplier = 1.0f;
phases[i].mCompressionLimit = 1.0f;
phases[i].mStretchLimit = 1.0f;
}
cloth->setPhaseConfig(nv::cloth::Range<nv::cloth::PhaseConfig>(phases, phases + fabric->getNumPhases()));
delete [] phases;
Note that there might be more phase of each type. Check if the gravity vector used for cooking is correct (and in the same space as the cooked mesh) when the vertical phases are missing.
There are more properties you can set on the cloth. They will be described below in the Usage section.
The solver represents something similar to a scene. It has a list of cloths that will be simulated together and it contains some simulation properties. Creating a solver is simple:
nv::cloth::Solver* solver = factory->createSolver();
...
NV_CLOTH_DELETE(solver);
We can add and remove cloths to/from the scene as follows:
solver->addCloth(cloth);
...
solver->removeCloth(cloth);
Advancing the simulation is broken up in a couple of function calls:
float deltaTime = 1.0f/60.0f;
solver->beginSimulation(deltaTime);
for(int i = 0; i < solver->getSimulationChunkCount(); i++)
{
solver->simulateChunk(i);
}
solver->endSimulation();
The simulateChunk() calls can be called concurrently from multiple threads to increase performance.
We need to retrieve the new positions of the cloth particles after the simulation step to display the results:
nv::cloth::MappedRange<physx::PxVec4> particles = mCloth->getCurrentParticles();
for(int i = 0; i<particles.size(); i++)
{
//do something with particles[i]
//the xyz components are the current positions
//the w component is the invMass.
}
//destructor of particles should be called before mCloth is destroyed.
Cloth instances have many properties that can influence their behavior. This section shows how to use some of the most common properties.
We can set the gravity acceleration vector in global space:
cloth->setGravity(physx::PxVec3(0.0f, -9.8f, 0.0f));
This vector doesn’t have to be the same as the one provided to the cooker.
Sometimes it is desirable to dampen the particle motion:
cloth->setDamping(0.5f); //0.0f is default
Note that the effect of the damping varies between local and global space simulation, as it dampens the local space motion only (so everything is damped if global space simulation is used as local and global space are equal in that case). To create effects like air drag or underwater motion it is better to use the wind / air drag properties instead.
We can change the accuracy (and stiffness) of the cloth by increasing the solver frequency. The solver frequency is used to calculate how many solver iterations are executed per frame (Solver::begin/endSimulation call):
cloth->setSolverFrequency(60.0f); //default is 300
There will be at least 1 iteration executed per frame, regardless of the value set. Sometimes lowering the solver frequency helps to avoid instabilities and oscillations, while a higher solver frequency can help to increase stiffness and improve other behavior like collision detection. It is common to set this value to a multiple of the fps target for the application.
Tethers are generated at the cooking stage and stored in the fabric. They improve behavior and reduce stretchiness by adding long range constraints between particles and fixed points.
We can change some properties of the tethers in the cloth instance. We can change the length of all tethers by setting the scale:
cloth->setTetherConstraintScale(1.2f); //Increase the length by 20%
We can make the tethers more spring like, or even disable them by changing the stiffness:
cloth->setTetherConstraintStiffness(0.0f); //Disable tethers
cloth->setTetherConstraintStiffness(0.5f); //Springy tethers
cloth->setTetherConstraintStiffness(1.0f); //Default value
The whole tether stage is skipped if the stiffness is set to 0 (increasing performance).
NvCloth provides a couple of different methods to add collision to the simulation. All collision primitives are defined in local space.
We can define up to 32 collision spheres per cloth:
physx::PxVec4 spheres[2] = {
// First 3 components of each vector is sphere center in local space and the 4th one is its radius
physx::PxVec4(0.0f, 0.0f, 0.0f, 1.0f),
physx::PxVec4(0.0f, 5.0f, 0.0f, 1.0f)
};
nv::cloth::Range<const physx::PxVec4> sphereRange(spheres, spheres + 2);
cloth->setSpheres(sphereRange, 0, cloth->getNumSpheres());
The last two arguments define which range of spheres defined previously is replaced (the parameters work the same across all Cloth::set’CollisionShape’ functions). This can be useful if only a couple of collision primitives changed since previous frame. Here we use the range [0, cloth->getNumSpheres()[ to ensure we replace all spheres that might have been defined previously. To insert the spheres at the beginning we could use [0, 0[ and to insert at the end we use [cloth->getNumSpheres(), cloth->getNumSpheres()[.
We can connect spheres to create capsules:
uint32_t capsuleIndices[2];
capsuleIndices[0] = 0;
capsuleIndices[1] = 1;
cloth->setCapsules(nv::cloth::Range<uint32_t>(capsuleIndices, capsuleIndices + 2), 0, cloth->getNumCapsules());
This connects sphere 0 and 1. Indices always need to be provided in pairs. Also note that the last two arguments specify indices of pairs. So cloth->getNumCapsules() will return 1 after the above snippet is executed.
We can also define up to 32 collision planes:
physx::PxVec4 planes[2] = {
physx::PxVec4(physx::PxVec3(0.5f, 0.4f, 0.0f).getNormalized(), 3.0f),
physx::PxVec4(physx::PxVec3(0.0f, 0.4f, 0.5f).getNormalized(), 3.0f)
};
nv::cloth::Range<const physx::PxVec4> planesR(planes, planes + 2);
cloth->setPlanes(planesR, 0, cloth->getNumPlanes());
This on its own will not make the cloth collide with anything as we first need to tell the solver that each plane is one convex shape:
uint32_t indices[2];
for(int i = 0; i < 2; i++)
indices[i] = 1 << i;
nv::cloth::Range<uint32_t> indiceR(indices, indices + 2);
mCloth->setConvexes(indiceR, 0, mCloth->getNumConvexes());
The value stored in the indices array is a bit mask telling the solver which planes are part of the convex shape. Plane i is indicated by bit 1<<i. We can easily construct convex shapes consisting of more planes by setting more bits (e.g. (1<<i) | (1<<j)).
We can also use arbitrary triangle meshes for collision:
physx::PxVec3* triangles = ...; //Get triangle data from somewhere
//We can't use indexed meshes/vertex sharing,
// each triangle is defined with its own 3 vertexes
nv::cloth::Range<const physx::PxVec3> triangleR(triangles, triangles + triangleCount * 3);
cloth->setTriangles(triangleR, 0, cloth->getNumTriangles());
Note that the range arguments passed to setTriangles are counting whole triangles (3 PxVec3 vertexes each).
We can set the friction coefficient used for collision between cloth and collision shapes like this:
cloth->setFriction(0.5);
Using local space simulation gives you more control over the simulation allowing you to improve the stability and response of the simulation. We separate the global/render coordinate system from the particle simulation coordinate system in order to use local space simulation. Graphics transforms should be adjusted to render the cloth in the correct location again.
We can use the following methods to let the simulation know that the cloth coordinate system has moved relative to the global coordinates:
cloth->setTranslation(physx::PxVec3(x,y,z));
cloth->setRotation(physx::PxQuat(qx,qy,qz,qw));
This will not change the particle positions, but will apply impulses so that the cloth reacts properly to the movement of the coordinate system. Air drag and lift also reacts accordingly.
We can use an inertia multiplier to control the strength of these impulses. Fast movements can cause problems like tunneling through collision shapes, self-intersection, instabilities and stretchy cloth. Use smaller inertia multipliers if these issues are noticeable. We can set these multipliers as follows:
//All values should be between 0.0 and 1.0
cloth->setLinearInertia(physx::PxVec3(x,y,z));
cloth->setAngularInertia(physx::PxVec3(ax,ay,az));
cloth->setCentrifugalInertia(physx::PxVec3(cx,cy,cz));
If we want to move the cloth without applying any forces (in case of a world origin shift for example) we can teleport:
cloth->teleport(physx::PxVec3(deltaX, deltaY, deltaZ));
Or we can reset the inertia effects after using the setTranslation/setPosition functions:
//Clear any pending inertia
cloth->clearInertia();
Cloth is simulated in a vacuum by default. We can set the drag and lift coefficients to make the simulation more natural:
cloth->setDragCoefficient(0.5f);
cloth->setLiftCoefficient(0.6f);
We can also add wind to the simulation:
cloth->setWindVelocity(physx::PxVec3(x,y,z));
It is a good idea to vary this parameter continuously to simulate gusts, making the simulation livelier.
Sometimes it is useful to constrain the movements of the cloth to some limited space for stability or artistic control. Motion constraints can be used to limit the movement of individual cloth particles to the volume of a sphere. The following snippets show the motion constraints are setup:
nv::cloth::Range<physx::PxVec4> motionConstraints = cloth->getMotionConstraints();
for(int i = 0; i < (int)motionConstraints.size(); i++)
{
motionConstraints[i] = physx::PxVec4(sphereCenter[i], sphereRadius[i]);
}
All motion constraints can be removed using cloth->clearMotionConstraints().
The radius of all the motion constraints can be changed by setting a scale and bias using cloth->setMotionConstraintScaleBias(scale, bias). The resulting radius of the constraints will be newRadius = scale*oldRadius + bias (clamped to avoid negative numbers).
The stiffness of the motion constraints can be set using cloth->setMotionConstraintStiffness(stiffness).
Cloth can be attached in two ways: * Updating positions of locked particles (with inverse mass set to zero) * Using motion constraints
Sometimes a combination of both is used.
The following snippet shows how to update particle positions directly:
{ //do this inside a scope, particle update will be triggered when MappedRange is destroyed
nv::cloth::MappedRange<physx::PxVec4> particles = cloth->getCurrentParticles();
for all attached particles i
{
particles[attachmentVertices[i]] = physx::PxVec4(attachmentPositions[i], mAttachmentVertexOriginalPositions[i].w);
}
}
Note that you can also set the position of non-locked particles, but that will probably result in undesirable behavior. Note that you can also change the mass by providing different w components. This can also be used to lock and unlock particles dynamically.
Distance/motion constraints can be used in a similar way (locking the particle with a sphere radius of zero). Motion constraints can also be used to make particles stay within a set distance from the skinned mesh of a character. Limiting the motion of particles can improve stability and avoid unwanted cloth configurations. Gradually decreasing the motion constraint radius along the mesh can be used to blend between skinned and physical meshes.
Using SI units makes it easy to use physical plausible values, but is not always desirable due to precision issues or compatibility with game engines. It is often the case that the positional units need to be scaled by some factor. It is easy to do this when following these rules.
When scaling the distance units by factor \(n\):
Fluid density (for wind simulation) should be scaled by \(n^{-3}\)
So \(n = 100\) if your data is stored in SI units (meters for linear distance) but you need the simulation to run in centimeters while keeping all other base units the same.
Here we describe some potential problems one may run into when simulating cloth.
Parts of the (or the whole) cloth can disappear when the simulation generates floating point NANs. In some cases the simulation recovers after a single frame, sometimes parts of the cloth, or even the whole cloth disappear without coming back. The most common cause for this behavior are large constraint errors.
Things too look out for are rapidly moving collision shapes, especially while air drag/lift or damping are applied.
Having air drag/lift enabled while large constraint errors are present (stretching the cloth) can generate very large impulses as drag/lift is dependent on triangle area. Damping is less sensitive to these problems, as it doesn’t depend on other properties of the cloth.