OpenGL Swarm Intelligence
OpenGL Swarm Intelligence-Tutorial von Daniel Schwamm (22.01.2012 - 20.02.2012)
Inhalt
Es ist Sonntag der 22.01.2012 11:40 Uhr. Seit Tagen schon geistert eine Idee
in meinem Kopf herum, die ich nun versuchen werde umzusetzen. Ziel ist es,
mithilfe von OpenGL die Simulation eines Partikelstroms zu realisieren, der
ein halbwegs intelligentes Fluchtverhalten aufweist. Die einzelnen Partikel
sollen sich im Prinzip so verhalten, wie einzelne Fische eines Fischschwarms,
wenn sie von einem Fressfeind angegriffen werden.
Anders als bei meinen bisherigen Tutorials beschreibe ich diesmal kein fertiges
Produkt, sondern dokumentiere den Entwicklungsprozess fortlaufend mit. Ich weiss
also selbst nicht, wo die Reise hingehen wird, ob sie von Erfolg gekrönt sein
wird, und ob das ganze Vorhaben überhaupt einen Sinn macht. Aber, wie üblich
bei mir, muss es halt heraus, auch wenn es sich als Irrweg erweisen sollte,
damit mein Gehirn wieder Ruhe gibt.
Nun denn, ein neues Projekt sollte einen (vorläufigen) Namen erhalten. Da es
unser Ziel ist, eine Art Schwarmintelligenz zu programmieren, die in einer mit
OpenGL gestalteten Umgebung agieren soll, ist ein naheliegender Name für das
Projekt: "OpenGL Swarm Intelligence". Doch warum die englische Übersetzung von
Schwarmintelligenz? Weil englische Projektbezeichnungen eigentlich immer cooler
klingen. Und auch eher ein potenziell internationales Publikum ansprechen. Denn
wer weiss, wenn sich das Projekt als Erfolg erweist, könnte man später auch
noch einen kleinen Film dazu auf YouTube platzieren - und warum sollte man
sich hier durch einen deutschen Titel unnötigerweise das internationale
Publikum abschrecken?
Okay, Projektname "OpenGL Swarm Intelligence" also. Hauen wir diese drei
Wörter in einem nächsten Schritt in Google hinein und schauen, was dabei
herauskommt. - Mh ... sieht nicht so gut aus. Mit dem Thema Simulation von
Schwarmintelligenz hat sich offenbar bereits so manch andere beschäftigt.
Und dies auch noch zu allem Überfluss mithilfe von OpenGL. Aber immerhin,
der exakt gleiche Bezeichner tauchte nirgends bei den Suchergebnissen auf,
daher belassen wir es vorerst beim aktuellen Projektnamen.
Hier liste ich einmal auf, was mir in den letzten Tagen so zur Umsetzung
der Simulation von Schwarmintelligenz eingefallen ist:
-
Programmiersprache wird DelphiGL sein. Mit dieser Sprache habe
ich in mehreren Projekten bereits positive Erfahrung gemacht.
-
Die Umgebung soll zunächst ein komplett leerer Raum sein, in dem sich
die Partikel völlig frei bewegen können. Damit uns die Viecher nicht in
alle Richtungen abhauen, wird in ihre "genetische Struktur" eingebaut,
dass sie den Ursprung des Modells, also die Koordinaten (0,0,0) bevorzugt
aufsuchen. In der Realität wäre dies z.B. durch ein Futterangebot an
dieser Stelle gegeben.
-
Jedes Partikel wird mit individuellem Verhalten ausgestattet. So soll
es sich z.B. bei Erkennung eines Feindes von diesem fortbewegen. Die
Fluchtrichtung soll logischerweise weg vom Feind weisen. Allerdings
nicht immer stur in die genau entgegengesetzte Richtung, weil dies zu
mechanisch wirken würde. Die Fluchtrichtung bekommt daher ein
Zufallselement mit einprogrammiert.
-
Das Schwarmverhalten zeichnet sich dadurch aus, dass sich viele Individuen
gleichzeitig in ihren Bewegungen synchronisieren. Unser Partikel sollte
also bei seinem Fluchtverhalten stets auch bis zu einem gewissen Grad das
Fluchtverhalten seiner Artgenossen berücksichtigen.
-
Es gilt: Das Verhalten eines Partikels wird umso stärker von seinen
Artgenossen bestimmt, je näher diese sich zu ihm befinden. Dreht der
direkte Nachbar nach links, ist dies relevanter, als die Rechtsbewegung
eines Partikels, welches sich am Rand des Schwarms befindet.
-
Es muss eine Gewichtung berechnet werden, inwieweit das individuelle
Verhalten eines Partikels dominiert oder aber das Schwarmverhalten.
Sinnvoll klingt hier die Regel: Je näher ein Feind kommt, umso wichtiger
wird das individuelle Verhalten, je weiter er weg ist, umso wichtiger
wird das Schwarmverhalten.
-
Vermutlich muss den Partikeln neben der Ortsliebe zum Ursprung (0,0,0)
auch noch eine "Schwarmliebe" einprogrammiert werden. Das heisst, wann
immer es einem Partikel möglich ist, sollten es lieber die Nähe des
Schwarms aufsuchen als den leeren Raum. Dieses Verhalten kann in
gefahrlosen Zeiten gelockert sein, sollte im Falle eines Angriffes
von aussen aber Priorität besitzen.
-
Die Individuen eines Schwarms in der Natur, etwa Fische oder Vögel,
wissen nichts über Mathematik. Man kann auch davon ausgehen, dass
sie nicht allzu viel über den kompletten Schwarm an sich wissen,
in dem sie sich befinden. Sie können vermutlich nur die Anzahl und
das Verhalten ihrer direkten Nachbarn abschätzen. Und in diesem Sinne
sollen auch die Partikel in unserem Modell mit möglichst wenig
Meta-Wissen ausgestattet sein.
-
Die einzelnen Partikel werden als Klasse implementiert. Nennen wir
diese doch TParticle. Jedes einzelne Partikel entspricht dann einer
Instanz dieser Klasse.
-
In einer TParticle-Instanz wird die aktuelle Position des Partikels
im Modell vermerkt. Ausserdem seine aktuelle Geschwindigkeit sowie
die Richtung, in die es sich gerade bewegt.
-
Die Berechnung der Fluchtrichtung und Fluchtgeschwindigkeit wird
mithilfe einer inneren Funktion von TParticle vorgenommen. Ich stelle
mir dafür so eine Art Punktesystem vor, welches vorgibt, inwieweit
sich das Partikel eher individuell oder überindividuell verhält,
je nach den äusseren Gegebenheiten.
-
Der Schwarm als Ganzes wird wohl auf ein Array von TParticle
hinauslaufen. Wobei die Dimensionierung die leichte Erweiterung
bzw. Reduzierung der Grösse des Schwarms ermöglicht. Im Idealfall
funktioniert der Schwarm mit einem Partikel oder zwei oder drei
Partikeln ebenso wie mit Tausenden.
Da ich bereits mehrere DelphiGL-Projekte realisiert habe, kann ich mich
selbst beklauen, um an eine OpenGL-Umgebung zu kommen. Dieses Projekt
wird in einen neuen Ordner kopiert, umbenannt und danach von allen
Funktionen und Deklarationen befreit, die für das Schwarm-Projekt nicht
relevant sind. Das wird einen Moment dauern ...
So, da bin ich wieder. Als Muster-Projekt diente mir "OpenGL ISS", ein
Programm, welches auf dieser Homepage früher schon einmal ausführlich
erläutert wurde:
Delphi-Tutorial zu "OpenGL ISS"
Dort integriert war nämlich die recht aufwendige
Berechnung des Richtungsvektors des Betrachters der Szenerie gemäss
der Rotationen der x- y- und z-Achse. Dabei kommt auch wieder die Unit
"dan_geo_u.pas" zum Einsatz.
Eliminiert wurden jedoch alle Beschleunigungsfaktoren bei der Berechnung
der Geschwindigkeit der Bewegungen. Das heisst, sowie die Cursor-Tasten
nicht mehr gedrückt werden, stoppt die Bewegung sofort und wird nicht
wie bei "OpenGL ISS" allmählich abgebremst.
Das Malen der Szenerie geschieht (vorerst) weiterhin über einen Timer,
der periodisch aufgerufen wird. Durch Änderung der Intervall-Zeit kann
so leicht Einfluss auf die Ausgabegeschwindigkeit genommen werden.
Ebenfalls übernommen wurde der Hintergrund, damit man überhaupt etwas
von der Bewegung wahrnehmen kann. Die Szenerie spielt sich dadurch
innerhalb einer riesigen Sphäre ab, deren Aussenfläche vorerst mit einer
Weltraum-Textur versehen wurde.
Die Steuerung im Raum wird berechnet, wenn der oben erwähnten "frequncy_t"-Timer
feuert. Die für die Steuerung relevanten Tastaturereignisse wurden zuvor in
den globalen Variablen "a_key" und "a_ctrl" gesichert. Die zur Berechnung des
Look-Vektors (also des Richtungsvektors des Betrachters in die Szenerie hinein)
benötigten Funktionen sind in der Unit "dan_geo_u.pas" definiert.
Eine Erläuterung hierzu findet sich im
Delphi-Tutorial "OpenGL ISS".
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
procedure Tmain_f.frequency_tTimer(Sender: TObject);
//--------------------------------------------------------------
//give back rotated look vector about all axis
function my_look_vector_get:tv;
var
v:tv;
mx,my,mz,m:tm;
begin
//fill rotation matrices
mx:=m_fill_rot_x_deg(my_rotation_v.x);
my:=m_fill_rot_y_deg(my_rotation_v.y);
mz:=m_fill_rot_z_deg(my_rotation_v.z);
//create general rotation matrix
m:=m_mul(mz,my);
m:=m_mul(m,mx);
//rotate the look vector about all axis
v:=v_fill(0,0,-1);
v:=m_v_mul(m,v);
result:=v;
end;
//--------------------------------------------------------------
procedure my_position_calc(dir:integer);
var
mlv:tv;
begin
mlv:=my_look_vector_get;
my_position_v.x:=my_position_v.x+mlv.x*dir*_my_postion_delta;
my_position_v.y:=my_position_v.y+mlv.y*dir*_my_postion_delta;
my_position_v.z:=my_position_v.z+mlv.z*dir*_my_postion_delta;
end;
//--------------------------------------------------------------
procedure my_rotation_calc(dir:integer;axis:byte);
begin
if axis=0 then my_rotation_v.x:=deg_norm(my_rotation_v.x-dir*_my_rotation_delta);
if axis=1 then my_rotation_v.y:=deg_norm(my_rotation_v.y-dir*_my_rotation_delta);
if axis=2 then my_rotation_v.z:=deg_norm(my_rotation_v.z-dir*_my_rotation_delta);
end;
begin
//change position/direction?
if active_key<>0 then
begin
if active_ctrl then
begin
//rotation x-axis
if active_key=vk_up then my_rotation_calc( 1,0)
else if active_key=vk_down then my_rotation_calc(-1,0);
//rotation z-axis
if active_key=vk_left then my_rotation_calc( 1,2)
else if active_key=vk_right then my_rotation_calc(-1,2);
end
else
begin
//flying: rotation y-axis
if active_key=vk_right then my_rotation_calc( 1,1)
else if active_key=vk_left then my_rotation_calc(-1,1)
//movement forward or backward
else if active_key=vk_up then my_position_calc( 1)
else if active_key=vk_down then my_position_calc(-1);
end;
end;
draw_scene;
end;
Die Steuerung ist knackig klein und elegant formuliert, wie ich finde.
Im Wesentlichen wird nur der Richtungsvektor bestimmt, der wiedergibt,
in welche Richtung der Anwender gerade schaut. Dieser Richtungsvektor
wird mit einem konstanten Wert entweder verlängert oder verkürzt, je
nach Richtung, in die geflogen werden soll. Zuletzt wird der so
berechnete Richtungsvektor zum aktuellen Positionsvektor des Betrachters
("my_position_v") hinzuaddiert, und schon ist die neue Position im Raum
bestimmt. Anschliessend wird unter diesen Bedingungen die Szenerie
über "draw_scene" neu berechnet.
Mh ... wie mir eben auffiel, ist die Steuerung doch noch reichlich suboptimal.
Bewege ich mich nämlich in der Ausgangssituation nach links, dann drehe
ich mich korrekt im Raum um die eigene Achse, d.h. um die y-Achse.
Wenn ich nun aber senkrecht nach oben schwenke, mich also 90 Grad um die
x-Achse drehe, dann verläuft die y-Achse parallel zu meiner Sichtrichtung.
Drehe ich mich nun nach links, resultiert daraus eine Kreiselbewegung,
die so eigentlich nicht gewollt ist. Stimmig wäre es erst, wenn sich meine
Drehung nicht an den absoluten Achsen des Modells orientieren würde, sondern
am Richtungsvektor und den dazu orthogonalen Achsen. Nun ja, dass schauen
wir uns vielleicht später noch einmal näher an.
Unsere Welt wird über den "freqwuency_t"-Timer permanent neu gezeichnet.
Und dazu wird die Prozedur "draw_scene" aufgerufen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
procedure tmain_f.draw_scene;
//space-------------------------------------------------
function float_to_string(d:single):string;
begin
result:=format('%.0f',[d]);
end;
//space-------------------------------------------------
procedure draw_space;
begin
glTranslatef(0,0,0);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,space_tx);
gluSphere(space_quad,_space_radius,20,20);
glDisable(GL_TEXTURE_2D);
end;
begin
//get graphic mode
glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
glClear(GL_COLOR_BUFFER_BIT OR GL_DEPTH_BUFFER_BIT);
glLoadIdentity;
//rotation & positioning
glRotatef(360-my_rotation_v.x,1.0,0,0);
glRotatef(360-my_rotation_v.y,0,1.0,0);
glRotatef(360-my_rotation_v.z,0,0,1.0);
glTranslatef(-my_position_v.x,-my_position_v.y,-my_position_v.z);
draw_space;
SwapBuffers(handle_DC);
status_p.caption:=
'Position: '+
float_to_string(-my_position_v.x)+'/'+
float_to_string(-my_position_v.y)+'/'+
float_to_string(-my_position_v.z)+
' | Rotation: '+
float_to_string(my_rotation_v.x)+'/'+
float_to_string(my_rotation_v.y)+'/'+
float_to_string(my_rotation_v.z);
end;
Es werden zunächst ein paar Grundeinstellungen am OpenGL-Modell vorgenommen
und der Grafikpuffer geleert. Mit "glLoadIdentity" wird der Betrachter
imaginär an den Ursprung positioniert. Anschliessend werden die Rotationen
und Positionsänderungen ausgeführt, die der Benutzer vollführt hat, und
zwar gerade in negierter Weise - denn nicht der Betrachter bewegt sich
(der hockt ja starr vor dem Monitor), sondern das Modell. Die Prozedur
"draw_space" kopiert die Sphäre der Raumkugel in den Grafikpuffer. Zuletzt
werden dann die genauen Positionsangaben noch auf dem Panel "status_p"
ausgegeben.
Grundgerüst von OpenGL Swarm Intelligence: Wir befinden uns noch in einem grossen, leeren Raum, dessen Ränder
durch eine Textur kenntlich gemacht wurden. Wir können uns bereits
mittels der Cursor-Tasten und mithilfe der STRG-Taste in alle
Richtungen bewegen.
Das grösste Problem bei dieser Prozedur war für mich eigentlich die
Textausgabe. Denn eigentlich wollte ich nicht auf ein Panel schreiben,
sondern direkt in den Grafikpuffer von OpenGL. Ich habe aber eben auf
die Schnelle nichts im Web gefunden, worüber sich dieses realisieren
liesse. Seltsamerweise. Nun denn - stellen wir das erst einmal zurück.
Ausgehend von unseren früheren Überlegungen im Kapitel
2.1.2 Ideen zur Realisierung
bauen wir nun eine Klasse
"TParticle" auf, die alle Eigenschaften eines Partikels kapselt.
Spontan fällt mir dazu nur ein, dass die Positionsangaben im Raum benötigt
werden. Auch ein Bezeichner "quad" für die OpenGL-Objekt-ID wird deklariert.
Und dann natürlich noch eine Prozedur "draw" für die Ausgabe des Partikels
auf dem Bildschirm. Das ergäbe dann in etwa so etwas:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
type
TParticle = class(TObject)
public
quad:PGLUquadric;
position_v:tv;
constructor create;
procedure draw;
end;
implementation
//------------------------------------------------------------
constructor TParticle.create;
begin
quad:=gluNewQuadric;
position_v:=v_fill(random(100)-50,random(100)-50,random(100)-50);
end;
//------------------------------------------------------------
procedure TParticle.draw;
begin
glTranslatef(position_v.x,position_v.y,position_v.z);
gluSphere(quad,1,20,20);
glColor3f(1,1,1);
end;
Wie wir im Konstruktor sehen, verpassen wir dem Partikel zu Beginn eine
Zufallsposition im Raum, die irgendwo innerhalb eines Radius von 50 Metern
rund um den Ursprung herum liegt. Ausserdem bekommt das kugelförmige Partikel
eine Grösse von einem Meter zugewiesen (zweiter Parameter bei "gluSphere").
Nun müssen wir die TParticle-Klasse in unser Modell integrieren. Dazu bietet
sich ein Array aus TParticle-Elementen an, welches wir vorerst mit nur einem
einzigen Element füllen wollen. Die Initialisierung des Arrays geschieht bei
"FormCreate" und die Zerstörung in "FormDestroy". Die Ausgabe aller Partikel
erfolgt direkt nach der Ausgabe der Raum-Sphäre.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
const _particle_max=1;
type
Tmain_f = class(TForm)
[...]
particle_a:array[0.._particle_max] of TParticle;
[...]
end;
implementation
//---------------------------------------------
procedure Tmain_f.FormCreate(Sender: TObject);
//---------------------------------
procedure particles_create;
var
r:integer;
particle:TParticle;
begin
for r:=0 to _particle_max-1 do
begin
particle:=TParticle.create;
particle_a[r]:=particle;
end;
end;
begin
[...]
particles_create;
[...]
end;
//clear program-------------------------------------------------------
procedure Tmain_f.FormDestroy(Sender: TObject);
var
r:integer;
begin
[...]
for r:=0 to _particle_max-1 do particle_a[r].Free;
[...]
end;
//-----------------------------------------------------------
procedure tmain_f.draw_scene;
//-----------------------------------------
procedure draw_particles;
var
r:integer;
particle:TParticle;
begin
for r:=0 to _particle_max-1 do
begin
particle:=particle_a[r];
particle.draw;
end;
end;
begin
[...]
draw_space;
draw_particles;
SwapBuffers(handle_DC);
[...]
end;
Das erste Partikel: Wir haben unser erstes Partikel generiert. Per Zufall wird
es irgendwo in der Nähe des Ursprungs generiert. Dieses Exemplar liegt in 10 Metern
Entfernung direkt vor uns. Wir können, Dank OpenGL, bereits jetzt schon zu ihm
fliegen und es aus allen Richtungen betrachten.
Der Source zur Generierung und Ausgabe unseres Einzelpartikels, den wir im vorherigen
Kapitel kennenlernten, ist schlauerweise von vorneherein dafür ausgelegt worden, nicht
nur ein Partikel, sondern beliebig viele Partikel im Array "particle_a" zu verwalten.
Alles, was wir machen müssen, um z.B. 100 Partikel zu erzeugen, ist die Konstante
"_particle_max" auf 100 zu setzen. Schauen wir uns das Ergebnis an:
Der erste Partikelschwarm: Nur durch Änderung einer Konstante haben wir aus unserem Einzelpartikel exakt 100 Partikel gemacht, die zufällig verteilt im Raum um uns herum schweben. Allerdings können bisher nur wir uns bewegen, die Partikel selbst ändern ihre Position noch nicht.
Unser Schwarm sieht schon ganz hübsch aus. Betrachtet man jedoch die Koordinatenangaben
im Kopf-Panel, dann sieht man, dass wir uns an einer Position im Raum befinden, an der
es eigentlich gar keine Partikel mehr geben dürfte (-119/104/268). Geplant hatten wir
doch, dass diese sich innerhalb eines Radius von nur 50 Metern rund um den Ursprung
bewegen.
Mir wurde bald klar, worin der Grund für die falsche Positionierung liegt. OpenGL
zeichnet immer relativ von der letzten Position aus weiter. Wird also ein Partikel
an Position (20,0,0) gezeichnet, dann landet das nächste Partikel mit Position
(40,0,0) nicht an dieser absoluten Stelle, sondern relativ zur vorherigen Position
bei Koordinate (20+40,0,0). Um die relative Positionierung zu verhindern, gibt es
die OpenGL-Befehle "glPushMatrix" und "glPopMatrix". Diese sorgen dafür, dass nach
einer Malaktion stets wieder von der vorherigen Position ausgegangen wird, was in
unserem Fall dem Ursprung entspricht. Wir müssen also "draw" von TParticle noch
ein klein wenig erweitern:
00001
00002
00003
00004
00005
00006
00007
00008
procedure TParticle.draw;
begin
glPushMatrix();
glTranslatef(position_v.x,position_v.y,position_v.z);
gluSphere(quad,0.5,20,20);
glColor3f(1,1,1);
glPopMatrix();
end;
Korrigierter Partikelschwarm: Bei diesem Beispiel wurde die Anzahl der
Partikel auf 1.000 erhöht. Sie sind jetzt nur noch 10 cm im Radius gross, etwa
so wie dicke Fische. Die zufällige Positionierung wurde auf 10 m um den Ursprung
reduziert. Und voilà, das Ergebnis sieht schon eher so aus, wie es einem Schwarm
geziemt.
Einen Moment lang wusste ich eben nicht, wie fortfahren. Angesichts des recht
dichten Partikelschwarms, den wir bereits generieren können, stellte ich mir
dann aber vor, dass es sicher cool aussähe, wenn wir durch diesen hindurchfliegen
und die Partikel sich daraufhin alle vom Betrachter wegbewegen. Wir würden damit
also quasi den Schwarm der Partikel teilen wie einst Moses das Rote Meer.
Dazu ist eine neue Prozedur von TParticle nötig. Nennen wir sie "calc_position".
Diese rufen wir unmittelbar vor dem Zeichnen eines jeden Partikels auf. Darin
wird dann anhand der Nähe des Betrachters ermittelt, ob und wie eine Ausweichbewegung
initiiert werden muss. Entsprechend wird dann "postion_v" modifiziert.
Na, das habe ich eben aber wieder locker angekündigt, diese Flucht der Partikel
vor dem Betrachter. Denn sinnvollerweise bewegen sich die Partikel dann in die genau
entgegengesetzte Richtung. Ist das Partikel direkt vor dem Betrachter, dann flieht
es also tiefer in den Raum hinein. Das ist noch einfach zu kalkulieren, denn dies
entspricht ja genau dem Richtungsvektor des Betrachters "look_vector". Komplizierter
wird es aber, wenn sich Partikel seitlich vom Betrachter befinden. Hier muss
idealerweise ein Vektor bestimmt werden, welcher die Richtung zwischen Partikel
und Betrachter wiedergibt. Das Partikel müsste dann für die optimale Flucht nur
noch gemäss dieses Richtungsvektors seine Position ändern. Nur, wie ermittle ich
den gesuchten Richtungsvektor?
Da stand ich echt eine Weile auf dem Schlauch. Ich wusste zwar, ich kann den Winkel
zwischen zwei Punkten berechnen, denn dazu gibt es eine Funktion in der "dan_geo_u"-Unit,
nämlich "v_angle". Doch diese Funktion liefert als Ergebnis einen Skalar zurück,
also einen Einzelwert. Doch was bringt der mir? Wie mache ich daraus dann einen Vektor?
Letztlich half mir Google auf die Sprünge. Nach mehreren Versuchen hatte ich
endlich eine geeignete Suchanfrage formuliert: "Richtungsvektor zwischen zwei
Punkten". Gleich einer der ersten Treffer führte mich auf die Webseite
http://www.rither.de/. Dort stand peinlicherweise, dass die Berechnung des
Vektors zwischen zwei Punkten zu den einfachsten Aufgaben in der Geometrie gehört.
Und das stimmte auch. Man erhält den gesuchten Vektor nämlich schlicht durch eine
Subtraktion der Punktvektoren. Na, dann wollen wir doch einmal schauen, ob das
wirklich so stimmen kann!
Um die Fluchtrichtung eines Partikels zu berechnen, benötigen wir einen Richtungsvektor.
Dieser ergibt sich laut vorherigem Abschnitt durch Subtraktion der Partikelposition
von der Betrachterposition. Weiterhin soll gelten: Je näher der Betrachter kommt,
umso ausgeprägter soll die Fluchtrichtung eingeschlagen werden. Und um das Ganze
optisch noch etwas attraktiver zu gestalten, werden zudem die Partikel mit
verschiedenen Farben versehen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
const
_particle_max=5000;
_particle_pos_radius=5;
_particle_radius=0.1;
_particle_slices=5;
_particle_ecaspe_viewer_distance=5;
_particle_ecaspe_viewer_scale=0.01;
implementation
//------------------------------------------------------------
constructor TParticle.create;
begin
[...]
r_color:=1-random(200)/200;
g_color:=1-random(200)/200;
b_color:=1-random(200)/200;
end;
//------------------------------------------------------------
procedure TParticle.draw;
begin
calc_position;
[...]
glColor3f(r_color,g_color,b_color);
[...]
end;
//------------------------------------------------------------
procedure TParticle.calc_position;
var
distance:single;
escape_v:tv;
escape_speed:single;
begin
//calculate distance to user
distance:=dan_geo_u.v_distance(position_v,main_f.my_position_v);
if distance>_particle_ecaspe_viewer_distance then exit;
escape_speed:=(_particle_ecaspe_viewer_distance-distance)*_particle_ecaspe_viewer_scale;
escape_v:=dan_geo_u.v_sub(position_v,main_f.my_position_v);
escape_v:=dan_geo_u.v_norm(escape_v);
escape_v:=dan_geo_u.v_scale(escape_v,escape_speed);
position_v:=dan_geo_u.v_add(position_v,escape_v);
end;
Zunächst erweitern wir die TParticle-Klasse um drei Fliesskommazahlen, die
bei "create" mit zufälligen Werten gefüllt werden. Diese Variablen dienen als
Übergabewerte für die OpenGL-Prozedur "glColor3f" in der "draw"-Prozedur.
Dadurch werden die Partikel farbig wiedergegeben.
In "calc_position" wird zunächst mithilfe der "v_distance"-Funktion aus
der "dan_geo_u"-Unit der Abstand zwischen Partikel und Betrachter ermittelt.
Liegt dieser über fünf Meter, dann wird die Prozedur wieder verlassen. Ansonsten
wird eine Fluchtbewegung des Partikels vorgenommen. Aus dem Abstand zum
Betrachter wird über eine Formel die Fluchtgeschwindigkeit berechnet.
Den Fluchtvektor berechnen wir durch Subtraktion der Partikelposition
von der Betrachterposition. Der sich so ergebene Fluchtvektor wird mit
der genormten Fluchtgeschwindigkeit skaliert. Und zuletzt wird der Fluchtvektor
zum Positionsvektor addiert, wodurch sich die neue Position des Partikels
ergibt.
Fluchttunnel durch den Partikelschwarm: Hier kann man sehen, wie sich
der Betrachter durch den Partikelschwarm bewegt hat. Die Partikel in seiner
Nähe sind ihm aus dem Weg gegangen, während die weiter entfernten Partikel
ihre Position nicht verlassen haben.
Mein PC muss bei 1.000 Partikeln schon kämpfen, bei 10.000 Partikeln geht
er regelrecht in die Knie. Aber andere OpenGL-Programmierer schaffen es,
sogar noch wesentlich mehr Partikel flüssig zu animieren. Wie machen die
das? Der Grund für die Trägheit unserer Partikel ist eigentlich klar und
den hatte ich auch schon die ganze Zeit im Hinterkopf: Die Sphären, die wir
über den OpenGL-Befehl "gluSphere" generieren, sind relativ komplexe Objekte,
deren Berechnungen entsprechend viel CPU-Power verschlingen.
Auf der Suche nach weniger aufwendigen Partikeln stiess ich rasch auf Polygone.
Diese lassen sich durch die Angabe ihrer Eckpunkte, den Vertices, relativ
exakt definieren. Ich entschied mich für nur drei Eckpunkte, wodurch
dreieckige Flächen entstanden, die man mit etwas Fantasie als Fische
interpretieren kann. Und das wäre ja für ein Schwarm-Partikel nicht die
ungünstigste Form. Unsere "Fisch"-Polygone erhalten übrigens eine Länge
von 20 cm und eine Höhe von 6 cm; die Ausmasse eines Standard-Fisches halt.
Ansonsten sind sie flach wie Flundern. Dadurch, dass wir von nun an mit
Vertices arbeiten, kann das Objekt "quad" aus der TParticle-Klasse eliminiert
werden.
Ebenfalls wieder entfernt wurden die drei Farbwerte für Rot, Grün und Blau
aus der TParticle-Klasse. Dies Farbe berechnen wir nämlich jetzt innerhalb
der "draw"-Prozedur selbst, und zwar anhand der Distanz zum Betrachter.
Verwendet werden nur Grüntöne, wobei gilt, je weiter das Partikel vom
Betrachter entfernt ist, umso dunkler wird es dargestellt. Die ehemalige
Prozedur "calc_position" wurde dafür zur Funktion, die uns den Distanzwert
zurückliefert.
Ausserdem stolperte ich eben über einen dicken Fehler, der mir bisher nicht
aufgefallen war. Bei der zufälligen Positionierung der Partikel wurden
ganzzahlige Zufallswerte generiert, was zur Folge hatte, dass die Partikel
anfangs alle einen Mindestabstand von einem Meter zueinander hatten. Dadurch
war auch die Wahrscheinlichkeit gross, dass verschiedene Objekte den gleichen
Ort einnahmen. So sah man letztlich viel weniger Objekte, als tatsächlich
berechnet wurden. Die "create"-Prozedur wurde entsprechend verbessert.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
const
_particle_width=0.03;
_particle_height=0.1;
//------------------------------------------------------------
constructor TParticle.create;
begin
position_v:=v_fill(
(random(1000*2*_particle_pos_radius)-1000*_particle_pos_radius)/1000,
(random(1000*2*_particle_pos_radius)-1000*_particle_pos_radius)/1000,
(random(1000*2*_particle_pos_radius)-1000*_particle_pos_radius)/1000
);
[..]
end;
//-------------------------------------------------------
function TParticle.calc_position:single;
[...]
begin
[...]
end;
//-------------------------------------------------------
procedure TParticle.draw;
var
distance:single;
color:single;
begin
distance:=calc_position;
glPushMatrix();
glTranslatef(position_v.x,position_v.y,position_v.z);
color:=10-distance;if color<0 then color:=0;
color:=color/20+0.5;
glColor3f(0,color,color);
glBegin(GL_TRIANGLES);
glVertex3f(0, _particle_width, _particle_height); // lower left vertex
glVertex3f(0,-_particle_width, _particle_height); // lower right vertex
glVertex3f(0, 0,-_particle_height); // upper vertex
glEnd();
glPopMatrix();
end;
Schneller Partikelschwarm aus Polygonen: Die Verwendung von Polygonen statt
Sphären für die Partikel erlaubt eine erheblich schnellere Wiedergabe des Schwarms.
Die Partikel wurde zudem so eingefärbt, dass sie umso dunkler werden, je weiter sie
vom Betrachter entfernt sind. Der Schwarm ähnelt damit immer mehr einem natürlichen
Schwarm aus Fischen.
Durch die Verwendung von Polygonen wurde die Zeichengeschwindigkeit unseres Schwarms
enorm gesteigert. Leider besitzen die Polygone aber anders als Sphären eine Richtung.
Es ist also für die physikalische Korrektheit nötig, die Partikel mit der Spitze
in die Richtung weisen zu lassen, in die sie sich bewegen. Damit haben wir uns aber
ein ziemliches Problem aufgehalst.
Alle Partikel zeigen in die gleiche Richtung: Obwohl wir uns durch den
Schwarm bewegt haben und jedes Partikel für sich auch in die korrekte Richtung
geflohen ist, so weisen doch alle Partikel mit der Spitze in die gleiche
Richtung. Das entspricht so natürlich nicht den von Fischen gewohnten Gegebenheiten.
Das muss noch geändert werden. Nur wie?
Autsch! Hier machten sich einmal wieder meine bisweilen eingerosteten bzw. mangelnden
mathematischen Kenntnisse schmerzlich bemerkbar. Die Fluchtrichtung der Partikel haben
wir ja bereits in einem Richtungsvektor ermittelt. Wir wissen also im Prinzip,
in welcher Richtung die Spitze des Partikels weisen müssten. Da wir jedes Partikel
einheitlich malen, und zwar vom Ursprung aus gesehen als Dreieck, dessen Spitze
tiefer in den Raum weist, müssen wir jedes Partikel einzeln in seine Bewegungsrichtung
rotieren lassen. Der OpenGL-Befehl "glRotatef" bietet sich dafür ja ganz offenkundig an.
Nur, der erwartet eine Winkelangabe für jede Achse. Doch wie kalkulieren wir aus dem
gegebenen Richtungsvektor die passenden Winkel, um daraus den geforderten
Rotationsvektor zu bilden?
Diesmal wurde ich im Web nur wenig fündig. Mir fiel aber selbst wieder ein, dass
man den Winkel in einem rechtwinkligen Dreieck dadurch berechnen kann, indem
man die Höhe durch die Breite teilt. Dieser Wert entspricht der Steigung der
Geraden, also der Hypotenuse. Füttert man die Arcustangens-Funktion mit der
Steigung, dann erhält man ihren Winkel - allerdings im Bogenmass, nicht in Grad.
Ein Umstand, der mich noch einmal etliche Minuten meines Lebens gekostet hat, bis
ich es endlich registriert hatte.
Folgender Source wird verwendet, um den Winkel des Fluchtvektors zu berechnen,
der nötig ist, damit unser Partikel in gewünschter Weise um die y-Achse
rotiert werden kann:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
function TParticle.calc_position:single;
begin
[...]
escape_v:=dan_geo_u.v_sub(position_v,main_f.my_position_v);
if escape_v.x<>0 then rotation_v.x:=deg_norm(rad2deg(arctan(escape_v.y/escape_v.x)));
[...]
end;
//------------------------------------------------------------
procedure TParticle.draw;
begin
[...]
glRotatef(rotation_v.y,0,1,0);
glBegin(GL_TRIANGLES);
[...]
glEnd();
glPopMatrix();
end;
Rotation der Partikel um die y-Achse: Als wir uns durch den Schwarm
bewegten, wurden die Partikel in der Gefahrenzone gezwungen, sich in eine andere
Richtung zu bewegen. Diese Bewegungsrichtung lässt sich nun sehr schön und
naturgemäss an ihrer Spitze erkennen.
Die Rotation um die y-Achse in Bewegungsrichtung funktionierte korrekt, soweit
ich das überblicken konnte. Als ich nun jedoch das Gleiche mit der x- und z-Achse
Achse versuchte, klappte es leider nur manchmal, aber nicht immer. Der Grund dafür
ist mir nicht klar, aber ich vermute einmal, es hat mit der Rotationsreihenfolge
der Achsen zu tun, mit falschen Formeln und meinem beschränktem Verständnis für
die gesamte Materie.
Um überhaupt abschätzen zu können, in welche Richtung die Partikel genau zeigen
müssten, wenn es mathematisch korrekt zu geht, habe ich bei folgendem Beispiel
jeden Partikel-Fluchtvektor mit ins Modell eingezeichnet. Was eigentlich auch
ziemlich cool aussieht, oder? Allerdings zeigt sich jetzt auch überdeutlich,
dass irgendetwas mit unserer Rechnung noch nicht so ganz stimmen kann.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
function TParticle.calc_position:single;
begin
[...]
escape_v:=dan_geo_u.v_sub(position_v,main_f.my_position_v);
if escape_v.x<>0 then rotation_v.x:=deg_norm(rad2deg(arctan(escape_v.y/escape_v.x)));
if escape_v.z<>0 then rotation_v.y:=deg_norm(rad2deg(arctan(escape_v.x/escape_v.z)));
if escape_v.y<>0 then rotation_v.z:=deg_norm(rad2deg(arctan(escape_v.x/escape_v.y)));
[...]
end;
//------------------------------------------------------------
procedure TParticle.draw;
begin
[...]
glPushMatrix;
[...]
glRotatef(rotation_v.x,1,0,0);
glRotatef(rotation_v.y,0,1,0);
glRotatef(rotation_v.z,0,0,1);
glBegin(GL_TRIANGLES);
[...]
glEnd();
glPopMatrix();
//escape-vectors
glPushMatrix;
glPointSize(4);
glColor3f(1,0.5,0.5);
glBegin(GL_LINES);
glVertex3f(position_v.x,position_v.y,position_v.z);
v:=dan_geo_u.v_add(position_v,v_scale(escape_v,100));
glVertex3f(v.x,v.y,v.z);
glEnd;
glPopMatrix();
end;
(Fehlerhaft) ausgerichtete Partikel mit Fluchtvektor: Wieder sind wir durch
den Schwarm geflogen, und wieder sind die Partikel nach allen Seiten davon gerauscht.
Anhand der roten Fluchtvektoren erkennt man aber auch, das nicht wenige Partikel trotz
der berechneten Achsenrotationen noch immer in die falsche Richtung weisen.
Aaaargh!
Einen ganzen verdammten Tag habe ich mich mit der Ausrichtung der Partikel
beschäftigt, ohne auf einen grünen Zweig zu komme. Zwischenzeitlich habe ich die
Polygone sogar durch Punkte ersetzt, die mittels des OpenGL-Befehls "glPointSize"
bei Annäherung in passend grosse quadratische Flächen transformiert wurden. Das
funktionierte auch gut und schnell, aber rein optisch gefallen mir die
"Fisch"-Partikel halt eindeutig besser.
Dennoch sei hier kurz gezeigt, wie die Points gezeichnet wurden:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
procedure TParticle.draw;
begin
[...] if distance<10 then
begin
gltranslatef(position_v.x,position_v.y,position_v.z);
distance:=sqr(10/(distance));
glPointSize(2+distance);
glBegin(GL_POINTS);
glVertex3f(0,0,0);
glEnd();
end;
end;
Partikel als richtungslose Punkte: Um den Schwierigkeiten der Ausrichtung der dreieckigen Partikel zu umgehen, wurden sie versuchsweise als Punkte gezeichnet, die sich bei Annäherung vergrössern. Diese Vorgehensweise wurde aber wieder rückgängig gemacht, da die Fisch-Partikel einfach cooler aussehen.
Dann fiel mir jedoch zum Glück endlich auf, dass ich bereits weiter oben durchaus
in der Lage war, die Fluchtvektoren ins Modell einzuzeichnen. Und zwar alleine unter
Verwendung des Richtungsvektors, d.h. ohne jede Rotation. Ich müsste also nur statt
einer Linie ein dreieckiges Polygon berechnen, dann hätte ich doch genau die korrekt
ausgerichteten Partikel! Und hier ist auch schon der Source dazu, der mich ganz anders
als die Rotationsgeschichte nur einige wenige Minuten Arbeit gekostet hat:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
const
_particle_length=0.2;
implementation
//------------------------------------------------------------
procedure TParticle.draw;
var
distance:single;
color:single;
v:tv;
begin
glPushMatrix();
distance:=calc_position;
color:=10-distance;if color<0 then color:=0;
color:=color/20+0.5;
glColor3f(0,color,color);
escape_v:=dan_geo_u.v_norm(escape_v);
escape_v:=dan_geo_u.v_scale(escape_v,_particle_length);
v:=dan_geo_u.v_add(position_v,escape_v);
glBegin(GL_TRIANGLES);
glVertex3f(position_v.x,position_v.y-_particle_width,position_v.z);
glVertex3f(position_v.x,position_v.y+_particle_width,position_v.z);
glVertex3f(v.x,v.y,v.z);
glEnd();
glPopMatrix;
end;
Der Source zur Winkelberechnung für die Achsen-Rotation des Partikels
in Funktion "calc_position" wurde eliminiert.
In der Prozedur "draw" wird zunächst der Fluchtvektor normiert, d.h.,
auf eine Länge von 1 gebracht. Danach wird er durch Skalierung auf
die Partikel-Länge reduziert. Anschliessend wird der Fluchtvektor
mit dem Positionsvektor addiert, wodurch wir die Position der Spitze
des Partikels erhalten. Zuletzt wird dann noch das Polygon gezeichnet,
und zwar von der Partikelposition bis zur berechneten Spitze.
Korrekt ausgerichtete Partikel mit Fluchtvektor: Basierend auf dem Fluchtvektor werden die Partikel ohne Rotation direkt perspektivisch korrekt gezeichnet. Dadurch sind alle Partikel gemäss ihre Bewegung ausgerichtet. Und die roten Richtungsvektoren schauen jetzt aus wie Laserstrahlen, die aus kleinen Raumschiffen abgeschossen werden.
Das nächste Vorhaben ist, den Partikeln eine Eigenbewegung zu geben. Und zwar
sollen sie zunächst ganz simpel um den Ursprung kreisen. Doch wie stellen wir
das an? Wir haben gesehen, dass wir den Ortsvektor mit dem Richtungsvektor
addieren müssen, um eine Bewegung zu simulieren. Ziel müsste also sein, einen
passenden Richtungsvektor zu berechnen, wodurch das Partikel auf eine Kreisbahn
gezwungen wird.
Schnell zeigte sich jedoch, dass dies gar nicht so einfach ist. Obwohl uns
eigentlich alle nötigen Werte vorliegen. Der Radius ergibt sich durch die Länge
des Positionsvektors. Als Winkelzuwachs wollen wir jeweils einen Grad nehmen. Die
Formeln für die Drehung im zweidimensionalen Raum habe ich noch im Kopf. Die
Winkel-Variable "periode" wird bei jeder Aktualisierung des Modells um einen Grad
nach oben gezählt wird. Daraus basteln wir uns also einmal Folgendes zusammen:
00001
00002
00003
00004
00005
00006
00007
00008
radius:=dan_geo_u.v_len(position_v);
radius:=radius;
escape_v.x:=radius*cos(main_f.periode);
escape_v.y:=radius*sin(main_f.periode);
escape_v.z:=0;
escape_v:=v_norm(escape_v);
escape_v:=v_scale(escape_v,1);
position_v:=dan_geo_u.v_add(position_v,escape_v);
Okay ... die Partikel drehen sich durchaus. Aber nicht um den Mittelpunkt, sondern
in sehr unvorhersehbarer Weise. Mh ... da müssen wir wohl nachbessern.
Ich experimentierte noch sehr viel mit den Parametern und Formeln für die Rotation
herum, bekam aber nie das gewünschte Ergebnis.
Doch warum sollen wir eigentlich alles selber machen? OpenGL verfügt ja bereits über
den Befehl "glRotatef". Den rufen wir jetzt einmal jeweils direkt vor dem Zeichnen
eines Partikel auf, um so den "Zeichenstift" im Modell an die gewünschte Stelle zu
platzieren. Mal schauen, was dabei herauskommt ...
00001
00002
00003
00004
00005
00006
00007
angle:=main_f.periode;
glRotatef(angle,0,1,0);
glRotatef(angle,1,0,0);
glRotatef(angle,0,0,1);
glBegin(GL_TRIANGLES);
[...]
glEnd();
Na bitte! Das klappte ja auf Anhieb. Alle Partikel drehten sich nun brav und synchron
um den Ursprung herum.
Synchrone Partikel-Rotation mittels OpenGL-Befehl: Dank mächtiger OpenGL-Befehle
werden hier ohne Einsatz komplizierter Formeln alle Partikel um den Ursprung rotiert.
Um das aktuelle doch sehr statische Rotationsverhalten des Schwarms etwas aufzubrechen,
variieren wir den Winkel in Abhängigkeit zum Abstand des Betrachters. Wenn wir
also in den Schwarm hineinfliegen, sollten sich die Bewegungsmuster der einzelnen
Partikel ändern. Verstärkt wird der Effekt noch, indem die Betrachter-Detektion
wieder aktiviert wird, die dafür sorgt, dass die Partikel ausweichen und so auf neue
Bahnen gelangen. Der Single-Wert "angle" wird zusätzlich in die TParticle-Klasse mit
aufgenommen.
00001
00002
00003
00004
00005
00006
00007
00008
angle:=angle+(10-distance)/10;
if angle>360 then angle:=rotation_v.y-360;
glRotatef(angle,0,1,0);
glRotatef(angle,1,0,0);
glRotatef(angle,0,0,1);
glBegin(GL_TRIANGLES);
[...]
glEnd();
Wow! Ja, das Ergebnis kann sich wirklich sehen lassen. Was allerdings durch ein
Einzelbild nur sehr unvollkommen wiedergegeben werden kann.
Partikel-Rotation mittels OpenGL-Befehl und variierenden Variablen: Unser Schwarm rotiert mit verschiedenen Geschwindigkeiten um das Zentrum und weicht
dem Betrachter aus. Es ergeben sich dadurch sehr schöne Muster, die bis zu einem
gewissen Grad durchaus an einen echten Fischschwarm erinnern.
Wie man sieht, rotieren die Partikel zwar fleissig um das Zentrum, doch ist ihr Spitze
wieder nicht zur Bewegungsrichtung ausgerichtet. Vielmehr behalten die Partikel
beständig ihre Ursprungs- bzw. letzte Fluchtrichtung bei.
Ausserdem hat sich erwiesen, dass die Betrachter-Detektion teilweise nicht funktioniert.
Der (mögliche) Grund hierfür: Die Teilchen befinden sich durch die OpenGL-Rotation
innerhalb des Modells womöglich an einem anderen Ort, als dem, den wir ursprünglich
für sie berechnet haben. Obwohl also ein Partikel direkt vor dem Betrachter erscheint,
muss die Ausweichbewegung nicht initiiert werden, weil die Partikelposition u.U.
etliche Meter entfernt berechnet wurde.
Teufel aber auch! Ohne die konkrete Berechnung des Orts und der Richtung eines
Partikels funktionieren die bereits entwickelten Mechanismen der Ausrichtung
und Betrachter-Detektion nicht. Also müssen wir uns wohl oder übel doch mit
der Berechnung der Kreisbahn um den Ursprung herum befassen. Doch erneut
stellt sich jetzt die Frage: wie herangehen?
Die nötigen Berechnungen der Kreisbahnen eines Partikels haben mich einen ganzen
Tag gekostet. Es scheint bei OpenGL-Programmieren irgendwie Konsens zu sein,
sich nicht selbst mit diesen Dingen zu befassen, sondern so wie wir zuvor dafür
OpenGL-Kommandos zu verwenden. Daher war im Web auch keine Lösung dafür zu finden.
Erst nach etlichen Stunden kam mir die Idee, dass ein Partikel, welches sich in einer
Kreisbahn bewegt, stets einen Winkel von 90 Grad zum Ursprung halten muss. Befindet
es sich also am Punkt A und soll 1 cm weit fliegen, dann müsste der neue Punkt B
in Flugrichtung und um 90 Grad versetzt zum Ursprung hin liegen. Wir müssen uns
also einen Richtungsvektor ermitteln, der aufaddiert zum Punk A den Punkt B ergibt.
Erneut drohte ich an dem Problem zu scheitern. Folgenden Ansatz hatte ich aber
bereits ausklamüsert: Wir betrachten den Positionsvektor als Richtungsvektor
zum Ursprung. Der Fluchtvektor beschreibt die aktuelle Flugrichtung. Ziehen wir
beide Vektoren voneinander ab, erhalten wir einen neuen Richtungsvektor, der
genau zwischen den beiden liegt. Leider verweist dieser Richtungsvektor jedoch
nicht auf einen Punkt 90 Grad versetzt zum Nullpunkt, sondern wandert bei jeder
Neuberechnung sehr schnell direkt auf den Ursprung zu. Die Folge: Unser Schwarm
implodiert! Das allerdings sieht recht cool aus.
Implosion des Schwarms durch Richtungsvektoren auf den Ursprung: Wird der
neue Richtungsvektor eines jeden Partikels zwischen Ursprungsvektor und aktuellen
Richtungsvektor gelegt, so zeigt er nach ein paar Iterationsschritten direkt auf
den Ursprung. Und der Partikelschwarm wird dann vom Zentrum aufgesogen wie von
einem schwarzen Loch.
Besonders bitter ist, dass ich anfangs schon fast die richtige Idee zur Lösung
des Problems hatte, nämlich einen Richtungsvektor zu finden, der senkrecht auf
dem Positionsvektor steht. Aus meiner "dan_geo_u"-Unit wusste ich, dass man
mithilfe des Kreuzprodukts einen orthogonalen Vektor auf die durch zwei Vektoren
aufgespannte Fläche erhält. Diese Fläche ergibt sich ja aus Positionsvektor
und Fluchtvektor. Doch der aus dem Kreuzprodukt berechnete Vektor stand zwar
senkrecht zur Fläche, wies aber im dreidimensionalen Raum in eine ganz andere
Achse hinein als in die, in der wir uns bewegen mussten. Was wir benötigten, wäre
eigentlich noch einen orthogonalen Vektor zum Kreuzprodukt-Vektor, dann würde die
Richtung wieder stimmen.
Oh Mann! Da hatte ich die Lösung schon im Kopf, schaltete aber einfach nicht.
Und so verbrachte ich erneut etliche Stunden im Web, ohne eine Lösung für
mein Problem finden zu können. Im Gegenteil, dass, was ich fand, behauptete
sogar, den einen gesuchten 90-Grad-Vektor zum Positionsvektor gibt es gar nicht,
weil es deren ja für eine Gerade unendlich viele gibt (ich allerdings fand
nicht einmal einen).
Erst am nächsten Tag stellte ich endlich die richtige Frage an Google, bei
der die entscheidenden Wörtchen "Dreieck" und vor allem "Höhenvektor" vorkamen.
Das führte mich auf diese Seite:
http://www.matheboard.de/archive/11405/thread.html
Hier fragt ein Schüler (?) nach einer Methode zur Berechnung des Höhenvektors.
Und er bekommt prompt kompetente Antwort: Berechne das Kreuzprodukt aus den
Vektoren AB und AC des Dreiecks ABC und du erhältst einen darauf senkrecht stehenden
Vektor v. Bilde nun aus diesem Vektor v und dem Positionsvektor AB erneut das
Kreuzprodukt und es ergibt sich der gesuchte Höhenvektor des Dreiecks ABC.
Aaaaarg!
Kreuzprodukt zweimal anwenden! Natürlich! Genau das habe ich am Vortag
doch sogar selbst angedacht. Allerdings dann dämlicherweise nicht weiterverfolgt.
Daniel, Daniel, du wirst zu unflexibel auf deine alten Tage.
Na, jetzt wollen wir aber wissen, ob das tatsächlich hinhaut. Wir berechnen
also den neuen Fluchtvektor folgendermassen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
center_v:=position_v; //Richtungsvektor Mittelpunkt
dir_v:=escape_v; //aktuelle Richtung
v:=v_cross(center_v,dir_v); //orthogonaler Vektor 1
v:=v_cross(center_v,v); //orthogonaler Vektor 2
v:=v_norm(v); //Einheitsvektor
v:=v_scale(v,-0.05); //Geschwindigkeit 5 cm
escape_v:=v; //neuen Richtungsvektor merken
//neue Position berechnen
position_v:=dan_geo_u.v_add(position_v,v);
Alle Partikel haben zu Beginn eine Richtung erhalten. Diese Richtung
behalten sie nach obiger Formel auch bei, nur dass sie sich jetzt
kreisend um das Zentrum bewegen. Dadurch schwimmen die "Fische" nicht
synchron, sondern in mehreren Schichten in entgegengesetzte Richtungen.
Zudem rotieren sie nicht einfach um die y-Achse, sondern um alle
Achsen gleichermassen, wodurch sie eine Kugel zu bilden scheinen.
Und darüber hinaus weisen alle Partikel auch noch mit der Spitze in
die korrekte Flugrichtung.
Kugelschwarm mit perfekt ausgerichteten Partikeln: Alle Partikel richten
sich über alle Achsen hinweg in einem Winkel von 90 Grad auf den Mittelpunkt aus.
Sie schwimmen dabei nicht synchron, sondern zum Teil in entgegengesetzte Richtungen.
Die Spitze der Partikel weist stets perfekt in Flugrichtung.
Mathematisch korrekt ist das Ganze wohl nicht zu 100%, den man kann beobachten,
dass sich der Kugelschwarm mit der Zeit immer mehr ausweitet. Dies wird uns aber
im Weiteren nicht stören. Schöne Effekte erhält man übrigens auch, wenn man beim
Richtungsvektor vor der Addition zum Positionsvektor jeweils einen Achsenwert auf
null setzt; dann rotieren die Partikel nämlich röhrenförmig statt kugelförmig.
Röhrenschwarm mit perfekt ausgerichteten Partikeln: Beim Richtungsvektor der
Partikel wurde die z-Achse künstlich auf null gesetzt. Dadurch rotieren die Teilchen
nur noch innerhalb ihrer x-/y-Ebene und der Schwarm als Ganzes nimmt die Form einer
Röhre statt einer Sphäre an.
Gestern habe ich noch etwas im Web geforscht und nirgends einen Hinweis dafür
gefunden, dass meine Rotationsformel vom vorherigen Abschnitt irgendwo Anwendung
findet. Nicht einmal die Idee, dass eine Kreisbahn erzwungen wird, wenn sich ein
Teilchen stets in einem Winkel von 90 Grad zum Ursprung bewegt, fand ich bestätigt.
Es ist also anzunehmen, dass das Ganze mathematischer Humbug ist. Ein Humbug
allerdings, der so gut funktioniert, dass wir das Konzept beibehalte.
Bereits erwähnt wurde ja, dass sich der Schwarm während der Rotation fortlaufend
auszudehnen scheint. Um dem entgegenzuwirken, bauen wir nun in die Partikel
eine zusätzliche Bewegung ein, die sie näher an den Ursprung führt. Auch ein
echter Fischschwarm versucht ja instinktiv sein Heil in der Gruppe und verdichtet
diese dadurch, weshalb überhaupt erst ein Schwarm entsteht.
Idealerweise würden sich die Partikel bei der Rotation um einen immer kleiner
werdenden Radius drehen, wodurch automatisch eine Ballung im Zentrum gegeben
wäre. Weil der Radius jedoch in unserer Rotationsformel nirgends so richtig
vorkommt, ausser beim Positionsvektor, fand ich auch nicht die passende Schraube,
an der ich drehen musste, um das Partikel näher zum Mittelpunkt zu führen.
Wir lösen daher das Problem ganz billig dadurch, dass wir den Positionsvektor
eines jeden Partikels permanent etwas verkürzen - theoretisch müsste sich
dann der Schwarm ja früher oder später komplett in der Mitte zusammenballen.
Mal schauen, ob das hinhaut!
00001
00002
00003
00004
00005
00006
[...]
//neue Position berechnen
position_v:=dan_geo_u.v_add(position_v,escape_v);
//Zentrierung zum Mittelpunkt hin
position_v:=v_scale(position_v,0.999);
Na, das war ja einfach. Mit nur einer Programmzeile haben wir unsere Simulation
erheblich verbessern können. Und es macht grossen Spass, sich in dieses wilde
Getümmel der Partikel hineinzustürzen und anschliessend all die zersplitternden,
rotierenden Strukturen zu beobachten, die sich allmählich wieder im Zentrum zu
treffen beginnen.
Konzentration der Partikel zum Mittelpunkt hin: Nach dem Eintauchen des
Betrachters in den Schwarm weichen die Partikel zunächst explosionsartig aus.
Durch Schrumpfung ihres jeweiligen Positionsvektors ballen sie sich jedoch bald
wieder im Zentrum zusammen.
Kritisch anzumerken ist, dass unser Schwarm aktuell nicht mehr allzu viel
Ähnlichkeit mit einem Fischschwarm besitzt. Nein, der Partikelstrom wirkt
nun eher wie eine Vielzahl von Elektronen, die um einen Atomkern kreisen,
und dabei immer wieder verschmelzen und auseinanderplatzen. Der Grund hierfür
ist - vermutlich - schon genannt: Fische sind Partikel der Makrowelt - und diese
verschmelzen nicht miteinander, die beanspruchen Raum für sich selbst. Der nächste
Schritt wird also sein, dieser physikalischen Gesetzmässigkeit Rechnung zu tragen.
Wie gehen wir das an? Eine Idee, die mir einfällt: Die Partikel müssen irgendwie
Informationen darüber gewinnen, wo sich ihre direkten Nachbarn befinden. Kommen
diese zu nahe, muss eine Ausweichbewegung initiiert werden, ähnlich wie bei der
Betrachter-Detektion.
Zunächst ist der naheliegendste Weg praktisch nicht durchzuführen, aufgrund der
hohen Anzahl an Partikel. Nämlich der, dass jedes Partikel vor bzw. nach der eigenen
Positionsberechnung in einer Schleife den Abstand zu allen anderen Partikeln
berechnet, und dem entsprechend sein Verhalten anpasst. Bei 10.000 Partikeln
wären das 10.000 hoch 2, also 100.000.000 Aufrufe von "v_distance" - und das
ist wohl mit keinem herkömmlichen PC in Echtzeit zu realisieren.
Da wir nicht jeden einzelnen Punkt überprüfen können, gehen wir in gewisser
Weise statistisch vor. Wir legen ein virtuelles Gitterraster über die Welt,
repräsentiert durch ein dreidimensionales Integer-Array. Wenn wir nun bei
der Zeichenprozedur alle Partikel durchlaufen, verteilen wir die Partikel
je nach Position in eine passende Gitterbox, indem wir deren Wert um eins
erhöhen. Anschliessend prüfen wir, wie viele Partikel sich bereits in der
Gitterbox befinden. Überschreitet nun diese Anzahl einen gewissen Wert, dann
wird das Partikel statt auf den Ursprung zu einfach von diesem wegbewegt.
Dies müsste zur Folge haben, dass Partikel, die sich zu sehr zusammenballen,
stets wieder voneinander gelöst werden.
Um sich das Ganze etwas besser vorstellen, werden wir zunächst das Gitterraster
in das Modell einzeichnen. Nötig sind dazu folgende Zeilen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
const
_raster_cell_dim=0.5; /0.5 m Kantenlaenge je Box
_raster_radius=4; //0.5 m * 4 = 2 m Radius, also 4 m Wuerfel
type
Tmain_f = class(TForm)
[...]
raster_a:array[-_raster_radius.._raster_radius,-_raster_radius.._raster_radius,-_raster_radius.._raster_radius]of integer;
swarm_radius_max:single;
[...]
end;
implementation
//------------------------------------------------------------
procedure tmain_f.draw_scene;
procedure draw_raster;
procedure draw_it;
var
x,y,z:integer;
begin
for z:=-_raster_radius to _raster_radius do
begin
for x:=-_raster_radius to _raster_radius do
begin
glBegin(GL_LINE_LOOP);
glVertex3f(x*_raster_cell_dim,-_raster_radius*_raster_cell_dim,z*_raster_cell_dim);
glVertex3f(x*_raster_cell_dim, _raster_radius*_raster_cell_dim,z*_raster_cell_dim);
glEnd();
end;
for y:=-_raster_radius to _raster_radius do
begin
glBegin(GL_LINE_LOOP);
glVertex3f(-_raster_radius*_raster_cell_dim,y*_raster_cell_dim,z*_raster_cell_dim);
glVertex3f( _raster_radius*_raster_cell_dim,y*_raster_cell_dim,z*_raster_cell_dim);
glEnd();
end;
end;
end;
begin
glPushMatrix();
glColor3f(1,0,0);
draw_it;
glrotatef(90,1,0,0);
draw_it;
glPopMatrix();
end;
begin
[...]
draw_raster;
draw_particles;
[...]
end;
Die Erläuterung, wie wir die Gitterbox genau erstellen, schenken wir uns hier.
Anhand der Konstanten "_raster_cell_dim" wurde festgelegt, dass jede Gitterbox
eine Kantenlänge von 0.5 m hat. Insgesamt wird ein Rastergitter von 4 Kubikmetern
erzeugt, d.h., es gibt insgesamt (4*2) hoch 3 = 512 Gitterboxen. Im Übrigen werden
Partikel, die sich ausserhalb dieses Bereichs befinden, nicht auf eine
mögliche Zusammenballung hin überprüft, denn die Wahrscheinlichkeit, dass
dies gegeben ist, ist dort relativ gering.
Ein Gitterraster zerteilt die Welt: Die roten Linien zeigen an, wo sich unser
Gitterraster befindet. Jede sich daraus ergebende Gitterbox entspricht dem Feld eines
dreidimensionalen Arrays. Die maximale Anzahl der in einer Gitterbox befindlichen
Partikel wurde gezählt, ebenso der maximale Radius des Schwarms ermittelt. Im linken
Bild haben sich alle 10.000 Partikel in nur einer Gitterbox zusammengeschlossen, da
der Radius nur noch 0.4 m beträgt, eine Gitterbox aber eine Kantenlänge von 0.5 m
besitzt. Im rechten Bild dagegen sind die Partikel deutlich verstreuter anzutreffen.
In der am dichtesten verpackten Gitterbox befinden sich 'nur' noch 192 Partikel,
wie man der Statusinformation im Kopf entnehmen kann.
Wie beschrieben, besteht unser Rastergitter aus 512 Gitterboxen. Um die Dichte
einer Gitterbox zu ermitteln, müssen die Koordinaten der einzelnen Partikel
so umgerechnet werden, dass sie den Array-Koordinaten der zugehörigen Gitterbox
entsprechen. Als Grenzwert der erlaubten Dichte legen wir den Wert 16 x 16 x 1
fest. Dies entspricht 16 Fischen mit 20 cm Länge nebeneinander, die in 16 Schichten
übereinanderliegen. Da wird es bei einer Höhe von 3 cm pro Fisch schon recht
eng in einer Gitterbox von 50 cm Kantenlänge.
Sehen wir uns nun an, wie wir die Füllung der Gitterboxen im Programm realisieren
können:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
function TParticle.calc_position:single;
begin
[...]
//neue position berechnen
position_v:=dan_geo_u.v_add(position_v,escape_v);
//maximalen Radius des Schwarms ermitteln
radius:=v_len(position_v);
if radius>main_f.swarm_radius_max then main_f.swarm_radius_max:=radius;
//befindet sich Partikel innerhalb des Gitterrasters?
if radius<_raster_radius*_raster_cell_dim then
begin
//yep: Rechne Position in Array-Koordinate um
x:=trunc(position_v.x/_raster_cell_dim);
y:=trunc(position_v.y/_raster_cell_dim);
z:=trunc(position_v.z/_raster_cell_dim);
//fuelle zugehoerige Rasterbox
main_f.raster_a[x,y,z]:=main_f.raster_a[x,y,z]+1;
//groesste Dichte merken
if main_f.raster_a[x,y,z]>main_f.raster_density_max then main_f.raster_density_max:=main_f.raster_a[x,y,z];
//pruefe aktuelle Dichte der Rasterbox
if main_f.raster_a[x,y,z]>16*16*1 then
begin
//zu viele Partikel, also Bewegung vom Mittelpunkt weg
position_v:=v_scale(position_v,1+random(20)/10);
exit;
end;
end;
//Zentrierung zum Mittelpunkt hin
position_v:=v_scale(position_v,0.99);
[...]
end;
//------------------------------------------------------------------
procedure tmain_f.draw_scene;
//-----------------------------------------
procedure draw_particles;
var
r:integer;
particle:TParticle;
x,y,z,raster_dim:integer;
begin
//Rastergitter leeren
raster_density_max=0;
swarm_radius_max:=0;
for z:=-_raster_radius to _raster_radius do
for y:=-_raster_radius to _raster_radius do
for x:=-_raster_radius to _raster_radius do raster_a[x,y,z]:=0;
//Schwarm-Partikel malen
for r:=0 to _particle_max-1 do
begin
particle:=particle_a[r];
particle.draw;
end;
//Statistik-Ausgabe
raster_dim:=_raster_radius*2;
raster_dim:=raster_dim*raster_dim*raster_dim;
status_p.caption:=
'Particles: '+inttostr(_particle_max)+' |'+
'Raster: '+inttostr(raster_dim)+' |'+
'Max. Particles/Raster: '+inttostr(r)+' | '+
'Max. Swarm Radius: '+float_to_string(swarm_radius_max);
end;
begin
[...]
draw_particles;
[...]
end;
In der Prozedur "draw_scene" wird "draw_particles" aufgerufen. Dort wird
das komplette Array "raster_a" mit null vorbelegt. Anschliessend werden
die Partikel gezeichnet und danach eine Statistik auf dem Bildschirm
ausgegeben, z.B. der grössten ermittelte Wert der Dichte (Partikel/Raster).
In der Prozedur "calc_position" von TParticle wird der Abstand des Partikels
zum Mittelpunkt ermittelt. Danach wird mithilfe dieses Wertes geprüft, ob
wir uns innerhalb des Rastergitters befinden. Ist dies der Fall, rechnen
wir die Pixel-Koordinaten in Array-Indizes um, indem wir die Fliesskommazahlen
der Positionskoordinaten durch die Grösse einer Rasterbox teilen und anschliessend
die Nachkommastellen verwerfen.
Sehen wir uns das an einmal einem Beispiel an:
00001
00002
00003
00004
00005
Position: (0.3,0.6,0.2)
Geteilt durch die Rasterbox-Dimension: (0.3/0.5,0.6/0.5,2/0.5)
Gecastet auf Integer: Array-Indizes (0,1,4)
Die ermittelte Rasterbox wird um 1 erhöht. Überschreitet ihr Wert die maximal
zulässige Dichte - hier 16*16*1 Partikel - dann wird die Dichte-Flucht initiiert.
Dies bedeutet einfach, dass der aktuelle Positionsfaktor skaliert mit einem Wert
grösser 1 zum neuen Positionsvektor wird. Der Vektor wird also nur etwas verlängert,
wodurch die Partikel sich vom Zentrum wegbewegen.
Schwarm mit verteilter Dichte: Noch immer bewegen sich unsere Partikel
um das Zentrum herum und gleichzeitig auch auf das Zentrum zu. Wird jetzt
jedoch in einer Gitterbox eine zu hohe Dichte an Partikeln festgestellt, so bewegen
sich diese auf zufällige Weise wieder nach aussen. Dadurch wird eine 'unnatürliche'
Zusammenballung der Partikel erfolgreich verhindert.
Bis hierher war ich innerhalb meines Eine-Woche-Urlaubs gekommen. Zu diesem
Zeitpunkt war ich schon recht zufrieden mit meinem Schwarm und wollte
zunächst auch gar nicht mehr weiter daran herumbasteln. Dieser Vorsatz fiel
aber schnell in sich zusammen, als ich bereits zwei Tage später nach Dienstschluss
nichts Rechtes mit mir anzufangen wusste. Also spuckte ich in die Hände und
liess erneut meine flinken Fingerchen Samba tanzen auf der Tastatur.
Das Programmieren ging mir dabei gut von der Hand und der Schwarm wurde noch
sehr stark verbessert. Leider fehlte mir allerdings die Energie, dieses Tutorial
wie bisher parallel zur Entwicklung fortzuschreiben. Aus diesem Grund
beschränken wir uns im Folgenden damit, einige der Verbesserungen, die den
"Evolutionsprozess" überlebt haben, zu erläutern. All die Irrwege, die ich
zwischenzeitlich verfolgte und die teilweise durchaus interessante Ansätze
enthielten, bleiben also leider aussen vor.
Wie wir gesehen haben, wird zu jedem Partikel eine neue Richtung berechnet,
die dann zum Positionsvektor dazu addiert wird. Eine leicht zu realisierende
Möglichkeit war es, hier den Partikeln einen Trägheitszähler zu geben, der
per Zufall auf einen bestimmten Zahlenwert gesetzt wird. Bei jedem
Draw-Ereignis wird der Trägheitszähler dekrementiert. Solange er grösser
als Null ist, findet keine Neuberechnung der Richtung statt. Dadurch bleibt
die alte Richtung bestehen und unser Teilchen verhält sich gewissermassen
träge. Und das bringt ganz nette optische Effekte mit sich.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
type
//-------------------------------------------
TParticle = class(TObject)
public
inertia_steps:integer;
[...]
end;
//------------------------------------------------------------
procedure TParticle.calc_pos;
begin
//inertia-movement?
if inertia_steps>0 then
begin
//hold old direction
result:=dir_v;
exit;
end
//calculate new direction
[...]
//set new inertia
if(random(10)=1) then
begin
inertia_steps:=trunc(get_particle_random(main_f.particle_inertia));
end;
end;
//------------------------------------------------------------
procedure TParticle.draw;
begin
dec(inertia_steps);if inertia_steps<0 then inertia_steps:=0;
calc_pos;
[...]
end;
Trägheit von Partikeln: Auf dem Standbild ist es natürlich nur schwer
zu erkennen, aber alle weiss gefärbten Partikel auf diesem Bild verhalten sich
träge. D.h., sie behalten die einmal ermittelte Richtung für eine zufällige
Zeitdauer bei. Dadurch wird die bisherige etwas stupide Kreisbewegung der
Partikel aufgebrochen, da sie nun ab und an auch einmal eine Gerade fliegen.
An verschiedenen Stellen arbeiten wir bei der Berechnung der Richtung eines
Partikels mit Zufallswerten. Die Berücksichtigung dieser Zufallswerte wird
ab sofort mithilfe eines optionalen Wertes der Individualität bestimmt.
Dabei gilt, dass auf Zufallswerte so weit als möglich verzichtet wird, wenn
die Individualität auf null steht. Die exakt berechnete Werte werden im Gegensatz
dazu umso weniger berücksichtigt, je höher der Grad der Individualität
eingestellt wird. So kann man mit nur einem zentralen Regler recht genau
den Grad an Chaos im Schwarm in der gewünschten Weise variieren.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
procedure TParticle.calc_pos;
//------------------------------------------------
function get_particle_random(v:single):single;
begin
result:=v-(random(main_f.particle_random)/100)*v;
end;
//------------------------------------------------
function get_particle_invers_random(v:single):single;
begin
result:=(random(100-main_f.particle_random)/100)*v;
end;
//------------------------------------------------
function get_rotation_v:tv;
var
speed:single;
begin
//set rotation speed
speed:=get_particle_random(main_f.particle_rotation_speed);
[...]
end;
//------------------------------------------------
function get_center_v:tv;
var
distance,speed:single;
begin
//get distance to middle point
distance:=v_len(pos_v);
//set speed to the middle
speed:=get_particle_invers_random(main_f.particle_center_speed);
[...]
end;
begin
[...]
end;
Hier sind nur zwei Stellen exemplarisch genannt, die auf unsere Zufallsfunktionen
zurückgreifen. "get_particle_random" liefert einen Wert zurück, der maximal "v"
betragen kann, aber je weniger Individualität vorgegeben ist, umso kleiner fällt
der Rückgabewert aus. ""get_particle_invers_random" verhält sich genau umgekehrt.
Je grösser die Individualität, umso kleiner wird das Ergebnis.
Individualismus eines Partikels: Im linken Bild wurde der Individualismus
der Partikel auf null gesetzt - alle Partikel ordnen sich brav zu einer Kugelformation
an. Im zweiten Bild hingegen haben wir 100% Individualismus. Die Teilchen halten
sich kaum mehr an Regeln und erkunden in chaotischer Formation ihr Umfeld.
Bei jedem Draw-Ereignis wird für alle Partikel eine neue Richtung ermittelt
(sofern sie nicht gerade träge sind). Um hierbei zu abrupten - und damit unnatürlichen -
Richtungsänderungen vorzubeugen, wird die neue Richtung dergestalt berechnet, dass
dabei bis zu einem gewissen Grad auch die alte Richtung mit berücksichtigt wird.
Dadurch fliegen die Partikel in deutlich sanfteren Kurven und schlagen nur noch
selten plötzliche Haken.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
procedure TParticle.calc_pos;
var
d:single;
old_dir_v:tv;
begin
//save old direction
old_dir_v:=dir_v;
//calculate new direction
[...]
//calculate new direction: mixture between old and new direction
d:=random(100)/100;
old_dir_v:=v_scale(old_dir_v,d);
dir_v:=v_scale(dir_v,1-d);
dir_v:=v_add(dir_v,old_dir_v);
//set new position
pos_v:=v_add(pos_v,dir_v);
end;
Zu Beginn wird die alte Position in "old_dir_v" gesichert. Dann erfolgt die
Berechnung der neuen Richtung in "dir_v". Am Schluss wird mit "d" ein
Zufallswert zwischen 0 und 1 ermittelt. Die alte Richtung wird mit "d"
skaliert, die neue Richtung mit 1-d. Anschliessend wird durch Vektoraddition
ein Mittelwert aus beiden Vektoren berechnet, der die neue Richtung
vorgibt. Durch den Zufallsfaktor "d" kann es unter Umständen auch passieren,
dass ein Partikel seine Richtung überhaupt nicht ändert - oder aber doch wie
zuvor schon in abrupter Weise. Alles ist möglich, alles ist erlaubt.
Bisher sind wir bei der Dichteflucht so vorgegangen, dass einfach ein
verlängerter Positionsvektor zurückgeliefert wurde, der das Partikel
wieder nach aussen treibt. Dabei gingen jedoch alle zuvor berechneten
Richtungsänderungen verloren, wodurch sich ein etwas statisches Bild
der fliehenden Partikel ergab. Nun berechnen wir aus dem Positionsvektor
einen Richtungsvektor und liefern diese zurück, wobei dieser Richtungsvektor
Einfluss nimmt auf den endgültigen Richtungsvektor, in den auch Rotation,
Zentralisation und Feindesflucht einfliessen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
procedure TParticle.calc_pos;
[...]
begin
rotation_v:=get_rotation_v;
center_v:=get_center_v;
dir_v:=v_add(rotation_v,center_v);
density_v:=get_density_escape_v(x,y,z);
dir_v:=v_add(dir_v,density_v);
enemey_v:=get_enemy_escape_v([...]);
dir_v:=v_add(dir_v,enemy_v);
[...]
pos_v:=v_add(pos_v,dir_v);
end;
Ähnlich wie bei der Trägheit wird auch bei der Dichteflucht ein Zähler
gesetzt, der dafür sorgt, dass die Fluchtrichtung eine Weile eingehalten
wird. Um den Verlauf der gesamten Fluchtbewegung etwas natürlicher zu
gestalten, steigert das Partikel zunächst sein Tempo und bremst dann
ab der Mitte allmählich wieder ab.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
function get_density_escape_v(var x,y,z:integer):tv;
begin
[...]
//density escape: to much particles, move away from middle point
if density_escape_steps>main_f.density_escape_steps/2 then
begin
//first half of escape: speed goes slow to fast
speed:=main_f.density_escape_steps-density_escape_steps;
speed:=speed*speed;
end
else
begin
//last half of escape: speed goes fast to slow
speed:=density_escape_steps*density_escape_steps;
end;
speed:=speed*main_f.density_escape_speed;
speed:=get_particle_random(speed)/10000;
result:=v_norm(pos_v);
result:=v_scale(result,speed);
[...]
end;
Die Praxis hat gezeigt, dass bei einer Verflachung des Schwarms die
Dichteflucht dafür sorgt, dass sich die Teilchen nur innerhalb der
rotierenden Schwarmscheibe nach aussen bewegen, da dies der schnellste
Weg ist, sich vom Mittelpunkt zu entfernen. Ein kleiner Korrekturterm
sorgt dafür, dass bei dieser Flucht auch in die jeweils vernachlässigte
dritte Dimension vorgestossen wird; der fliehende Schwarm bekommt
dadurch mehr Volumen verpasst und sieht damit natürlicher aus. Übrigens
gilt hierbei: Je höher der Grad an Individualität, umso stärker
fällt die Fluchtbewegung aus dem Scheibenverband aus.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
function get_density_escape_v(var x,y,z:integer):tv;
begin
[...]
result:=v_scale(result,speed);
//give escape stream some volume
if abs(result.x)<abs(result.y) then result.x:=get_particle_random(density_escape_steps)*result.x;
if abs(result.y)<abs(result.z) then result.y:=get_particle_random(density_escape_steps)*result.y;
if abs(result.z)<abs(result.x) then result.z:=get_particle_random(density_escape_steps)*result.z;
end;
Verbesserte Dichteflucht: Im linken Bild haben die Partikel wenig
Individualität und neigen unter gewissen Umständen zur Scheibenbildung.
Kommt es dabei zu hohen Dichteverhältnissen in den Gitterboxen, dann fliehen
die Partikel derart nach aussen, dass die Scheibe vergrössert wird. Beim
rechten Bild haben wir es dagegen mit individuellen Partikeln zu tun, die
auch weniger optimale Fluchtwege in Betracht ziehen. Bei Verdichtungen
springen diese geradezu heraus aus der Scheibe - und erinnern damit einmal
mehr an natürliche Fischschwärme. Die Dauer und die Geschwindigkeit der
Fluchtbewegung lassen sich jetzt zudem über Parameter beeinflussen.
Wie weiter oben implementiert, verfügen unsere Partikel über einen "angeborenen"
Fluchtmechanismus, wenn man sich ihnen zu sehr nähert. Sie fliehen
dann explosionsartig davon, und zwar so, dass sie möglichst effektiv
dem potenziellen Angreifer entkommen können.
Leider musste man bisher als Betrachter der Szenerie zuerst in den Schwarm
hineintauchen und dann sofort wieder (rückwärts) herausfliegen,
damit man von dem schönen Schauspiel auch etwas mitbekommen konnte.
Und so entstand der Wunsch, dass man aus einiger Entfernung Bomben
auf den Schwarm abschiessen kann. Dann könnte man in aller Ruhe die
Reaktion geniessen. Ja, durch mehrfaches, gezieltes Schiessen könnte
man dann sogar die Formation des Schwarms in gewünschter Weise
abändern.
Also mussten ein paar Bomben her. Etwas Erfahrung damit habe ich ja schon bei
"OpenGL ISS" sammeln können:
Kapitel 3.9.2. von "OpenGL ISS" - Meteor-Handling.
Wir beschränken uns auf 10 Bomben, deren Eckdaten
jeweils in einem Record "TBullet" gesichert werden. Wie auch der Fischschwarm
werden die Bomben mittels eines Arrays verwaltet. Anders als bei den Partikeln
verwenden wir für die Bomben jedoch OpenGL-Objekte, die wir mit einer Textur
versehen. Aufgrund ihrer geringen Anzahl dürfte dies kaum ein Problem mit der
Performance mit sich bringen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
const
_bullet_max=10;
type
TBullet = record
quad:PGLUquadric;
count:integer;
pos_v:tv;
dir_v:tv;
end;
Tmain_f = class(TForm)
public
bullet_tx:gluint;
bullet_a:array[0.._bullet_max]of TBullet;
[...]
end;
implementation
procedure Tmain_f.FormCreate(Sender: TObject);
begin
[...]
bullet_tx:=img2tx(bullet_img);
for r:=0 to _bullet_max-1 do
begin
bullet_a[r].quad:=gluNewQuadric;
gluQuadricTexture(bullet_a[r].quad,TGLboolean(true));
bullet_a[r].count:=0;
end;
[...]
end;
procedure Tmain_f.FormDestroy(Sender: TObject);
begin
[...]
glDeleteTextures(1,@bullet_tx);
for r:=0 to _bullet_max-1 do gluDeleteQuadric(bullet_a[r].quad);
[...]
end;
procedure Tmain_f.model_frequency_tTimer(Sender: TObject);
begin
[...]
if active_key=vk_space then
begin
//schuss frei?
for r:=0 to _bullet_max-1 do
begin
if bullet_a[r].count>0 then continue;
bullet_a[r].pos_v:=my_pos_v;
bullet_a[r].count:=50;
bullet_a[r].dir_v:=my_look_vector_get;
break;
end;
active_key:=0;
end;
[...]
end;
In "FormCreate" wird "bullet_tx" mit der Textur des Bildes "bullet_img"
verknüpft. Anschliessend wird das TBullet-Array in einer Schleife mit Werten
gefüllt, wobei jeweils die Textur-Eigenschaft aktiviert wird.
In "FormDestroy" wird das TBullet-Array wieder sauber eliminiert.
In "model_frequency_tTimer" wird geprüft, ob die Leertaste betätigt wurde.
Wenn ja, wird in einer Schleife nach der ersten freien Instanz des
TBullet-Arrays gesucht. Wird eine solche Instanz gefunden, wird die
Schussrichtung "dir_v" auf den letzten "my_look_vector_get"-Vektor
gesetzt, der ja exakt in die Richtung zeigt, in die der Benutzer in
unsere virtuelle Welt hineinschaut. Anhand von "bullet_a[r].count" kann
dann später festgestellt werden, ob diese Bombe gerade unterwegs zu
ihrem anvisierten Ziel ist.
Bombe auf ihrem Weg zum Schwarm: Der Benutzer hat die Leertaste betätigt
und damit eine Bombe auf ihren Weg zum Schwarm geschickt. In wenigen Sekunden
wird sie dort mitten hineinbrechen und den Schwarm gehörig in Aufregung
versetzten. Um die Verwirrung noch mehr zu steigern, können sogar bis zu 10
Bomben gleichzeitig abgeschossen werden!
Natürlich müssen wir unsere bisherige Viewer-Escape-Routine nun so erweitern,
dass sie nicht nur auf die Nähe des Betrachters reagiert und eine Fluchtbewegung
des Partikels initiiert, sondern dies auch macht, wenn sich eine Bombe im
Gefahrenbereich befindet.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
procedure TParticle.calc_pos;
function get_enemy_escape_v(distance:single;enemy_pos_v,rotation_v:tv;enemy_ok:bool):tv;
var
speed,len,d:single;
begin
//no enemy escape a standard
result:=v_fill(0,0,0);
//enemy escape speed: bigger when near
speed:=main_f.enemy_escape_distance-distance;
//enemy active and in danger radius?
if enemy_ok and(speed>0) then
begin
//yes: get in escape-mode
inc(main_f.enemy_escape_count);
//calculate escape direction
enemy_escape_v:=dan_geo_u.v_sub(pos_v,enemy_pos_v);
//allow some variations
d:=1-get_particle_random(100)/100;if random(2)=1 then d:=-d;
d:=d/10;
enemy_escape_v.x:=enemy_escape_v.x+d;
enemy_escape_v.y:=enemy_escape_v.y+d;
enemy_escape_v.z:=enemy_escape_v.z+d;
//norm vector for scaling
enemy_escape_v:=v_norm(enemy_escape_v);
//scale vector with speed
enemy_escape_v:=v_scale(enemy_escape_v,speed);
//set enemy escape steps
len:=0.1+get_particle_random(main_f.enemy_escape_steps)/100;
enemy_escape_steps_max:=trunc(len*get_particle_random(main_f.enemy_escape_steps));
enemy_escape_steps:=enemy_escape_steps_max;
//max 1 m/step
enemy_escape_speed:=get_particle_random(main_f.enemy_escape_speed)/100;
enemy_escape_speed_delta:=get_particle_random(enemy_escape_speed/(enemy_escape_steps_max/4));
//deactivate density escape
density_escape_steps:=0;
end;
//particle in escape-mode? if not get out!
if enemy_escape_steps<=0 then exit;
//last part of escape?
if enemy_escape_steps<enemy_escape_steps_max/3 then
begin
//yes: turn around to middle point
//deactivate inertia movement
inertia_steps:=0;
//reached a random radius around middle point?
len:=v_len(pos_v)-random(2)+0.2;
if len<0 then
begin
//yes: stop escape mode
enemy_escape_steps:=0;
exit;
end;
//flight to middle point with some variations
speed:=get_particle_random(len/50);
if random(5)=0 then result:=rotation_v
else result:=v_scale(v_norm(pos_v),-speed);
exit;
end;
//calculate escape speed: get lower with the steps
enemy_escape_speed:=enemy_escape_speed-enemy_escape_speed_delta;
if enemy_escape_speed<0 then enemy_escape_speed:=0;
if enemy_escape_speed<0.1 then enemy_escape_speed:=0.1;
//build in some variations
if random(5)=0 then result:=rotation_v
else result:=v_scale(enemy_escape_v,enemy_escape_speed);
end;
begin
[...]
//viewer escape
if main_f.enemy_escape_viewer_ok then
begin
//calculate distance to user
distance:=dan_geo_u.v_distance(pos_v,main_f.my_pos_v);
v:=get_enemy_escape_v(distance,main_f.my_pos_v,rotation_v,main_f.enemy_escape_viewer_ok);
dir_v:=v_add(dir_v,v);
end;
//escape from bullets
for r:=0 to _bullet_max-1 do
begin
//bullet active? if not then out
if main_f.bullet_a[r].count<=0 then continue;
//calculate distance between particle and bullet
distance:=dan_geo_u.v_distance(pos_v,main_f.bullet_a[r].pos_v);
//calculate direction of escape from enemy
v:=get_enemy_escape_v(distance,main_f.bullet_a[r].pos_v,rotation_v,true);
//adapt actual direction
dir_v:=v_add(dir_v,v);
end;
[...]
end;
Wir befinden uns in Prozedur"calc_pos". Wir haben die Rotation, die
Zentralisation und die Dichteflucht-Richtung zu einer neuen Richtung
gemittelt. Nun wird geprüft, ob sich unser Partikel im Gefahrenbereich
von Feinden befindet.
Zunächst überprüfen wir dies bezüglich des Betrachters. Dazu berechnen
wir in "distance" den Abstand des aktuellen Partikels zum Betrachter.
Diesen Wert übergeben wir dann an die Funktion "get_enemy_escape_v".
In ganz ähnlicher Weise verfahren wir direkt im Anschluss mit den Bomben,
die wir in einer Schleife durchlaufen. Zunächst wird geprüft, ob die Bombe
überhaupt abgeschossen wurde (ihr interner Zähler ist dann grösser null).
Ist dem so, wird wie beim Betrachter "distance" berechnet, jetzt natürlich
auf die Bombe bezogen, und an "get_enemy_escape_v" übergeben. In allen Fällen
erhalten wir einen neuen Richtungsvektor zurück (einen Null-Vektor, wenn
keine Gefahr besteht), der zum bereits bestehenden Richtungsvektor dazu
addiert wird.
In der Funktion "get_enemy_escape_v" wird überprüft, ob der übergebene
"distance"-Wert im kritischen Bereich ist. Ist dem so, wird die
Fluchtbewegung initiiert. Der neue Richtungsvektor wird dabei so
gewählt, dass sich unser Partikel in direkter Linie vom Feind
entfernt. Wir erhalten diesen gesuchten Vektor, indem wir einfach die
Position des Feindes von der Position des Partikels abziehen. Danach
folgt noch etwas Programmierer-Mystik mit der Geschwindigkeit und einige
Variationen mit Zufallswerten, damit die Fluchtrichtung nicht zu
einheitlich wird.
Des Weiteren wird festgestellt, ob wir uns in der Endphase der Flucht
befinden. Ist dem so, entfernt sich das Partikel nämlich nicht weiter
vor seinem Feind, sondern steuert vielmehr wieder auf den für das Partikel
sichersten Ort zu - das Zentrum des Schwarms.
Schwarm flieht vor Bombe: Eine Bombe bewegte sich seitlich am Schwarm
vorbei. Dies hatte zur Folge, dass die rechtsseitigen Partikel nach links
geflohen sind. Je näher sie an der Bombe dran waren, umso schneller sind
sie auseinandergestoben. Dadurch wirkt es nun so, als sei seitlich des Schwarms
eine gewaltige Explosion erfolgt. Alle fliehenden Partikel sind gelb markiert.
Partikel fliehen in den Schwarm: Nachdem die Partikel in der ersten
Phase der Flucht in entgegengesetzter Richtung zu ihrem Feind flohen, kehren
sie in der zweiten Phase verstärkt in die Sicherheit des Schwarms zurück.
Man kann dies z.B. ausnutzen, indem man um die Ränder des Schwarms Bomben
verteilt, wodurch sich die Partikel mit der Zeit immer mehr zu einem dichten
Schwarm zusammenballen. Alle zurückkehrenden Partikel sind grün markiert.
Um (als Programmierer) abschätzen zu können, was die hauptsächliche Motivation eines
Partikel für seine aktuelle Richtung ist, lassen sich diese optional eingefärbt.
Alle Partikel bekommen bei ihrer Erzeugung zunächst einen Farbwert zugeordnet, der
einem Blauton entspricht - dies erschien mir nämlich für Fische als am geeignetsten.
Partikel, die eine träge Bewegung ausführen, werden dagegen weiss wiedergegeben.
Partikel wiederum, die aufgrund zu hoher Dichte aus dem Schwarm entfliehen, werden
rot markiert. Durch äussere Feinde aufgeschreckte Partikel sind panisch gelb - und
sowie sie sich wieder etwas beruhigt haben und zum Schwarm zurückkehren, werden sie
gemütlich grün gemalt. Wenn sie sich dann wieder in einem neutralen Zustand befinden,
nehmen sie erneut ihre originale Blaufärbung an. Da diese Umfärbung für Fische eher
untypisch ist, kann diese Option selbstverständlich auch wieder deaktiviert werden.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
type
TParticle = class(TObject)
public
[...]
r_col,g_col,b_col:single;
end;
implementation
constructor TParticle.create;
begin
[...]
//set color with domination of blue
r_col:=(random(100)+100)/255;
g_col:=(random(100)+120)/255;
b_col:=(random(100)+155)/255;
end;
procedure TParticle.draw;
begin
[...]
//colorization of particles
if main_f.swarm_colored_ok then
begin
color:=1;
if main_f.swarm_inertia_color_ok and(inertia_steps>0) then
begin
//density-escape color red
glColor3f(color,color,color);
end
else
if main_f.swarm_density_escape_color_ok and(density_escape_steps>0) then
begin
//density-escape color red
glColor3f(color,g_col,b_col);
end
else
if main_f.swarm_enemy_escape_color_ok and(enemy_escape_steps>0) then
begin
//enemy-escape color yellow (out) or green (in)
if enemy_escape_steps<enemy_escape_steps_max/2 then glColor3f(color/2,color,b_col)
else glColor3f(color,color,b_col)
end
else
begin
//normal color: blue
glColor3f(r_col,g_col,b_col);
end;
end;
[...]
end;
Umfärbung der Partikel: Bei obigem Bild haben wir alle Farbstufen
beisammen. Am rechten Rand überwiegen die blauen Fische im Normalzustand.
Dazwischen gibt es immer wieder auch weisse Fische, die gerade eine träge
Bewegung ausführen, ihre aktuelle Richtung als für eine Weile beibehalten.
Die gelben Fische unten links sind auf der Flucht vor einer Bombe, während
die grünen Fische in der Mitte die Sicherheit des Zentrums des Schwarms
anstreben. Und ganz oben links sieht man einige rote Partikel - diese
entfliehen gerade einer zu sehr verdichteten Zone im Schwarm.
Ein besonderes Schmankerl habe ich erst sehr spät in den Schwarm einbauen
können: Synchronisation. Lange Zeit fehlte mir nämlich die rechte Idee, wie ich
es umsetzen könnte, dass die Partikel die Neigung entwickeln, sich auch
an der eingeschlagenen Richtung ihrer Nachbarn zu orientieren. Denn wie schon
bei der Dichte-Ermittlung erfahren, ist es schlicht nicht möglich, die Positionen
alle Nachbarn eines Partikels permanent zu ermitteln, zumindest nicht in vertretbarer
Zeit.
Daher verfahren wir jetzt ähnlich wie bei der Berechnung der Dichte:
Wir holen uns die Richtungsvektoren aller Partikel einer Rasterbox und
summiere diese zu einem gemittelten Gesamtvektor auf. Danach muss ein
Partikel nur noch in seiner Rasterbox den entsprechenden Richtungsvektor
entnehmen und davon seine eigene Richtung bis zu einem gewissen Grad
beeinflussen lassen. Und schon haben wir eine Angleichung der Bewegungen
der Partikel erreicht. Im Nachhinein weiss ich eigentlich auch nicht mehr,
warum es mit der Umsetzung so lange nicht klappen wollte.
Über das sagenhafte Ergebnis dieses Verfahrens war ich jedenfalls selbst
äusserst positiv überrascht. Bisher neigte unser Schwarm nämlich stets dazu,
entweder flache Scheiben oder aber kugelrunde Blasen zu bilden.
Durch die Synchronisation der Partikeln mit der gemittelten Richtung
der zugehörigen Rasterbox kamen aber auf einmal sehr schöne und
abwechslungsreiche Muster zustande, die sich permanent ändern und
manchmal abrupt die Richtung ändern, was dem Schwarm letztendlich
doch noch ein sehr natürliches Aussehen verleiht, wie ich finde.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
type
//-------------------------------------------
tdensity = record
count:integer;
dir_v:tv;
end;
Tmain_f = class(TForm)
public
[...]
raster_a:array[
-_raster_radius.._raster_radius,
-_raster_radius.._raster_radius,
-_raster_radius.._raster_radius
]of tdensity;
end;
implementation
procedure TParticle.calc_pos;
function get_density_escape_v(var x,y,z:integer):tv;
begin
[...]
//particle in density raster? if not get out!
if radius>_raster_radius*_raster_cell_dim then exit;
//yes: calculate position to index of raster array
x:=trunc(pos_v.x/_raster_cell_dim);
y:=trunc(pos_v.y/_raster_cell_dim);
z:=trunc(pos_v.z/_raster_cell_dim);
//increment particle counter of the raster box
main_f.raster_a[x,y,z].count:=main_f.raster_a[x,y,z].count+1;
//add particle direction to raster box
main_f.raster_a[x,y,z].dir_v:=v_add(main_f.raster_a[x,y,z].dir_v,dir_v);
[...]
end;
begin
[...]
//density escape
v:=get_density_escape_v(x,y,z);
dir_v:=v_add(dir_v,v);
//particle in a raster box?
if x<>-1 then
begin
//yes: set direction to middle direction of in the raster box
if(random(100)>main_f.particle_random)and(random(100)<=main_f.particle_synch) then
begin
dir_v:=v_add(dir_v,v_scale(main_f.raster_a[x,y,z].dir_v,random(main_f.particle_synch)/500));
end;
end;
[...]
end;
procedure tmain_f.draw_scene;
procedure draw_particles;
begin
//clear raster box
for z:=-_raster_radius to _raster_radius do
for y:=-_raster_radius to _raster_radius do
for x:=-_raster_radius to _raster_radius do
begin
raster_a[x,y,z].count:=0;
raster_a[x,y,z].dir_v:=v_fill(0,0,0);
end;
//draw swarm-particles
glPushMatrix();
glenable(GL_DEPTH_TEST);
glColor3f(0.8,0.8,1);
if random(2)=0 then
begin
for r:=0 to swarm_particles-1 do particle_a[r].draw;
end
else
begin
for r:=swarm_particles-1 downto 0 do particle_a[r].draw;
end;
glenable(GL_DEPTH_TEST);
glPopMatrix();
end;
begin
[...]
draw_particles;
[...]
end;
Während uns vorher für die Dichte ein einfaches mehrdimensionales Array
mit Integer-Werten genügte, müssen wir nun einen Record "TDensity" damit
verwalten. Denn neben der Anzahl der Teilchen einer Rasterbox müssen
wir uns nun auch die Durchschnittsrichtung all dieser Teilchen merken.
Fangen wir unten bei "draw_scene" an. Dort wird die interne Prozedur
"draw_particle" aufgerufen. In dieser mussten wir bisher auch schon
das Raster-Array durchlaufen und die Zähler auf null setzen. Nun löschen
wir bei dieser Gelegenheit auch noch gleich den neu dazugekommenen
Richtungsvektor mit. Die Schleife zur Ausgabe der Partikel kehrt sich
im Gegensatz zu früher periodisch um. Der Grund dafür liegt darin,
dass nicht immer nur die Richtungen der ersten paar Partikel die
Durchschnittsrichtung der zugehörigen Rasterbox dominieren sollen.
Idealerweise hätte man wohl die Partikel in zufälliger Reihenfolge
ausgegeben, doch meine Versuche in dieser Richtung schluckten zu viel
Zeit. Indem wie jetzt die Reihenfolge einfach nur ständig umgekehrt
wird, kommt aber ebenfalls ein mehr als hinreichendes Ergebnis zustande.
In Prozedur "TParticle.calc_pos" wird in gewohnter Weise die Richtung
der Dichteflucht ermittelt. Gleichzeitig werden aber auch die Koordinaten
der zugehörigen Rasterbox zurückgeliefert, sondern das Partikel sich
innerhalb einer solchen befindet. Nun muss nur noch der gemittelte
Richtungsvektor der Rasterbox genommen werden und mit einem Zufallswert
durch Vektoraddition Einfluss auf den bisher ermittelten Richtungsvektor
nehmen.
Übrig bleibt noch die Füllung der Rasterbox mit der Durchschnittsrichtung.
Dies geschieht in Prozedur "get_density_escape_v". Wurde hier festgestellt,
dass sich das Partikel innerhalb einer Rasterbox befindet, wird nun der
aktuelle Richtungsvektor zum Richtungsvektor der Rasterbox dazu addiert.
Bei den ersten Partikeln ist der so generierte Richtungsvektor eigentlich
noch wenig aussagekräftig, aber das ändert sich sehr schnell. Diejenigen
Partikel, die sich am anfänglichen Richtungsvektor orientieren, sind dann
halt Ausreisser, aber von denen gibt es in der Natur ja auch immer ein paar.
Synchronisierung 1 - Partikel-Stränge im Schwarm: Je nachdem, durch welche Rasterbox
die Partikel laufen, verbinden sie sich mit ihren direkten Nachbarn zu verdichteten
Schwärmen innerhalb des Gesamtschwarms. Einige dieser Stränge sind sehr dick,
während andere bisweilen nur ein Partikel nach dem anderen aufweisen.
Synchronisierung 2 - dreidimensionale Schleifen: Ein chaotisch verwirbelter
Schwarm wird durch äussere Einflüsse in seiner Struktur verändert. Rotation und
Zentralisierung, aber auch Dichte- und Feindesflucht sorge so immer wieder dafür,
das sich bestimmte Richtungen durchsetzten, und dadurch im Schwarm pulsierende
Ströme aus Partikeln entstehen, die sich gegenseitig synchronisieren.
Synchronisierung 3 - Brücken und Wirbel: An Kreuzstellen verdichteter
und synchronisierter Stränge kommt es oft zu spontanen Richtungsänderungen der Partikel,
da hier keine Richtung dominiert. Dann treten plötzliche geradlinige Bewegungen auf
und die Partikel schiessen mitten durch das Zentrum des Schwarms, um sich mit dem
Strom der Partikel auf der anderen Seite zu verbinden. Manchmal wirbeln sie auch
aus dem Schwarm heraus, durchlaufen eine Schleife und tauchen dann wieder in den
Hauptschwarm hinein. Eine derartig angelegte Bahn wird mit der Zeit immer
dominanter, bis es vielleicht zu einer Kollision mit einem anderen anwachsenden
Strang kommt - und alle Wege wieder offen sind.
Als wir mit unserem Projekt begonnen haben, das Verhalten eines Schwarms zu
simulieren, wussten wir nicht, ob und wie sich das Ziel erreichen lässt.
Wie fängt man so etwas an? Wie kann man zig tausend Individuen steuern?
Und wie kann man das Ganze dann auch noch grafisch reizvoll wiedergeben?
Wie so oft (bei der Programmierung) half uns hier eine simple Lebensweisheit,
die die folgende Frage beantwortet: "Wie ist man einen Elefanten?" Einen
Bissen nach dem anderen!
Zunächst schufen wir einen virtuellen Raum für den Schwarm mithilfe von
OpenGL. Wir generierten erste, kugelförmige Partikel, die wir in diesem
Raum zufällig verteilten, und die dort unbeweglich stehen blieben. Die
Implementierung einer Steuerung setzte uns in die Lage, diese Partikel
aus allen Richtungen zu betrachten. Als nächste Massnahmen liessen wir
die Partikel auf die Nähe des Betrachters reagieren, indem wir diese von
ihrem "Feind" zurückweichen lassen. Das Aussehen der Partikel wurde
variiert - sie wurden von Kugeln zu Dreiecken. Dreiecke besitzen jedoch
eine Richtung, weshalb wir das vielleicht grösste Problem des Projekts
zu lösen hatten, nämlich die Partikel stets in ihre Bewegungsrichtung
blicken zu lassen. Auch die Rotation war eine harte Nass, die jedoch
ohne Verwendung von komplizierten Formeln, alleine durch wenige
Vektoroperationen realisiert werden konnte. Der Rest - die Zentrierung,
die Dichteflucht, die Bomben, die Feindesflucht und sogar die zuletzt
eingebaute Synchronisation der Partikel - all das war dann vergleichsweise
einfach umzusetzen.
Das Ergebnis mag nicht perfekt sein. So stört mich, dass die Partikel
sich stets nur am Ursprung des Modells orientieren. Echte Fische kennen
aber keinen Ursprung. Die Vermutung liegt nahe, dass diese sich ihren
Mittelpunkt ständig neu suchen, und zwar vermutlich dort, wo das jeweilige
Zentrum ihres (lokalen) Schwarms liegt, d.h. die dichteste Stelle, die sie
optisch erfassen können.
So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy. Ist halt
nichts 100%iges. But who cares?
"OpenGL Swarm" wurde in Delphi 7 programmiert. Im ZIP-File
enthalten ist der vollständige Source-Code, die OpenGL-Unit
"dglOpenGL.pas", die OpenGL-DLL "opengl32" sowie die EXE-Datei.
Das Paket, etwa 760 kB, gibt es hier:
OpenGL-Swarm.zip
Have fun!