www.codeworx.org/opengl-tutorials/codeworx.org - Partikelsysteme mit OpenGL - Teil 3 - Blitze

Partikelsysteme mit OpenGL - Teil 3 - Blitze


In diesem dritten Partikel-Tutorial möchte ich nun einmal zeigen wie leicht man das bisher programmierte Partikelsystem nutzen kann um optische Effekte zu erzielen, ohne das man den Code des Partikelsystems selbst groß ändern müsste.

An der Klasse CParticle nehmen wir nur ein klitzekleine Änderung vor, die allerdings keinen Einfluss auf das Verhalten der Klasse hat:

class CParticle
{
   public:
   CParticle(CPartEmitter* pEmitter);
   virtual ~CParticle();
   virtual void Update();// *NEU* virtual
   void Render();
   public:
   // der Reste dieser Klasse bleibt unverändert
};

Neu ist einzig und allein das virtual vor der Deklaration der Methode Update(). Dies erlaubt uns Unterklassen von CParticle zu bilden, die Update() überschreiben und ermöglicht folgenden Seiteneffekt:

class CExample : public CParticle 
{
   public:
   CExample(CPartEmitter* pEmitter);
   virtual ~CExample();
   void Update();
};

inline void CExample::Update() { CParticle::Update(); Ausgabe("wird aufgerufen");}
CParticle* pPar = new CExample(g_pEmitter); 
pPar->Update();         
// -> Code in CParticle::Update wird ausgeführt und ebenfalls der 
// in CExample::Update() 

Wir können also CParticle-Objekte als Container für Unterklassen benutzen und spezielle Effekte der Unterklassen in der Update() Funktion realisieren.

Dies wollen wir nun tun, und versuchen uns an einem Blitz-Effekt. Nach dann obigen Überlegungen sieht unsere Blitzklasse CBolt dann folgendermaßen aus:

class CBolt : public CParticle 
{
   public:
   CBolt(CPartEmitter* pEmitter);
   virtual ~CBolt();
   void Update();
   public:
   float* m_pColChange;
};

Der Konstruktor ist gemäß einem CParticle-Konstruktor aufgebaut, mit dem Emitter als Parameter. Wir überschreiben Update() und fügen das Attribut m_pColChange ein, einen Zeiger auf einen float-Wert.


Der Effekt soll folgendermaßen funktionieren:
-> Ein Partikel mit einer Blitz-Textur soll sehr schnell erscheinen,
-> während er an Intensität zunimmt soll die Beleuchtung der gerenderten Szene entsprechend zunehmen.
-> Je mehr Blitze zum gleichen Zeitpunkt sichtbar sind, desto heller soll die Beleuchtung werden, das heißt die Helligkeitszunahme pro Blitz soll Additiv sein.
-> Ein Partikel soll ebenfalls sehr schnell wieder verblassen, dabei soll die Beleuchtung entsprechend der vorhergehenden Zunahme wieder abnehmen (ebenfalls Additiv).

So kompliziert das jetzt vielleicht alles klingt, so einfach ist die Realisierung: 3 Zeilen Code in der Update() Funktion von CBolt!

Hier die komplette Implementierung der CBolt-Klasse:

