Particle engine

Update: The particle engine along with other tech for Might is Right has been open-sourced under MIT as meep game engine.


Hey guys, I have implemented a particle engine for my game, and I thought I’d provide a slightly better look at it’s technical detail for those who might be interested.

Here’s a link to live demo.

Demo controls:

  • Left Mouse button - drag to change angle
  • Right Mouse button - drag to pan
  • Mouse wheel - zoom

Since I wrote the engine myself for my own game, it’s completely bespoke. I did draw quite a bit of inspiration from squarefeet’s SPE. Let me know what you think. Some of the features:

  • depth sorting
  • texture atlas. All particles use just 1 texture.
  • “soft particle” shader, particles fade out when penetrating surfaces (you can see this on the “earth” emitter where dust and clumps of dirt fall through the ground)
  • curve-based color and scale mutation over time. Supports arbitrary number of colors and scales at arbitrary points in time. Curves are compiles to a single lookup texture for all particles.
  • dynamic particle count. No hard limit on how many particles an emitter can spawn, you can add 1000 particles and then remove 900 underlying vertex pool will be resized dynamically as needed in an efficient manner.
  • precise dynamic bounding box, at every tick of the simulation you have a tight bounding box on the particle set for each emitter, the box is updated incrementally each frame.
  • frustum culling and sleeping. Particle emitters that are off-screen get suspended, when they become visible again - they are simply fast-forwarded to current moment of the simulation, it’s completely transparent for the user.
  • each emitter uses a single shader, no matter how many “layers” it has. In my engine, the “Emitter” is actually a group, consisting of 1 or more "Layer"s, each layer defines it’s own transform, sprite, curves etc.

All in all, it does most of what I wanted from a particle engine. And its performance is good enough that most of the times I don’t even see particle engine when I do profiling.

for those who might be interested, here’s a JSON serialized emitter for healing effect:

