LiquidFun Programmer's Guide
|
About
Particles
Particle Systems
Particle Groups
Discrete Particles vs. Particle Groups
Stepping the World
Maximum Velocity
Creating and Destroying Particles
Creating and Destroying Particle Groups
Particle Behaviors
Particle Properties
Rendering with OpenGL
Sample Applications
The Particle module offers the ability to create and manipulate liquid or soft (deformable) bodies. It allows you to create (and destroy) particles with various behaviors and properties, and provides various methods for manipulating them. The module permits you to define particles discretely or as groups. It is designed to allow you to manipulate large numbers of particles efficiently.
A particle is round, and the minimal unit of matter in a particle system. By default, a particle behaves as a liquid. You can set behavioral flags, however, to assign different behaviors (explained in Particle Behaviors) to individual particles or groups of particles. You can also set other particle properties including position, velocity, and color.
The b2Particle.h
file contains the enumerated behavior values, as well as the variables specifying other particle properties. The corresponding enum is named b2ParticleFlag.
The "world" that particles inhabit is called a particle system. A particle system describes a wide variety of physical coefficients that help dictate how particles interact with the world around them. A few examples of these conditions are default particle radius, elasticity, and viscosity. For more detail, see the API Reference description of the b2ParticleSystemDef struct.
The following example creates a particle system:
const b2ParticleSystemDef particleSystemDef;
m_particleSystems[0] =
m_world->CreateParticleSystem(&particleSystemDef);`
You can also create more than one particle system: Thus, one "world's" particles may have a certain default radius, elasticity, etc., while the other "world" has different default values for these properties. The following sample shows the creation of multiple particle systems:
const b2ParticleSystemDef particleSystemDef;
for (int i = 0; i < NUM_PARTICLE_SYSTEMS; ++i) {
m_particleSystems[i] = m_world->CreateParticleSystem(&particleSystemDef);
}
In many, if not most, cases, it will not be necessary to adjust the default values or create multiple particle systems. You may find it useful in some cases, however.
For example, dividing particles into multiple systems can yield a performance gain by allowing you to simulate only the visible systems while putting all other systems in a "paused" state using b2ParticleSystem::SetPaused()
.
The "Multiple Systems" example in the Testbed provides an example of two particle systems influencing a rigid body while not interacting with each other.
Instead of creating particles individually, you can create a group of particles to manipulate en masse. Some of the particle-group properties that you can set are the same as those for discrete particles: behavior, position, linear velocity, and color. There are also properties specific to groups: rotational angle, rotational velocity, and strength.
The b2ParticleGroup.h
file contains the declarations for all of these variables, as well as the enum for particle-group behavior: b2ParticleGroupFlag
.
With one main exception, there is no functional difference between working with individual particles and groups of particles. The exception is rigid particles: Because of the internal algorithm used to make particles rigid, you must define them as a group.
Particle groups do offer several conveniences. First, they allow you to create and destroy large numbers of particles automatically. If you do not create a group, you must create all of the particles individually. Also, a group allows you to assign the same property, such as angle of rotation, to all of its particles at once.
The particle solver can iterate multiple times per step. Larger numbers of steps improve the stability and fidelity of the particle simulation. However, more steps also require more processor cycles.
The cycles cost is almost linear: double the number of particle iterations will almost double the cycles cost of b2ParticleSystem::Solve.
Use the particleIterations
parameter in b2World::Step
to set the number of iterations. The default value of particleIterations
is 1.
You should experiment with particleIterations
in your game to find the best balance of stability versus cycles. Try calling b2CalculateParticleIterations
or b2World::CalculateReasonableParticleIterations
to estimate a reasonable value. Note that these functions are, necessarily, a simplification, and should be used only as a starting point.
If your simulation seems overly bouncy or energetic, or if the particles in your simulation are passing through contacts, try increasing the number of particle iterations.
Note that, as particle iterations increases, the affect of pressure on highly-compressed particles also increases. That is, particles get more incompressible as you increase particle iterations.
The particle simulation enforces a maximum velocity on the particles, for stability and to prevent excessive interpenetration. The maximum velocity is,
particle diameter / (particle iterations * b2World::Step's dt)
To create individual particles, create a b2ParticleDef
-struct object. Next, specify the behavior and properties of the particle. Finally, call the method to create the particle.
The following example creates an individual particle.
b2ParticleDef pd;
pd.flags = b2_elasticParticle;
pd.color.Set(0, 0, 255, 255);
pd.position.Set(i, 0);
int tempIndex = m_particleSystem->CreateParticle(pd);
Particle lists are self-compacting. Therefore, the index returned by CreateParticle is only valid until a lower-indexed particle, or a group referencing a lower-indexed particle, is deleted.
To destroy an individual particle, invoke the function
void DestroyParticle(int32 index);
The following example destroys the particle created above.
m_particleSystem->DestroyParticle(tempIndex);
In addition to manual destruction of particles as described above, particles can also expire and be destroyed due to age.
The following example tells the system to track particle ages for the purpose of destroying them.
m_particleSystem->SetParticleDestructionByAge(true);
A particle can die one of two "age-related" deaths. First, you can set a lifetime for a particle–a period of time after which it expires. The following example does this:
m_particleSystem->SetParticleLifetime(
index, Random() *
(k_particleLifetimeMax - k_particleLifetimeMin) +
k_particleLifetimeMin);
where index
specifies the number of the particle whose lifetime is being assigned, and the Random()
function generates a random value for that lifetime.
You do not need to set a specific lifetime for a particle for it to have an age-related death. If you set a maximum number of particles that can exist in a particle system, and you have have told the system to track particle ages, the system clamps particle count by culling "excess" particles. Particle culling takes place in age order, with the oldest ones destroyed first.
The following example sets a maximum particle count for a particle system.
m_particleSystem->SetMaxParticleCount(k_maxParticleCount);
The Faucet example in the Testbed provides an example of both types of lifetime-driven particle destruction.
Particles may get stuck and become obstructions that need to be destroyed or relocated. A particle is identified as possibly stuck if it remains in contact with two or more surfaces for a user-specified number (threshold) of particle iterations. Once "candidates" are identified, you can implement your own logic to decide whether they are actually stuck, and how to deal with them.
The ability to implement your own logic gives you flexibility in deciding when you want to consider a particle stuck. For instance, a ball may be traveling down a chute, making contact with walls on multiple sides. This state satisfies the "possibly stuck" condition described in the previous paragraph. But you could implement logic judging the ball not stuck as long as it keeps traveling down the chute.
On the other hand, you could also decide that not only an immobile particle, but even a mobile one trapped in a certain spatial range, is stuck. The system relies on you to judge the candidates.
The following example shows one possible implementation for such a case.
// This code example of app logic deciding whether or not to eliminate stuck // particles shows a user who set up a global array of sensor fixtures // covering areas they know to be "problematic" for stuck particles in // their geometry, and then at each step testing any stuck particles against // those sensors, eliminating any stuck particles that lie inside a known // problem region. void DestroyStuckParticlesInSensors( const b2Fixture * const *sensors, int32 num) { const int32 stuck = gParticleSystem->GetStuckCandidateCount(); if (stuck > 0) { const int32 *candidates = gParticleSystem->GetStuckCandidates(); const b2Vec2 *positions = gParticleSystem->GetPositionBuffer(); for (int32 i = 0; i < stuck; ++i) { const int32 particle = candidates[i]; const b2Vec2 &position = positions[particle]; for (int32 j = 0; j < num; ++j) { if(sensors[j]->TestPoint(position)) { gParticleSystem->DestroyParticle(particle); } } } } } // particles in multiple contacts for 5 or more iterations are // candidates gParticleSystem->SetStuckThreshold(5);
// step the world (assuming the timestep, velocity iterations, // and position iterations have been set globally). gWorld->Step(gTimeStep, gVelocityIterations, gPositionIterations);
// Perform the above check for stuck particles against sensors // in this global array. DestroyStuckParticlesInSensors(gProblemAreaSensors, gNumSensors);
A particle group begins life in a shaped container. You must therefore start a particle group definition by specifying a shape. Next, create a b2ParticleGroupDef-struct object. Then, specify the behavior and properties of the particles themselves. Finally, call the method to create a particle group.
The following example creates five differently colored, box-shaped groups of particles.
b2ParticleGroupDef pd;
b2PolygonShape shape;
shape.SetAsBox(10, 5);
pd.shape =
pd.flags = b2_elasticParticle;
pd.angle = -0.5f;
pd.angularVelocity = 2.0f;
for (int32 i = 0; i < 5; i++)
{
pd.position.Set(10 + 20 * i, 40);
pd.color.Set(i * 255 / 5, 255 - i * 255 / 5, 128, 255);
m_particleSystem->CreateParticleGroup(pd);
}
To destroy a particles in a group, invoke the function
DestroyParticles(bool callDestructionListener);
Groups are automatically destroyed when they contain no particles if the b2_particleGroupCanBeEmpty
is not set in the group's flags.
The following example destroys all particle groups in the particle system.
b2ParticleGroup* group = m_particleSystem->GetParticleGroupList(); while (group) { m_particleSystem->SetGroupFlags( m_particleSystem->GetGroupFlags() & ~b2_particleGroupCanBeEmpty); group->DestroyParticles(false); // The destruction of particle groups are deferred to the next call of // Step() so it's safe to reference the group here. group = group->GetNext(); }
The next several sections provide more information on how to define particle behaviors and properties.
Particle behaviors are defined either for entire groups of, or individual, particles.
For a group of particles, use the b2ParticleGroupFlag
enum, which provides two types of particle groups:
A solid particle group prevents other bodies from lodging inside of it. Should anything penetrate it, the solid particle group pushes the offending body back out to its surface.
A solid particle group also possesses an especially strong repulsive force. It is useful, for example, in a case where:
Use the b2_SolidParticleGroup
flag of the b2ParticleGroupFlag
enum to specify a solid particle group. For example:
pd.groupFlags = b2_solidParticleGroup;
Rigid particle groups are ones whose shape does not change, even when they collide with other bodies. Working with rigid particle groups confers a few advantages over simply working with rigid bodies: With a rigid particle group, you can:
Use the b2_rigidParticleGroup
flag of the b2ParticleGroupFlag
enum to specify a rigid particle group. For example:
pd.groupFlags = b2_rigidParticleGroup;
For individual particles, use the b2ParticleFlag enum. The b2ParticleFlag enum provides the flags described in the following sections. Note that different particle behaviors may exact different performance costs.
Elastic particles deform and may also bounce when they collide with rigid bodies.
Set particle behavior as elastic using the statement
pd.flags = b2_elasticParticle;
The green circle and the blue box in the "Elastic Particles" demo of the Testbed application comprise elastic particles.
Color-mixing particles take on some of the color of other particles with which they collide. If only one of the two colliding particles is a color-mixing one, the other particle retains its pre-collision color.
The following example shows how color mixture is calculated. It shows the collision of two color-mixing particles: one red ("R") and one green ("G").
First, the system calculates deltaColor, which is the value by which each color will change.
deltaColor = colorMixingStrength * (B's color - A's color).
= 0.5 * ((0,255,0,255) - (255,0,0,255))
= 0.5 * (-255,255,0,0)
= (-127.5,127.5,0,0)
Then, it applies the delta to each particle
R's color += deltaColor
G's color -= deltaColor
As a result, both particles are now yellow:
A's color = (127.5,127.5,0,255)
B's color = (127.5,127.5,0,255)
Note that when one of the operations in step 2 results in a negative number, the system uses the absolute value of that number. When it results in a value over 255, it rolls over from zero.
Set particle behavior as color-mixing using the statement
pd.flags = b2_colorMixingParticle;
The "Surface Tension" demo of the Testbed application uses color-mixing particles.
Powder particles produce a scattering effect such as you might see with sand or dust.
Set particle behavior as powder using the statement
pd.flags = b2_powderParticle;
The "Sparky" demo of the Testbed application uses powder particles.
Spring particles produce the effect of being attached to one another, as by a spring. Particles are "connected" in pairs. Each particle is connected to the one that was closest to it at time of creation. Once paired, particles do not change "partners." The farther an external force pulls them from one another, the greater the power with which they collide when that external force is removed. No matter how far particles get from one another, the connection between them does not "snap."
Set spring behavior using the statement
pd.flags = b2_springParticle;
The red circle in the "Elastic Particles" demo of the Testbed application comprises spring particles.
Tensile particles are used to produce the effect of surface tension, or the taut curvature on the surface of a body of liquid. They might be used, for example, to create the surface tension you would see on a drop of water.
Once the tension is broken, the particles bounce as if they were elastic, but also continue to attract each other. As a result, particles tend to form clusters as they bounce.
Set tensile behavior using the statement
pd.flags = b2_tensileParticle;
The "Surface Tension" demo of the Testbed application uses tensile particles.
Viscous particles exhibit clinginess or stickiness, like oil.
Set viscous behavior using the statement
pd.flags = b2_viscousParticle;
The "Liquid Timer" demo of the Testbed application uses viscous particles.
Particles are subject to compression when pressure acts upon them. For example, when particles pour into a container, the ones at the bottom of the container are "crushed" under the weight of those above them and packed more tightly together than the ones at the top of the pile.
The static-pressure particle eliminates this differential; the same amount of pressure acts upon each particle in the group.
The following example sets static-pressure behavior.
pd.flags = b2_staticPressureParticle;
Wall particles are static. They are permanently stationary, even if something collides with them.
Set wall behavior using the statement
pd.flags = b2_wallParticle;
Solid or rigid particle groups are not inherently tunneling-proof. Particles traveling at high enough velocities may penetrate them. Barrier particles, used in conjunction with other particle types, provide particle groups with protection against tunneling. This functionality is useful when, for example, you want to ensure that liquid particles will not leak out of a container formed of wall particles.
Barrier particles only prevent penetration of the particle groups they inhabit. They cannot prevent particles from getting between groups of particles, even if the groups' positions make them look as if they are contiguous.
You can use barrier particles with elastic, spring, or wall particles.
The following example creates an impermeable group of wall particles:
pd.flags = b2_wallParticle | b_barrierParticle;
pd.groupFlags = b2_solidParticleGroup;
Zombie particles are useful when you want efficiently to destroy multiple particles in a single step. All of the particles that you designate as zombies are destroyed at the same time, in a single iteration of the solver. Destroying particles in a batch, after designating them as zombies, yields better performance than destroying them one by one: Whereas destroying particles one-by-one takes (number of parti`cles) * (time per particle) to complete, destroying them all in a batch takes the same time as it would to destroy a single particle.
In the following example, every other particle in a group is designated as a zombie, and will be destroyed in the next step of the solver. (For more information on the LiquidFun solver, see Chapter 1. Introduction.)
b2ParticleGroup*group= m_particleSystem->CreateParticleGroup(pd);
for (int32 i=0;i<group->GetParticleCount();i+=2)
{
group->GetFlagsBuffer()[i] |=
b2_zombieParticle;
}
Note that you can assign multiple behaviors to a group or particle. Use the | ("bitwise OR") operator to chain behavior flags. For example, for a group:
pd.groupFlags = b2_solidParticleGroup | b2_rigidParticleGroup;
And for particles:
pd.flags = b2_elasticParticle | b2_viscousParticle;
To define a group combining a specific group behavior with a specific particle behavior, use two statements. For example:
pd.flags = b2_elasticParticle;
pd.groupFlags = b2_solidParticleGroup;
Set particle or particle-group color using the statement
pd.color.Set(r, g, b, a);
whose parameters set red, green, blue, and opacity, respectively. Each parameter takes a value of 0-255.
There are two points to keep in mind when using small particles. First, in the case of particle groups, particle size can affect performance. This is because particle size is inversely proportional to the number of particles generated to constitute a group. Having a large number of particles, in turn, can diminish performance.
Set particle size using the statement
m_particleSystem->SetRadius(r);
where r
is a float32 value greater than 0.0f. Default particle radius is 1.0f.
Small particles may also behave unpredictably (i.e., break conservation of momentum) in scenarios such as explosions. Slowing these particles down by reducing gravity scale can stabilize their behavior.
Set gravity scale using the statement
m_particleSystem->SetGravityScale(g);
where g
is a float32
value greater than 0.0f. Default gravity scale is 1.0f.
It is worth noting that adjusting the number of particle iterations per solver step can also affect the effect of gravity on particles. Larger iteration sizes confer greater resistance to gravity. A common reason for increasing the number of particle-iterations is to prevent volume loss (i.e. compression) due to gravity.
Set particle or particle-group position using the statement
pd.position.Set(x, y);
where x
and y
are the world-coordinates of the translation of the particle group.
For discrete particles, set velocity using the statement
pd.velocity.Set(x,y);
where x
is velocity along the x-axis, and y
is velocity along the y-axis.
For particle groups, set velocity using the statements
pd.linearVelocity.Set(x,y);
pd.angularVelocity = aV;
where x
is the group's velocity along the x-axis, y
is velocity along the y-axis, and aV
is the group's angular (i.e., rotational) velocity (expressed as radians per second).
This property applies only to rigid particle groups. It indicates the angle at which a group is tilted. Set angle with the statement
pd.angle =checkout a;
where a
is the angle of tilt, expressed in radians. Left unspecified, the value defaults to 0.
Strength describes the cohesion of a group of particles. Set strength with the statement
pd.strength = s;
where s
is a float32 value between 0.0 (least cohesive) and 1.0 (most cohesive). The default value is 1.0.
The Particle module provides particularly efficient rendering via OpenGL.
Each type of particle property lives in a contiguous memory buffer. For example, all particles' position data live next door to one another, all color data live next door to one another, and so forth. Table 1 provides a visual representation of this storage.
**_Table 1. Memory Map of Particle Buffers_**
Particle 1 | Particle 2 | Particle 3 | |
Position | x1,y1 | x2,y2 | x3,y3 |
Address | 0x00001000 | 0x00001008 | 0x00001010 |
Color | r1,g1,b1,a1 | r2,g2,b2,a2 | r3,g3,b3,a3 |
Address | 0x00002000 | 0x00002004 | 0x00002008 |
OpenGL can use these buffers directly in rendering.
In this example, OpenGL 1.1 would use glVertexPointer and glColorPointer to get the values from memory. OpenGL 2.0 would use glVertexAttribPointer.
OpenGL can be used to render either individual particles or particle groups.
Among the samples included in the LiquidFun distribution are two applications that offer a quick look into the capabilities of the library.
Testbed includes a large number of demos that provide examples of different types of particle behavior. While some of the demos are "look only," others are interactive, allowing you to use your mouse or touchscreen to affect the behavior on screen.
Experimenting with each of the demos, and comparing their behavior against the source code, can provide useful insights into how different particles behave under various conditions. Testbed builds and runs on Android, MacOSX, Linux, and Windows.
EyeCandy is an Android-only application and is twofold in purpose: It provides a simple Android example of how to use LiquidFun; and, it seeks to inspire developers with its demonstration of the powerful liquid shaders it brings to mobile hardware.
When running the program, you can slosh the fluid around by changing the orientation of the Android device. You can also toggle bewteen shaders by tapping the screen.
This content is licensed under Creative Commons Attribution 4.0. For details and restrictions, please see the Content License.