CBolt::CBolt(CPartEmitter* pEmitter) : CParticle(pEmitter)
{
   m_pColChange = NULL; 
}
CBolt::~CBolt()
{}
void CBolt::Update()
{
   CParticle::Update(); // Aufruf der Update() Funktion der Oberklasse
if(m_pColChange) // falls der Zeiger gültig ist (*m_pColChange) += m_clrColor[3]; // erhöhe die Helligkeit um den Alphawert }

Den Parameter des Konstruktors geben wir weiter an den Konstruktor von CParticle und setzen unseren Zeiger m_pColChange auf NULL.

In Update() rufen wir zunächst die Update() Funktion der Oberklasse auf und erhöhen dann den float-Wert von m_pColChange um den Alphawert des Partikels.
m_pColChange zeigt auf eine float-Variable im Hauptprogramm, die vor jedem Update der Szene auf 0 gesetzt wird und zur Helligkeit der Szene addiert wird.

Wird also m_pColChange durch die Blitze erhöht, erhöht sich automatisch die Helligkeit der gesamten Szene, da wir immer etwas hinzuaddieren wird m_pColChange größer je mehr Blitze aktiv sind.

Im Hauptprogramm haben wir also nun die zwei Globalen Variablen g_fColVal und g_fColChange, die die Beleuchtung der Szene speichern, g_fColVal ist die Standardbeleuchtung und g_fColChange der additive Faktor, auf den alle CBolt-Objekte zeigen.

Unter anderem haben wir 2 verschiedene Emitter, einfach um zwei verschiedene Blitz-Texturen leicht handhaben zu können.
Diese Emitter füllen wir nun wie folgt mit Partikeln:

void BuildParticles(bool b)
{
   // falls b true, wird neuer Partikel zu Emitter1 hinzugefügt,
   // ansonsten zu Emitter2
   if(b)
   {
      CBolt* p = new CBolt(g_pEmitter); // *NEU* 
      VECTOR vPos = {2*((rand() % 400)-200), 50, -550 -(2*(rand() % 500))};
      VECTOR vVel= {0,0,0};
      float clrCol[4] = { 1,1,1,1};
      float clrChangeCol[4] = {0,0,0,0};
      float fFade = 4.0f; 
      float fLife = 0.2f;
      float fSize = 220.0f + ( (rand() % 10) -5)*0.1;
      float fChangeSize = 0;
      p->m_fFadeInSpeed = 10.0f;
      p->Set(vPos,vVel,clrCol,clrChangeCol,fSize,fChangeSize,fLife,fFade);
      p->m_pColChange = &g_fColChange; // *NEU* 
      g_pEmitter->Add(p);
   }

   else
   {
      CBolt* p = new CBolt(g_pEmitter2);// *NEU* 
      VECTOR vPos = {2*((rand() % 200)-100),50,-550-((rand() % 200)*2)};
      VECTOR vVel= {0,0,0};
      float clrCol[4] = { 1,1,1,1};
      float clrChangeCol[4] = {0,0,0,0};
      float fFade = 4.0f;//10.0f; 
      float fLife = 0.2f;
      float fSize = 220.0f + ( (rand() % 10) -5)*0.1;
      float fChangeSize = 0;
      p->m_fFadeInSpeed = 10.0f;
      p->Set(vPos,vVel,clrCol,clrChangeCol,fSize,fChangeSize,fLife,fFade);
      p->m_pColChange = &g_fColChange;// *NEU* 
      g_pEmitter2->Add(p);
   }
}

Bevor wir irgendwelche Objekte oder den Hintergrund zeichnen, fügen wir, da wir ja zur Einfachheit keine richtigen Lichter eingefügt haben, folgende Zeile Code ein:

   // *NEU* 
   glColor4f(g_fColVal+g_fColChange,
             g_fColVal+g_fColChange,
             g_fColVal+g_fColChange,
             1.0f);

Wir simulieren also die Helligkeit der Szene durch die Helligkeit der Farbe mit der die Objekte gezeichnet werden.

An der Funktion RenderScene() ist eigentlich nichts besonderes dran, wir dürfen nur nicht vergessen g_fColChange jedes mal, bevor wir die Emitter updaten, auf 0 zu setzen:

void RenderScene() 
{
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
   glLoadIdentity(); 
   glTranslatef(g_vPos[0],g_vPos[1],g_vPos[2]);
   DrawBackground(); // Hintergrund zeichnen
   g_fColChange = 0.0f; // *NEU* Helligkeitsänderung auf 0 setzen
   if(g_Timer.GetCurrentTime()-g_fLastUpdate>1.6f+(((rand()%6)-3)*0.1))
   {
      BuildParticles(true); // Neue Partikel für Emitter 1
      g_fLastUpdate = g_Timer.GetCurrentTime();
   }
   if(g_Timer.GetCurrentTime()-g_fLastUpdate2>2.6f+(((rand()%6)-3)*0.1))
   {
      BuildParticles(false);// Neue Partikel für Emitter 2
      g_fLastUpdate2 = g_Timer.GetCurrentTime();
   }
   // Emitter 1 hat Textur 0
   glBindTexture(GL_TEXTURE_2D, g_Texture[0]);
   g_pEmitter->Update(); // Partikel zeichnen
   // Emitter 2 hat Textur 3
   glBindTexture(GL_TEXTURE_2D, g_Texture[3]);
   g_pEmitter2->Update(); // Partikel zeichnen
   SwapBuffers(g_hDC); 
}

Spielt man ein wenig mit dem Code herum, wird man sicherlich noch den ein oder anderen netten visuellen Effekt erzielen können.
Wie man an dieser Demo sieht, ist die Idee und eine entsprechende Textur oft schon der größte Teil der Arbeit, die Realisierung ist dann nur noch Formsache.

Optimierungen des Partikelsystems während etwa:
- Vertex arrays statt Trianglestrips zum Rendern der Partikel
- Eigene Texturverwaltung durch den Emitter
- Verwendung von verschiedenen Presets (verschiedene Strukturen die die Partikel entsprechend einem Effekt initialisieren und so den Code in BuildParticles() erheblich verkürzen)
- Verknüpfung des Emitters mit physikalischen Strukturen (Gravitation, Anziehung, Wind,...)

Feedback erwünscht!

Den VC++ 6.0-Arbeitsbereich dazu gibts hier.

rick@diamond-productions.de, diamond-productions.de

codeworx.org