Search Unity

How safe/performent is Get/Set'ing a 10k-100k+ Particle array per-frame?

Discussion in 'General Graphics' started by megan_l_fox, Feb 3, 2017.

  1. megan_l_fox

    megan_l_fox

    Joined:
    Jul 10, 2010
    Posts:
    35
    I'm routing all of our "splatter" particle systems in through a centralized particle system. I have manually coded fake-emitters that make Emit() calls on the central system, so even though it looks like a bunch of separate particle systems, it's really all just one.

    We're using it for blood: http://twitter.com/glassbottommeg/status/811435366309736448

    ... and for smoke: http://twitter.com/glassbottommeg/status/814239943736238081

    Neat stuff. It's all 3D mesh particles (just simple cubes). Reacts to whatever surface it contacts, etc. Anyways!

    Right now, both systems just have a standard particle lifetime. That's fine for smoke, which naturally disappears, but with the blood, it'd be nice if it just hung around until we'd actually emitted enough blood particles to actually need to get rid of some. Then I can set the max size of the blood array based on performance settings, and after an epic combat on a high-end machine potentially the whole dang arena can be coated in the stuff.

    Our basic plan looks a bit like this answer: http://answers.unity3d.com/question...st-particleif-limit-riched.html#answer-767933

    In short, per-frame, do a GetParticles, determine if the array is approaching max size, and if so, loop through the entire array, find the oldest particles, set their lifetimes such that they'll disappear, and then SetParticles the modified array in. Now the particle system has more gaps to work with, easy.

    Since we'd be creating a fixed-size array, GetParticles shouldn't allocate. So, THEORETICALLY, SetParticles shouldn't either, and even though we're looping through 10k-100k particles in a frame, that's exactly the sort of work that modern CPUs shrug at. So we should be fine.

    But.

    Those are some big numbers. Has anyone done anything like this before? Any Unity-side employees able to confirm that this all should be fine? The end result should still be substantially more performent than a billion independent particle systems, one would imagine?

    On the other end of things, we could break this up to one set of emitters per entity that can bleed/smoke. That would involve substantially more emitters (80+ active at a time, if not more, given that it's 4 per entity, and that's assuming a given entity can't bleed AND smoke, which would double it again). It solves the maximum-count problem, but the drawcalls go through the roof, and given how dispersed an enemy's blood splatter is likely to be across a large arena space, it isn't like the independent emitters would be any more likely to be visually culled when out of view.

    The structure of our game is moving between large combat arenas, so it's probable we'd clear all blood/smoke/etc as you loaded between arenas. Within any given arena, the sight lines are such that beyond simple view frustum culling, there's nothing to be gained from attempting to cull the blood/smoke/etc more aggressively. We'd also have to make the frustum VERY wide even then, to make sure emissions happening off-screen near us actually take place, given that the game's first person melee.
     
  2. megan_l_fox

    megan_l_fox

    Joined:
    Jul 10, 2010
    Posts:
    35
    Note that there IS another approach I could take. Maybe. I could build out a toroidally mapped array of blood particle meshes, and on particle death, instead move the blood across to be persisted long-term, until something else comes along that replaces it. I'd end up with a single (huge) buffer of cubes, sharing a material - one draw call.

    This has the advantage that the huge array of persistent particle meshes isn't as frequently touched, and only ever in a write-only fashion. I'm not entirely sure if the particles would be capable of the same - even though the persistent blood particles don't move, I don't know if the particle system is smart enough or capable enough to recognize that state and not just regenerate the whole particle-emitter mesh on a per-frame basis.
     
  3. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,281
    Ill try to write more in depth next week but here are some quick observations

    I would suggest using multiple emitters. With a single system you wont get much benefit from multi threading where with many they will all be split into separate jobs which can provide a significant performance improvement.
    Also I suggest you take a look at this blog article, the custom culling could work for you here https://blogs.unity3d.com/2016/12/20/unitytips-particlesystem-performance-culling/

    Getting and setting the particles is likely to be slow; whilst you won't have any allocations in the C# side every time you call get or set the whole array will be copied back and forth between c++ and c#, unfortunately we dont have native array support although its something being looked into in the future.

    Also fire up the profiler(if you have not already) and see how things are looking as you go.
     
  4. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    2,285
    As Karl says, multiple emitters will help with multi-threading. There will be some trade-off to make.. if you have 1,000 emitters, there will be some significant GameObject performance cost. And if you have 1 emitter, you are missing out on a lot of multi-threading potential. So there will be some sweet spot in the middle where you are getting the best of both. Possibly something like 64 emitters would be good.

    But it depends if it makes sense to split up your effects like that .. so, even if you just split smoke, fire, liquid, something, something, having 4-8 emitters would almost certainly get you most of the multi-threading benefit.

    Using multiple emitters can introduce transparency sorting problems, but it looks like your particles are opaque, so those will be fine. Just wanted to mention that though!

    I don't think we have any fast clever way of killing the oldest particles, based on a particle count budget, rather than a time-based age. So I think your script-based Get/SetParticles thing is the only solution. Though I expect it to be quite slow :( (Check out the Timeline Profiler to get a clearer idea - you can add your own profile markers to your script, to measure this precisely. See BeginSample/EndSample here: https://docs.unity3d.com/ScriptReference/Profiling.Profiler.html)
     
    karl_jones likes this.
  5. megan_l_fox

    megan_l_fox

    Joined:
    Jul 10, 2010
    Posts:
    35
    I ended up, yes, backing this up to maintaining a spawn pool of effects - so blood, or smoke, or whatever - and each of those effects, internally, has 4 emitters. One for the primary physics-influenced particle, which shoots or squirts out until it hits enviro, and then one subemitter for each sort of impact (walls/floors/ceilings for now). End result, killing a single enemy spawns 8 blood-squirts given the number of bits that come flying off, each of those containing 4 total emitters, so a single enemy death involves 32 emitters but only 8 parent objects (the rest being children that exist to hold the sub-emitters). As they're spawn-pooled, no hitch occurs, I just happen to have a lot of inactive game objects hanging around.

    I still cringe a bit at those numbers, but it puts a lot more performance tweaking in my hands. On a slower machine, I can swap the effect out entirely, which means anything from just emitting fewer particles per body part exploding outward, disabling the cool "the particles can impact walls" stuff, or even straight up skipping a lot of the emitters and only spawning half of them in the first place. Once they're down, I can then avoid the (probably really slow) sweeping of a single huge buffer for the oldest particles, since it's comparitively easy for me to just track however many emitters are in the scene, and despawn them as necessary. Or, more likely, just despawn them when the player leaves a room.

    So, upsides and downsides, basically. What I should probably do from here is, instead of having the emitters hang around long-term, add a secondary "decal" system that those particles can call into on destruction, which then creates a simple red/etc cube at the appropriate point. If I did THAT, all of these effects settle out within a few seconds normally, so the likelihood of more than 64 blood sprays or whatever existing at one time would be VERY minimal. Even as it stands, I'd be pretty surprised to see more than 128, given typical room sizes and enemy counts, and frankly, my first optimization stop would be more "do I REALLY need to spawn 8 of these per enemy death? Can't I get away with 4? Because, 8? Seriously?", heh.
     
    karl_jones and richardkettlewell like this.