I’m working on a “programmable” particle system project. It’s programmable in the sense that you can specify how they are updated fairly freely, depending mostly on what commands are available. I’m not sure if I’ll get around to making a text-based parser for the particles, as right now I’m hard coding the streams. The stream for some ‘sparks’ might look something like:
- OP_Spawn (SpawnBufferId=0, Rate=100.f)
- OP_InitOBB (OBB=….)
- OP_InitSize (Size=3.f)
- OP_InitColor (Color={1.f,0.8f,0.8f.1.f})
- OP_FinalizeInit
- OP_ForceField (Force={0, 0, -100.f})
- OP_AttractPoint(Point={0,0,20}, Radius={some curve}, Magnitude={some curve})
- OP_SizeAge(Size={some curve}, TimeMultiplier=1.f)
- OP_ColorAge(Color={some curve}, TimeMultiplier=1.f)
- OP_KillAge(Age=1.f)
This would create particles at a rapid rate, the particles would begin to fall and then gravitate towards a nearby point, while slowly getting larger and more ‘red’ and translucent, before dying after a second of life.
Each particle system has a set of emitters, and each emitter has particle instance data. Particle emitter definitions contain a static command stream that specifies how the emitter instance is initialized and updated. Some commands only work with certain particle emitter types (applying a force to beam particles doesn’t necessarily make sense).
The system uses several fixed sized pool allocators as well as some static data to keep allocations constrained. The obvious downside is that the system allocates memory and can leave some of it unused. Right now I have it capped so it will never use more memory than I have set, but it could be easily changed so that the pool blocks are allocated as needed. Allocation works roughly like so:
- Given a ‘particle system definition’, allocate a particle system class (using a pool allocator).
- Depending on the number of emitters in the system, allocate some instance data from a set of graded pools (64bytes, 512bytes, 2048bytes, and 16k I think. The 64 & 512byte pools are meant for this purpose.). This used to contain an array of emitter ids (instead of an std::vector or similar).
- Allocate an emitter (using a pool allocator).
- Calculate size of the emitter’s instance data (sim data such as position and velocity, any data used by the command streams, and attach points), and allocate from the graded pools listed above.
- Run the init stream to initialize any command stream buffers.
The particle system manager allocates a fixed sized vertex and index buffer as well. These buffer objects are write-only and have a dynamic-draw usage in GL. When rendering, these are mapped using glMapBuffer() and written to using a class that wraps writing to memory buffers.
// somewhere way before...
vertBuffer.Map();
...
MemWriter writer;
size_t vbOffset;
std::tie(writer, vbOffset) = vertBuffer.Reserve(kBillboardVtxStride * numAlive);
if(writer) {
...
// write to buffer using writer.PutVec3() and writer.Put<>().
...
}
...
vertBuffer.Unmap();
A header block is added to a linked list in the frame data (again allocated from a special bit of memory – frame data is cleared every frame, but can be allocated as needed in the current update). The header block contains the offsets in the vertex buffer, texture to use, and type of particle to be rendered, to be used later by the frame renderer.
Some issues that I’ve run into that I don’t quite know how to resolve yet are:
- Alpha sorting different emitters. You can sort the particles within an emitter easily enough, but if you have several different emitters, it’s not necessarily clear how to sort them, and sorting them on a per-particle basis seems silly.
- Particle shadows – I’m having trouble thinking of a way to do this cheaply.
- Beam particles (like lightning) are composed of several rotated sheets. They look best when using an additive blend (glBlendFunc(GL_SRC_ALPHA, GL_ONE)), but the brightness depends on the number of sheets used. I suppose I could multiply each sheet by some weight so that the combined weight is correct, but I think part of the reason it ends up looking good is because the colors are saturating in places that would have artifacts. I’m not sure what the “right” way of doing this is.
- Collisions: my sprite & billboard particles have optional collisions, and currently I’m using line segment tests to test this. This is fine, visually, until particles become slow and are supposed to rest on a surface, then they leak through the geometry. I think I’d prefer to do some kind of moving sphere intersection, and perhaps mark particles that are resting, but for now all I have implemented is ray & line segment tests.
It is surprisingly hard to google for common ways to do particle system stuff – usually I just end up with how to do something in Unity or UDK, which isn’t particularly helpful. I’d like to know if what I’m doing is crazy or not. So, there may be better ways to do all of this! As a side project, it’s taking a bit longer than I wanted it to, and it’s nearly time to stop working on it despite it not being super polished. The extra time is partially due to me wanting to keep memory allocation constant and trying to share the same code path for all particle types. I went through a couple of rewrites before settling on something that seems to work. In any case, I’ll probably be posting whatever code I end up with in the next week or two.