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