{
  "position": {
    "x": -1.247165750193119,
    "y": 0,
    "z": -2.1666973927028286
  },
  "scale": {
    "x": 1,
    "y": 1,
    "z": 1
  },
  "rotation": {
    "x": 0,
    "y": 0,
    "z": 0,
    "w": 1
  },
  "parameters": [
    {
      "name": "scale",
      "itemSize": 1,
      "defaultTrackValue": {
        "itemSize": 1,
        "data": [
          1
        ],
        "positions": [
          0
        ]
      }
    },
    {
      "name": "color",
      "itemSize": 4,
      "defaultTrackValue": {
        "itemSize": 4,
        "data": [
          1,
          1,
          1,
          1
        ],
        "positions": [
          0
        ]
      }
    }
  ],
  "blendingMode": 0,
  "layers": [
    {
      "imageURL": "data/textures/particle/UETools/Light_Beam_04.png",
      "particleLife": {
        "min": 0.2,
        "max": 0.2
      },
      "particleSize": {
        "min": 0.35,
        "max": 0.35
      },
      "particleRotation": {
        "min": -3,
        "max": 3
      },
      "particleRotationSpeed": {
        "min": 0,
        "max": 0
      },
      "emissionShape": 3,
      "emissionFrom": 1,
      "emissionRate": 180,
      "emissionImmediate": 0,
      "parameterTracks": [
        {
          "name": "color",
          "track": {
            "itemSize": 4,
            "data": [
              0.9882352941176471,
              0.9882352941176471,
              0.9882352941176471,
              0.875,
              0.9490196078431372,
              0.9882352941176471,
              0.807843137254902,
              0.8035714285714286,
              0.9529411764705882,
              0.9882352941176471,
              0.011764705882352941,
              0.8928571428571429,
              0.9490196078431372,
              0.9882352941176471,
              0.5568627450980392,
              0.831874810506877,
              0.9254901960784314,
              0.9882352941176471,
              0.07058823529411765,
              0.8630952380952381,
              0.8549019607843137,
              0.9882352941176471,
              0.01568627450980392,
              0
            ],
            "positions": [
              0,
              0.043478260869565216,
              0.42934782608695654,
              0.16579809410363308,
              0.6191185229303157,
              1
            ]
          }
        },
        {
          "name": "scale",
          "track": {
            "itemSize": 1,
            "data": [
              1.0000284682291665,
              1.0000373125771604,
              0.9483778345360352,
              0.8304531540639897,
              0.7711138941647975,
              0.6556243239902346
            ],
            "positions": [
              0,
              0.105,
              0.265,
              0.41999999999999993,
              0.6449999999999999,
              0.955
            ]
          }
        }
      ],
      "position": {
        "x": 0,
        "y": 0,
        "z": 0
      },
      "scale": {
        "x": 0.1,
        "y": 0.1,
        "z": 0.1
      },
      "particleVelocityDirection": {
        "direction": {
          "x": 0,
          "y": 1,
          "z": 0
        },
        "angle": 6.283185307179586
      },
      "particleSpeed": {
        "min": 0.15,
        "max": 0.25
      }
    },
    {
      "imageURL": "data/textures/particle/travnik/plus_1.png",
      "particleLife": {
        "min": 0.55,
        "max": 0.7
      },
      "particleSize": {
        "min": 0.05,
        "max": 0.1
      },
      "particleRotation": {
        "min": 0,
        "max": 0
      },
      "particleRotationSpeed": {
        "min": 0,
        "max": 0
      },
      "emissionShape": 0,
      "emissionFrom": 1,
      "emissionRate": 43,
      "emissionImmediate": 0,
      "parameterTracks": [
        {
          "name": "color",
          "track": {
            "itemSize": 4,
            "data": [
              0.45098039215686275,
              0.9882352941176471,
              0.15294117647058825,
              0.9821428571428571,
              0.45098039215686275,
              0.9882352941176471,
              0.15294117647058825,
              0
            ],
            "positions": [
              0.5706521739130435,
              0.9836956521739131
            ]
          }
        },
        {
          "name": "scale",
          "track": {
            "itemSize": 1,
            "data": [
              0.8566666666666667,
              1.1319203456790123
            ],
            "positions": [
              0.13,
              1
            ]
          }
        }
      ],
      "position": {
        "x": 0,
        "y": 0,
        "z": 0
      },
      "scale": {
        "x": 0.15,
        "y": 0.15,
        "z": 0.15
      },
      "particleVelocityDirection": {
        "direction": {
          "x": 0,
          "y": 1,
          "z": 0
        },
        "angle": 3
      },
      "particleSpeed": {
        "min": 0.03,
        "max": 0.1
      }
    },
    {
      "imageURL": "data/textures/particle/UETools/Star_24.png",
      "particleLife": {
        "min": 0.17,
        "max": 0.2
      },
      "particleSize": {
        "min": 0.26,
        "max": 0.35
      },
      "particleRotation": {
        "min": 0,
        "max": 0
      },
      "particleRotationSpeed": {
        "min": 0,
        "max": 0
      },
      "emissionShape": 0,
      "emissionFrom": 1,
      "emissionRate": 23,
      "emissionImmediate": 0,
      "parameterTracks": [
        {
          "name": "color",
          "track": {
            "itemSize": 4,
            "data": [
              0.9882352941176471,
              0.9882352941176471,
              0.9882352941176471,
              0.6726190476190477,
              0.9882352941176471,
              0.9882352941176471,
              0.9882352941176471,
              0.9880952380952381,
              0.9882352941176471,
              0.9882352941176471,
              0.9882352941176471,
              0.9620584572947379,
              0.9882352941176471,
              0.9882352941176471,
              0.9882352941176471,
              0.5535714285714286,
              0.9882352941176471,
              0.9882352941176471,
              0.9882352941176471,
              0
            ],
            "positions": [
              0,
              0.10326086956521738,
              0.6304347826086957,
              0.7381625967837998,
              1
            ]
          }
        },
        {
          "name": "scale",
          "track": {
            "itemSize": 1,
            "data": [
              0.9699871782407408,
              1.859251561547733,
              1.9704398895068476,
              1.9227992842084616,
              1.673623753579152,
              1
            ],
            "positions": [
              0.015434782608695685,
              0.07782608695652167,
              0.19,
              0.625,
              0.7849999999999999,
              1
            ]
          }
        }
      ],
      "position": {
        "x": 0,
        "y": 0,
        "z": 0
      },
      "scale": {
        "x": 0.3,
        "y": 0.3,
        "z": 0.3
      },
      "particleVelocityDirection": {
        "direction": {
          "x": 0,
          "y": 1,
          "z": 0
        },
        "angle": 0
      },
      "particleSpeed": {
        "min": 0,
        "max": 0
      }
    }
  ]
}

Numeric enums:


/**
 * @readonly
 * @enum {number}
 */
const EmissionShapeType = {
    Sphere: 0,
    Box: 1,
    Point: 3
};

/**
 * @readonly
 * @enum {number}
 */
const EmissionFromType = {
    Shell: 0,
    Volume: 1
};

Bit flags

/**
 *
 * @enum {number}
 */
export const ParticleEmitterFlag = {
    /**
     * Emitters that are asleep are not being simulated
     */
    Sleeping: 1,
    /**
     * Emitting emitters are allowed to produce new particles
     */
    Emitting: 2,
    /**
     * Internal flag, triggers sprite update in the emitter shader
     */
    SpritesNeedUpdate: 4,
    /**
     * When enabled emitter will start with pre-emitted particles
     */
    PreWarm: 8,

    /**
     * Emitter needs to be initialized before simulation can start proper
     */
    Initialized: 16,

    /**
     * Before emitter can be used it must be built first
     */
    Built: 32,

    /**
     * Position has changed since last update
     */
    PositionChanged: 64,

    /**
     * Whether particles should be sorted by depth or not
     */
    DepthSorting: 128,
};
20 Likes

