Particle 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,
};
10 Likes

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

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.