Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Procedural Lightning Effect Startup Animation  (Read 2603 times)

0 Members and 1 Guest are viewing this topic.

JayhawkZombie

  • Jr. Member
  • **
  • Posts: 76
    • View Profile
Procedural Lightning Effect Startup Animation
« on: April 12, 2017, 03:47:47 am »
Hey, all. 

I feel this just sort of looks cool, so I'll show it off to see what you think.
Here our engine is spelling out the letters "SFENGINE" by outlining the letters in lightning.  4 lightning bolts will strike 4 corners of the letters every frame, and then multiple lightning bolts will travel across the letters.  The 4 corner bolts will strike every 750ms until every letter has been hit, at which time it'll just go to the main menu.

While our engine is still in a baby state, we did recently add procedural lightning generation and lightning "storms" (really just probabilistic lightning bolt generation). 

The lightning will look similar every run (as it should), but the actual bolts are generated at runtime every time this is run so the bolts will never be identical.

After that we added a class to handle time-sequenced events with custom callbacks, a pretty simple group of 2 classes. 

  class SequenceNode
  {
  public:
    SequenceNode() = default;
    SequenceNode(const SequenceNode &Copy);
    SequenceNode(std::initializer_list<SequenceNode> Seq);
    SequenceNode(double delta,
                 std::function<void(void)> start,
                 std::function<void(void)> end);

    ~SequenceNode() = default;

    void TickUpdate(const double &delta);
    bool IsDone() const;

    void Start();
    void End();

  protected:

    double m_Duration;
    double m_CurrentDuration = 0.0;
    bool   m_IsDone = false;
   
    std::function<void(void)> m_StartCallBack = []() {};
    std::function<void(void)> m_EndCallBack   = []() {};
  };

and, of course, the class that actually queues up the nodes 

  class TimedSequence
  {
  public:
    TimedSequence() = default;
    ~TimedSequence() = default;

    void AddSequence(
      double Duration,
      std::function<void(void)> StartCB,
      std::function<void(void)> EndCB
    );

    void AddSequences(
      std::function<void(void)> Start,
      std::function<void(void)> End,
      std::initializer_list<SequenceNode> Nodes
    );

    void Start();
    void TickUpdate(const double &delta);
  protected:

    bool m_IsTiming = false;
    std::queue<SequenceNode> m_Nodes;
    std::function<void(void)> m_StartCallBack = []() {};
    std::function<void(void)> m_EndCallBack   = []() {};
  };

Whenever a sequence is started, a callback is called (if one was registered for that sequence node), and one is also called whenever that sequence node's duration has expired.  Here we are just using the m_StartCallBack function pointer, and for the second we just pass an empty lambda.

Then we can do this to create the sequence of callbacks, as well as the durations between them: 

  m_LightningSequence.AddSequences(
    [this]() {this->LightningSequenceStarted(); }, //CB for the lightning sequence starting
    [this]() {this->LightningSequenceEnded(); },   //CB for the lightning sequence ending
    {
      { 750.0, [this]() { this->LightningSequenceCB(0,  1,  2,  3, "S");  }, []() {} }, // 4 bolts strike the 'S' character
      { 750.0, [this]() { this->LightningSequenceCB(4,  5,  6,  7, "SF");  }, []() {} }, // 4 botls strike the 'F' character
      { 750.0, [this]() { this->LightningSequenceCB(8,  9,  10, 11, "SFE"); }, []() {} }, // 4 botls strike the 'F' character
      { 750.0, [this]() { this->LightningSequenceCB(12, 13, 14, 15, "SFEN"); }, []() {} }, // 4 botls strike the 'F' character
      { 750.0, [this]() { this->LightningSequenceCB(16, 17, 18, 19, "SFENG"); }, []() {} }, // 4 botls strike the 'F' character
      { 750.0, [this]() { this->LightningSequenceCB(20, 21, 22, 23, "SFENGI"); }, []() {} }, // 4 botls strike the 'F' character
      { 750.0, [this]() { this->LightningSequenceCB(24, 25, 26, 27, "SFENGIN"); }, []() {} }, // 4 botls strike the 'F' character
      { 750.0, [this]() { this->LightningSequenceCB(28, 29, 30, 31, "SFENGINE"); }, []() {} }, // 4 botls strike the 'F' character
    }
  );

After that, it's just a matter of telling the bolts that travel across the letters to start off and walk, so the "LightningSequenceCB" function is only a few lines long

void Level1::LightningSequenceCB(int Bolt1, int Bolt2, int Bolt3, int Bolt4, std::string ltext)
{
  m_LightningText.setString(ltext);
  m_BoltTopLeft.Spark(    { 0.f,    0.f },   m_BoltStrikePositions[Bolt1]);
  m_BoltTopRight.Spark(   { 1700.f, 0.f },   m_BoltStrikePositions[Bolt2]);
  m_BoltBottomLeft.Spark( { 0.f,    900.f }, m_BoltStrikePositions[Bolt3]);
  m_BoltBottomRight.Spark({ 1700.f, 900.f }, m_BoltStrikePositions[Bolt4]);

  m_CrawlBolts[Bolt1].Spark(m_LightningTraces[Bolt1]);
  m_CrawlBolts[Bolt2].Spark(m_LightningTraces[Bolt2]);
  m_CrawlBolts[Bolt3].Spark(m_LightningTraces[Bolt3]);
  m_CrawlBolts[Bolt4].Spark(m_LightningTraces[Bolt4]);
}

m_BoltStrikePositions is where the 4 bolts coming from the corners strike, which is just 4 chosen corners of the letters. 
m_LightningTraces contains the sequence of positions for the corners of the letters that we want each bolt to travel along. 

To update the sequence, all we have to do is call
m_LightningSequence.TickUpdate(delta);

The bolts themselves keep track of their lifetime, so we can call render on them even if they're supposed to be dead and they just won't draw.

The result is this (the word "SFENGINE" doesn't get rendered, but we're fixing that):


 

anything