That’s very very cool ! :neutral_face:
I’d really like to use your engine in my own games…

1 Like

Same here, can you share it?

I’m very flattered by your wish to use it guys. I will be releasing the whole engine at some point, but I don’t know when I will have time to do that.

3 Likes

Well if you want to throw it to some early adopters, please count me in.

I was doing a bit of debugging and thought I’d show dynamic bounding box calculation that’s going on inside the engine:


the emission model is fairly simple, there’s a direction cone that you can control the direction and angle of, and AABB is computed using cone properties as well as particle speed.

4 Likes

Created a hyper-realistic weather simulation demo :slight_smile:

http://server1.lazy-kitty.com/tests/particles_2019_07_11/

it does show off soft particles rather well, as well as particle sorting

I know your point here is more about the cloud, but have you considered using lines instead of points for the rain? I would think it may help get a fake motion blur.

1 Like

@gui thanks, that’s a good point. The engine doesn’t handle anything other than sprites, so lines would be impossible in the current state. I do have a trail renderer, but there’s not easy way to connect the two.

A lot of particle engines on the marker separate simulation and rendering parts, where rendering is a plugin that goes on top and feeds from particle data. This way you can choose how to render your particles, as sprites, trails, meshes, lines etc.

I think that’s a great approach, but I do not have this implemented in my engine, sprites are all you get :smiley:

1 Like

Sweet soft particles cloud! :star_struck:

1 Like

I spent yesterday re-writing my texture atlas code to work incrementally both in terms of paint and layout. The work is complete and I’m happy to say that it takes a fraction of a millisecond to paint a new sprite onto the atlas now, including the finding the place for it. The Atlas also has a small adjustable cache which allows sprites that are not currently in use to remain painted. This way thrashing of the atlas is reduced significantly. I found that in my game all the sprites basically sit in the cache or are in use, so after a certain point the canvas is not being updated anymore as everything is on it. Here’s a small video with the atlas debug visualization overlayed on top of the game screen.

you might notice that as I click on the ground and a particle effect is spawned - it requests a new sprite, which is immediately painted onto the canvas, same when the chest opens.

2 Likes

@Usnul that is absolutely AMAZING! If you’d like a beta tester for the weather simulation, count me in! :slight_smile:

1 Like

I made a small demo of particle sorting.

http://server1.lazy-kitty.com/tests/particles_sorting_2019_08_22/

there are 1500 particles in each “cloud”, sorting on my machine takes around 0.5ms per frame.

1 Like

@Usnul : Wow! Good job! Keep up the great work! :slight_smile: Do you mind open-sourcing it instead of using a webpack? I would like to continue to beta test this! :slight_smile:

Hey @Aerion, I plan to open-source the entire engine. Right now I’m busy with the other things, I will probably do it in a month once my other work has quieted down. I’m glad you like this, I will post an announcement on this forum when I do release it.

1 Like

A note on bounding boxes.

I spent a disproportionate amount of time inventing solutions to efficiently track bounding box of the particle cloud. I created a time series database with a super-effcient traversal mechanism that has amazingly low code and data foot print, I wrote an analytical algorithm to estimate volume of the emission cone, I felt pretty good about myself! :sunglasses:

Then I found that my estimation was still lacking, sometimes the box was too large, which caused culling issues, more times than I care to admit the system would produce slightly wrong results which I spent hours debugging. :beetle:

After all that - I reverted to just updating the bounding box the old fashioned way, inside the simulation loop. I have to loop through all the live particles anyway to perform the simulation update - so I figured - hey, let’s just try this. Lo and behold - there was no dreaded performance drop, in fact, simulation time has barely gone up. And now we have exact bounding box at every tick of the simulation.

I feel pretty silly :smiley:

Truth be told - I don’t think that I could have used this solution from the start, internals of the engine have changed so much over the weeks of efforts invested into it that it’s an obvious solution now.

Here you have it, a story and best wishes to you all in this new decade! :christmas_tree:

6 Likes

Little bit of an update. I wanted to add some particle effect into my game for the end-boss, so I figured I’d share :mermaid:
The boss is a mage and he has 6 different “stances”, each stance is tied to one of four elements:
Fire


Earth

Water

Air

Arcane

Darkness

each effect is 2-3 “layers” of sorted particles with different emission volumes/sprites/properties.

3 Likes

Looks awesome, Alex! :star_struck:
I’d be very curious about your soft particle implementation.

btw I can’t help but notice that Earth aura looks sorta “stinky” :eyes: :skunk:

1 Like

Haha, that’s fair. I struggle with representation of “Earth”. As it stands, every “earthy” effect kinda sucks, I guess I’ll keep trying :slight_smile:

I found that a lot of stuff when it comes to particles is about technique, you have some vision, but unless you can come up with a way to break it down into working pieces - it’s just not going to look right. For me, particles still remain one of the tougher areas to handle.

1 Like

@Unsul : Have you released the game engine yet? Would LOVE to use it in my own game! <3