OGL_ISS - International Space Station in Danger

OGL_ISS-Tutorial von Daniel Schwamm (18.08.2008)

Inhalt

1. Vorspiel

1.1. Was einen halt so wurmt

Ihr erinnert euch? Beim "OGL_Planets"-Tutorial habe ich bedauert, dass es angesichts der gewaltigen Dimensionen des Sonnensystems nicht möglich war, Objekte darzustellen, deren Ausmasse kleiner als einen Kubikkilometer sind. Denn damit kam der verflixte Tiefenpuffer von OpenGL nicht zurecht. Solche Objekte verschwanden einfach, wenn man sich ihnen zu weit näherte.

Das war schade. Denn gerne hätte ich der Erde neben dem Mond noch einen weiteren Trabanten gegönnt, nämlich die ISS. Klein und unscheinbar sollte sie dort als der entfernteste Aussenposten der Menschheit still ihre Kreise ziehen.

Echt ärgerlich, dass das nicht geklappt hat. Wo sie doch so schön anzuschauen ist:

Delphi-Tutorials - OpenGL ISS - ISS on Wikipedia
Raumstation im Erdorbit: Die ISS im Juni 2008, aufgenommen aus dem Space Shuttle Discovery.
Quelle: Deutsche Wikipedia.

1.2. Deep Coding in the Sky

Zum Glück muss man als Programmierer mit solchem Ärger nicht leben. Wenn es eine ISS sein soll, dann bastelt man sich halt eine. Dazu musste nur der innere Schweinehund überwunden werden.

Wachte dann mal wieder zu früh auf, so um 6 Uhr herum. Passiert mir im Sommer öfter. An Schlaf war nicht mehr zu denken. Und noch zwei elend lange Stunden Zeit, bevor es zur Arbeit ging. Also, was tun?

Wie üblich gab es Kaffee, Kippen und Platzierung am PC. Das Ding habe ich mir "um" mein Zweit-Bett gebaut, was kuschelig-gemütliches Programmieren ermöglicht.

Delphi-Tutorials - OpenGL ISS - My personal workstation at home
Daniels Home-PC: Kaffee, Zigaretten, Bett, Computer und ewig flimmernder Monitor. Hier lässt es sich gemütlich, aber intensiv und störungsfrei arbeiten.

Es ging online. Zu Wikipedia. Zufällige Artikel. Und landete so beim Lemma Internationale Raumstation.

Die ISS. Da ist sie wieder. Da war doch noch was ...

1.3. Meine eigene ISS muss her

Die Kombination aus Erinnerung, neuen Informationen, PC und zu viel Zeit liess mich ein neues Projekt in Delphi beginnen.

Bis es zur Arbeit ging, war die Erde weitgehend fertig. Die ISS eine simple Sphäre. Eine Flugsteuerung noch nicht vorhanden.

Drei Tage später konnte man sich bereits virtuell durch die verschiedenen, 345 km über dem Erdboden schwebenden, Module meiner Raumstation bewegen.

Noch einmal fünf Tage später war die Fliegerei im 3-dimensionalen Raum hinreichend optimiert und alle Elemente mit Texturen versehen.

Nur Herumfliegen und -gucken war aber langweilig. Also wurde ein Spiel eingebaut. Mit Phaserfeuer, Explosionen, Soundeffekten, Level und Highscore-Listen.

Nach zwei Wochen Programmierrausch war die Sache dann gegessen.

Jetzt fehlte nur noch das obligatorische Tutorial ...

2. Aufbau von "OGL_ISS"

Wie schon "OGL_Henrys" und "OGL_Planets" wurde "OGL_ISS" in DelphiGL programmiert.

Das Projekt besteht aus drei Units:

  1. DanGeoU.pas: Funktionssammlung zur Vektor- und Matrizen-Verwaltung
  2. SoundsU.pas: const-Arrays mit den WAV-Daten für die Sound-Effekte
  3. HauptU.pas: Eigentlicher Programm-Code von "OGL_ISS"

2.1. Unit "DanGeoU.pas" - der Mathe-Refresh

Trotz mehrerer OpenGL-Programme auf dem Kerbholz habe ich mich stets gekonnt vor der Mathematik dahinter gedrückt.

Das war bei "OGL_ISS" leider nicht drin. Punktgenaues Navigieren in 3D ohne etwas Ahnung von Trigonometrie ist ... frustrierend. Da kommt man einfach nicht vom Fleck. Ich weiss es genau, denn ich habe Tage damit verschwendet, durch wüstes Trial-and-Error mit Sinus- und Kosinus-Kombinationen irgendetwas Sinnvolles hinzubekommen.

Also hiess es hinsetzen und lernen. Was ist ein Vektor? Was bringen mir Matrizen? Kann ich die für Rotationen einsetzen? Es entstand eine kleine Sammlung von Typen und Funktionen, die in der Unit "DanGeoU.pas" verpackt wurde.

2.1.1. Konstanten und Typen

Fangen wir mit den Konstanten und Typen an:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
unit dangeou;

interface

uses math;

const
  _rad2deg=180/pi;
  _deg2rad=pi/180;

type
  //vector
  Tv=record
    x,y,z:single;
  end;

  //matrix
  Tm=array[0..2,0..2]of single;

2.1.2. Winkel in Grad und Bogenmass

Winkel lassen sich bekanntlich in Grad und im Bogenmass angeben.

Delphi-Tutorials - OpenGL ISS - Angles in degrees and radians
Winkelmasse: Winkel lassen sich im Grad- und Bogenmass angeben. Ein Mass lässt sich aber jederzeit leicht in das andere Mass umrechnen.

Bogenmass-Winkel repräsentieren Längen von Einheitskreisabschnitten. Sie sind mathematisch einfacher zu bearbeiten als Grad-Winkel, da hier keine "künstlichen Grenzwerte" wie etwa der 360 Grad-Winkel berücksichtigt werden müssen.

Das dürfte auch der Grund sein, weshalb bei den meisten OpenGL-Funktionen Bogenmass-Winkel als Parameter erwartet werden. Gleiches gilt für die "DanGeoU.pas". Sollte eine Prozedur dennoch Grad-Winkel erwarten, dann verfügt sie über das Postfix "_deg" im Funktionsnamen.

Mit Hilfe der Konstanten "_rad2deg" und "_deg2rad" können Winkel leicht von einem Format in das andere umgerechnet werden:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
//converts the radian number to the equivalent number in degrees
function rad2deg(rad:single):single;
begin
  result:=rad*_rad2deg;
end;

//converts the degree number to the equivalent number in radians
function deg2rad(deg:single):single;
begin
  result:=deg*_deg2rad;
end;

//converts any degree number to a 'normed' angle between 0-359
function deg_norm(d:single):single;
begin
  if d>360 then d:=d-360;
  if d<0 then d:=360+d;
  if d=360 then d:=0;
  result:=d;
end;

2.1.3. Vektor-Operationen

Der Typ "Tv" beschreibt Vektoren. Einen Vektor kann man als Punkt im n-dimensionalen Raum interpretieren oder als ein vom Ursprung ausgehender Pfeil, der eine Richtung besitzt.

Die Vektoren, die in der "DanGeoU.pas" Verwendung finden, gelten ausschliesslich für n=3, also den 3-dimensionalen Raum mit den Achsen X, Y und Z:

Delphi-Tutorials - OpenGL ISS - xyz-Axis of the OpenGL space
Die x-/y-/z-Achsen des OpenGL-Raumes: Man beachte hierbei, dass die z-Achse bei OpenGL auf den Beobachter hin ausgerichtet ist (anders als bei Direct3D). Will man also einen Punkt tiefer in den Raum hinein wandern lassen, muss der z-Wert der Koordinaten negiert vergrössert werden.

Intern arbeitet OpenGL mit 4-dimensionalen Vektoren, wobei die vierte Dimension, die w-Achse, etwas mit der Projektion des Ergebnisses auf dem Monitor zu tun hat, und uns hier nicht weiter interessieren muss.

Auf Vektoren lassen sich verschiedene, wohldefinierte Operationen anwenden.

2.1.3.1. Addition und Subtraktion von Vektor

Beginnen wir mit Plus und Minus:

Delphi-Tutorials - OpenGL ISS - Addition und subtraction from vectors
Addition und Subtraktion von Vektoren: Geometrische Veranschaulichung der Wirkung von Addition (oben) und Subtraktion (unten) von Vektoren, hier zum leichteren Verständnis nur im zweidimensionalen Raum wiedergegeben.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
//set vector position
function v_fill(x,y,z:single):Tv;
begin
  result.x:=x;
  result.y:=y;
  result.z:=z;
end;

//add two vectors
function v_add(v1,v2:Tv):Tv;
begin
  Result:=v_fill(v1.x+v2.x,v1.y+v2.y,v1.z+v2.z);
end;

//subtract two vectors
function v_sub(v1,v2:tv):tv;
begin
  Result:=v_fill(v1.x-v2.x,v1.y-v2.y,v1.z-v2.z);
end;
2.1.3.2. Länge eines Vektors

Die Länge eines Vektors (oder auch sein Betrag) lässt sich mithilfe des Satzes von Pythagoras ermitteln:

Delphi-Tutorials - OpenGL ISS - Length of a vector
Länge eines Vektors: Die Länge eines Vektors lässt sich mithilfe des Satzes von Pythagoras ermitteln. Der Vektor fungiert dabei als Hypotenuse eines gleichschenkligen Dreiecks.
00001
00002
00003
00004
00005
//length of vector
function v_len(v:Tv):Single;
begin
   result:=sqrt(v.x*v.x+v.y*v.y+v.z*v.z);
end;

Teilt man die Werte eines Vektors durch seine Länge, erhält man den auf den Einheitskreis normierten Vektor. Das ist nützlich, wenn nur die Richtung eines Vektors interessiert, nicht aber seine Position im Raum.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
//normalize vector to length=1
function v_norm(var v:tv):tv;
var
  len:single;
begin
  len:=v_len(v);if len=0 then len:=1;
  result.x:=v.x/len;
  result.y:=v.y/len;
  result.z:=v.z/len;
end;

Um einen Vektor in gegebener Richtung zu verlängern, zu verkürzen oder zu negieren bietet sich die Skalierung an:

00001
00002
00003
00004
00005
00006
00007
//make vector longer/shorter
function v_scale(v:Tv;sf:Single):tv;
begin
  result.x:=v.x*sf;
  result.y:=v.y*sf;
  result.z:=v.z*sf;
end;
2.1.3.3. Winkel zwischen Vektoren

Den Winkel zwischen zwei Vektoren findet man mithilfe der Vektor-Länge (siehe weiter oben), dem Vektor-Punkt-Produkt und einer schicken kleinen Formel:

Delphi-Tutorials - OpenGL ISS - Angle between two vectors
Winkel zwischen zwei Vektoren
Punktprodukt=
 (v1.x*v2.x+v1.y*v2.y)=
 (1*2+2*1)=
 4

Länge v1=
 wurzel(v1.x*v1.x+v1.y*v1.y)=
 wurzel(2*2+1*1)=
 wurzel(5)

Länge v2=v1

Winkel a =
 arcos( 4/(wurzel(5)*wurzel(5)) )=
 arcos( 4/5 ) ~
 36.8 Grad
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
{
  vector-dot-product:
  is needed to calculate the angle between two vectors
}

function v_dot(v1,v2:tv):single;
begin
  result:=v1.x*v2.x+v1.y*v2.y+v1.z*v2.z;
end;

//calculate angle between two vectors
function v_angle(v1,v2:tv):single;
var
  vdot,vlen1,vlen2:single;
begin
  vdot:=v_dot(v1,v2);
  vlen1:=v_len(v1);
  vlen2:=v_len(v2);
  result:=arccos(vdot/(vlen1*vlen2));
end;

function v_angle_deg(v1,v2:tv):single;
begin
  result:=rad2deg(v_angle(v1,v2));
end;
2.1.3.4. Vektor-Kreuz-Produkt

Das Vektor-Kreuz-Produkt ist hilfreich, um die Senkrechte einer Fläche zu ermitteln, die durch zwei Vektoren aufgespannt wird. Die Rechte-Hand-Regel zeigt uns, in welche Richtung die resultierende Senkrechte weisen wird. Es gilt:

Delphi-Tutorials - OpenGL ISS - The surface normal can be calculated width the cross product of a vector in a three-dimensional space
Vektor-Kreuz-Produkt: Mithilfe des Vektor-Kreuz-Produkts kann die Senkrechte der durch zwei Vektoren aufgespannten Fläche im dreidimensionalen Raum berechnet werden.
  • Zeigefinger in v1-Richtung
  • Mittelfinger in v2-Richtung
  • Daumen ergibt Senkrechte
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
{
  vector cross product:
  => normale of the plane given by v1 and v2

  change of v1 and v2 => negative vector

  right hand rule:
    v1=trigger finger
    v2=middle finger,
    => cross=thumb
}

function v_cross(v1,v2:tv):tv;
begin
  result.x:=v1.y*v2.z-v1.z*v2.y;
  result.y:=v1.z*v2.x-v1.x*v2.z;
  result.z:=v1.x*v2.y-v1.y*v2.x;
end;

Für die Rotation eines Vektors um die Achsen des 3-dimensionalen Raumes gelten die folgenden Formeln:

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
//rotate vector about x-axis
function v_rot_x(v:tv;rad:extended):tv;
var
  asin,acos:extended;
begin
  math.SinCos(rad,asin,acos);
  result.x:=v.x;
  result.y:=v.y*acos-v.z*asin;
  result.z:=v.y*asin+v.z*acos;
end;

function v_rot_x_deg(v:tv;deg:extended):tv;
begin
  result:=v_rot_x(v,deg2rad(deg));
end;

//rotate vector about y-axis
function v_rot_y(v:tv;rad:extended):tv;
var
  asin,acos:extended;
begin
  math.SinCos(rad,asin,acos);
  result.x:=v.x*acos+v.z*asin;
  result.y:=v.y;
  result.z:=-v.x*asin+v.z*acos;
end;

function v_rot_y_deg(v:tv;deg:extended):tv;
begin
  result:=v_rot_y(v,deg2rad(deg));
end;

//rotate vector avout z-axis
function v_rot_z(v:tv;rad:extended):tv;
var
  asin,acos:extended;
begin
  math.SinCos(rad,asin,acos);
  result.x:=v.x*acos-v.y*asin;
  result.y:=v.x*asin+v.y*acos;
  result.z:=v.z;
end;

function v_rot_z_deg(v:tv;deg:extended):tv;
begin
  result:=v_rot_z(v,deg2rad(deg));
end;
2.1.3.5. Was haben Sinus und Kosinus mit Vektoren zu schaffen?

Wie erklären sich diese Formeln? Wie kommen zum Beispiel all die Sinus- und Kosinus-Werte in die Berechnungen?

Delphi-Tutorials - OpenGL ISS - Trigonometric sine function
Sinus-Kurve: Die trigonometrische Sinus-Funktion durchläuft den Ursprung bei 0 und hat ihre Minima und Maxima jeweils bei Vielfachen von PI/2.
Delphi-Tutorials - OpenGL ISS - trigonometric cosine function
Kosinus-Kurve: Die trigonometrische Kosinus-Funktion durchläuft den Ursprung bei 1 und hat ihre Minima und Maxima jeweils bei Vielfachen von PI.

Die Trigonometrie lehrt uns folgenden Zusammenhang zwischen den y- und y-Koordinaten, wenn ein Vektor um die z-Achse mit dem Winkel "a" rotiert wird:

Delphi-Tutorials - OpenGL ISS - Rotation of a vector about the z-axis
Rotation eines Vektors um die z-Achse: Wird ein Vektor (x/y) um die z-Achse rotiert, dann ergeben sich die neuen Koordinaten anhand einer Kombination aus Radius (Vektorlänge), Sinus und Kosinus.

Wird nun der Winkel "a" um einen Winkel "b" ergänzt, dann gilt für die neuen Koordinaten "xneu" und "yneu":

Delphi-Tutorials - OpenGL ISS - Extended rotation of a vector about the z-axis
Erweiterte Rotation eines Vektors um die z-Achse: Wird ein Vektor /x/y) um die Winkel 'a' und 'b' rotiert, dann können die neuen Koordinaten (xneu/yneu) mittels einer Formel berechnet werden.

Damit lässt sich nach "xneu" und "yneu" auflösen:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
cos(a) = x/r
sin(a) = y/r

cos(a+b) = cos(a)*cos(b) - sin(a)*sin(b)
sin(a+b) = sin(a)*cos(b) + cos(a)*sin(b)

cos(a+b) = xneu/r
sin(a+b) = yneu/r

-------

xneu = r*             cos(a+b)
     = r*( cos(a)*cos(b) -   sin(a)*sin(b) )
     = r*  cos(a)*cos(b) - r*sin(a)*sin(b)
     = r*  (x/r) *cos(b) - r*(y/r) *sin(b)
     =      x    *cos(b) -    y    *sin(b)

yneu = r*             sin(a+b)
     = r*( sin(a)*cos(b) +   cos(a)*sin(b) )
     = r*  sin(a)*cos(b) + r*cos(a)*sin(b)
     = r*  (y/r) *cos(b) + r*(x/r) *sin(b)
     =      y    *cos(b) +    x    *sin(b)
     =      x    *sin(b) +    y    *cos(b)

Entsprechend kann für die Rotationen um die x- und y-Achse verfahren werden.

Zu beachten ist, dass die Reihenfolge der Rotationen nicht einerlei ist. Es dauerte eine Weile, bis mir das klar war. Die folgenden 90 Grad-Rotationen um verschiedenen Achsen mögen dies illustrieren:

Delphi-Tutorials - OpenGL ISS - Rotation of a vector about the axis y, z and x
Vektorrotation um die Achsen y, z und x: Rotiert man einen Vektor mit jeweils 90 Grad in der Reihenfolge y-, z- und x-Achse, dann weist der Vektor letztlich wieder in die ursprüngliche Richtung.
Delphi-Tutorials - OpenGL ISS - Rotation of a vector about the axis z, y and x
Vektorrotation um die Achsen z, y und x: Rotiert man einen Vektor dagegen mit jeweils 90 Grad in der Reihenfolge z-, y- und x-Achse, dann weist der Vektor in eine andere Richtung als der ursprüngliche Vektor.
2.1.3.6. Distanz zwischen Vektoren

Um die Distanz zwischen zwei "Punkt"-Vektoren zu ermitteln, hilft einmal mehr der Satz von Pythagoras:

00001
00002
00003
00004
00005
00006
00007
00008
00009
//distance between two points
function v_distance(v1,v2:tv):single;
begin
  result:=sqrt(
    (v1.x-v2.x)*(v1.x-v2.x)+
    (v1.y-v2.y)*(v1.y-v2.y)+
    (v1.z-v2.z)*(v1.z-v2.z)
  );
end;

Weil das Rechnen mit Sinus und Kosinus irrationale Zahlen hervorbringt, bleiben Rundungsfehler nicht aus. Werte, die mathematisch eigentlich exakt null sein sollten, behalten dann häufig einen Restwert. Um den Ursprungs-Vektor (0,0,0) trotz enthaltener Restwerte identifizieren zu können, wurde die Prüfungsfunktion "v_iszero" mit Toleranz-Parameter implementiert:

00001
00002
00003
00004
00005
00006
00007
00008
00009
//check if vector is nearly zero
function v_iszero(v:tv;tol:single):boolean;
begin
  result:=false;
  if abs(v.x)>tol then exit;
  if abs(v.y)>tol then exit;
  if abs(v.z)>tol then exit;
  result:=true;
end;

2.1.4. Matrizen-Operationen

Der Typ "Tm" beschreibt eine 3 x 3 Matrix. Matrizen werden in OpenGL verwendet, um Vektoren auf elegante Weise zu rotieren und zu verschieben (dort werden jedoch aufgrund der w-Dimension 4 x 4 Matrizen eingesetzt).

Die folgende Funktion zur Erzeugung einer Einheitsmatrix zeigt, wie die Matrizen in der "DanGeoU.pas" aufgebaut sind:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
{
    1 0 0         m[0,0]=1 m[1,0]=0 m[2,0]=0
  ( 0 1 0 )  <=>  m[0,1]=0 m[1,1]=1 m[2,1]=0
    0 0 1         m[0,2]=0 m[1,2]=0 m[2,2]=1
}

function m_identity:tm;
begin
  result[0,0]:=1;result[1,0]:=0;result[2,0]:=0;
  result[0,1]:=0;result[1,1]:=1;result[2,1]:=0;
  result[0,2]:=0;result[1,2]:=0;result[2,2]:=1;
end;

Für eine Einheitsmatrix gilt im Übrigen, dass sie, multipliziert mit einem Vektor, diesen unverändert lässt.

Mit Nullen füllen kann man die "Tm"-Matrix über die Funktion "m_clr":

00001
00002
00003
00004
00005
00006
00007
00008
00009
//clear matrix
function m_clr:tm;
var
  c,r:integer;
begin
  for r:=0 to 2 do
    for c:=0 to 2 do
      result[c,r]:=0;
end;

In "OGL_ISS" benötigen wir Matrizen zur Achsen-Rotation.

Matrizen haben den Vorteil, dass man sie miteinander multiplizieren kann, wobei ihre Rotationseigenschaften erhalten bleiben. Auf diese Weise lässt sich eine generalisierte Rotationsmatrix bilden, die einen Vektor mit nur einer Multiplikation um alle drei Achsen rotiert.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
Vektor-Rotation                       Matrix-Rotation
------------------------------      ------------------------------
vector = v_rot_x(vector,alpha) <==> vector = m_rot_x(alpha)*vector
vector = v_rot_y(vector, beta) <==> vector = m_rot_y( beta)*vector
vector = v_rot_z(vector,gamma) <==> vector = m_rot_z(gamma)*vector

                                    oder auch:

                                    matrix = m_rot_x(alpha)*
                                             m_rot_y(beta) *
                                             m_rot_z(gamma)
                                    vector = matrix*vector
2.1.4.1. Achsen-Rotationsmatrizen

Die einzelnen Achsen-Rotationsmatrizen sind:

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
//create x-rotation-matrix
function m_fill_rot_x(rad:single):tm;
var
  acos,asin:single;
begin
  asin:=sin(rad);
  acos:=cos(rad);
  result[0,0]:=1;result[1,0]:=   0;result[2,0]:=    0;
  result[0,1]:=0;result[1,1]:=acos;result[2,1]:=-asin;
  result[0,2]:=0;result[1,2]:=asin;result[2,2]:= acos;
end;

function m_fill_rot_x_deg(deg:single):tm;
begin
  result:=m_fill_rot_x(deg2rad(deg));
end;

//create y-rotation-matrix
function m_fill_rot_y(rad:single):tm;
var
  acos,asin:single;
begin
  asin:=sin(rad);
  acos:=cos(rad);
  result[0,0]:= acos;result[1,0]:=0;result[2,0]:=asin;
  result[0,1]:=    0;result[1,1]:=1;result[2,1]:=   0;
  result[0,2]:=-asin;result[1,2]:=0;result[2,2]:=acos;
end;

function m_fill_rot_y_deg(deg:single):tm;
begin
  result:=m_fill_rot_y(deg2rad(deg));
end;

//create z-rotation-matrix
function m_fill_rot_z(rad:single):tm;
var
  acos,asin:single;
begin
  asin:=sin(rad);
  acos:=cos(rad);
  result[0,0]:=acos;result[1,0]:=-asin;result[2,0]:=0;
  result[0,1]:=asin;result[1,1]:= acos;result[2,1]:=0;
  result[0,2]:=   0;result[1,2]:=    0;result[2,2]:=1;
end;

function m_fill_rot_z_deg(deg:single):tm;
begin
  result:=m_fill_rot_z(deg2rad(deg));
end;
2.1.4.2. Multiplikation von Matrizen

Zwei "Tm"-Matrizen multipliziert man folgendermassen:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
//multiplication of two matrices
function m_mul(m1,m2:tm):tm;
begin
  result[0,0]:=m1[0,0]*m2[0,0]+m1[1,0]*m2[0,1]+m1[2,0]*m2[0,2];
  result[1,0]:=m1[0,0]*m2[1,0]+m1[1,0]*m2[1,1]+m1[2,0]*m2[1,2];
  result[2,0]:=m1[0,0]*m2[2,0]+m1[1,0]*m2[2,1]+m1[2,0]*m2[2,2];

  result[0,1]:=m1[0,1]*m2[0,0]+m1[1,1]*m2[0,1]+m1[2,1]*m2[0,2];
  result[1,1]:=m1[0,1]*m2[1,0]+m1[1,1]*m2[1,1]+m1[2,1]*m2[1,2];
  result[2,1]:=m1[0,1]*m2[2,0]+m1[1,1]*m2[2,1]+m1[2,1]*m2[2,2];

  result[0,2]:=m1[0,2]*m2[0,0]+m1[1,2]*m2[0,1]+m1[2,2]*m2[0,2];
  result[1,2]:=m1[0,2]*m2[1,0]+m1[1,2]*m2[1,1]+m1[2,2]*m2[1,2];
  result[2,2]:=m1[0,2]*m2[2,0]+m1[1,2]*m2[2,1]+m1[2,2]*m2[2,2];
end;
2.1.4.3. Multiplikation von Matrizen mit Vektoren

Und zuletzt haben wir noch die Multiplikation einer "Tm"-Matrix mit einem "Tv"-Vektor:

00001
00002
00003
00004
00005
00006
00007
//multiplication of a matrix with a vector
function m_v_mul(m:tm;v:tv):tv;
begin
  result.x:=m[0,0]*v.x+m[1,0]*v.y+m[2,0]*v.z;
  result.y:=m[0,1]*v.x+m[1,1]*v.y+m[2,1]*v.z;
  result.z:=m[0,2]*v.x+m[1,2]*v.y+m[2,2]*v.z;
end;

2.2. Unit "SoundsU.pas" - nie wieder Ressourcen-Editor

2.2.1. TImage-Komponente für Bilder in der Applikation

Um (JPG-)Bilder in ein Delphi-Programm zu integrieren, genügt es, sie mittels TImage im Objektinspektor einzuladen. Kompiliert man dann das Projekt, sind die Daten des Bildes im laufenden Programm direkt verfügbar, müssen also nicht erst von der Festplatte nachgeladen werden.

2.2.2. Doch was ist mit Sounds?

Leider existiert keine Klasse TSound, mit der man Gleiches für WAV-Daten realisieren könnte. Üblicherweise muss daher der Delphi-eigene Ressourcen-Editor bemüht werden, um WAV-Daten in eine EXE-Datei integrieren zu können.

2.2.3. Bytes sind Bytes sind Bytes

Da dieses Verfahren recht kompliziert ist, habe ich mir ein kleines Tool namens "File2Array" geschrieben, welches eine beliebige Datei ausliest und als konstantes Delphi-Array wiedergibt. Diese Daten können dann einfach in eine Unit kopiert und wie normaler Source kompiliert werden.

Folgendes Beispiel soll dies demonstrieren:

Wir haben die Text-Datei "hallo.txt" auf Festplatte gespeichert:

00001
Hallo, Welt!

Daraus macht dann "File2Array.exe" folgende Text-Datei:

00001
00002
00003
00004
00005
const
  _hallo:array[0..11] of byte=(
     72, 97,108,108,111, 44, 32, 87,101,108,116, 33
  );
  // H   a   l   l   o    ,      W   e   l   t    !

Da unter Windows letztlich alle Daten eine Folge von Bytes sind, funktioniert diese Methode mit jeder Art von Datei, egal, ob sie nun Bilder, Sounds, Texte oder Filme enthält.

2.2.4. Eine eigene Unit nur für Sounds

Auch wenn es eigentlich nicht nötig ist, wurden die drei so generierten WAV-Datenströme bei "OGL_ISS" in eine eigene Unit "SoundsU.pas" gelegt. Das Ganze sieht dann in etwa so aus:

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
unit soundsu;

interface

const
  _sound_explode:array[0..107789] of byte=(
     82, 73, 70, 70,  6,165,  1,  0, 87, 65, 86, 69,102,109,116, 32, 16,  0,  0,  0,
      1,  0,  1,  0, 17, 43,  0,  0, 34, 86,  0,  0,  2,  0, 16,  0,100, 97,116, 97,
  [...]
    103,  0, 23,  1,204,  1,131,  2, 67,  3,  3,  4,202,  4,152,  5,108,  6, 67,  7,
     28,  8, 92,  8, 91,  8, 88,  8, 88,  8
  );

  //----------------------------------------------------------------
  _sound_shot:array[0..111453] of byte=(
     82, 73, 70, 70, 86,179,  1,  0, 87, 65, 86, 69,102,109,116, 32, 16,  0,  0,  0,
      1,  0,  1,  0, 17, 43,  0,  0, 34, 86,  0,  0,  2,  0, 16,  0,100, 97,116, 97,
  [...]
      3,  0,  2,  0,  3,  0,  2,  0,  3,  0,  0,  0,  2,  0,  1,  0,  1,  0,  2,  0,
      0,  0,  1,  0,  1,  0,  1,  0,  0,  0,  0,  0,  1,  0
  );

  //-----------------------------------------------------------------------
    _sound_alarm:array[0..30905] of byte=(
     82, 73, 70, 70,178,120,  0,  0, 87, 65, 86, 69,102,109,116, 32, 16,  0,  0,  0,
      1,  0,  1,  0, 34, 86,  0,  0, 68,172,  0,  0,  2,  0, 16,  0,100, 97,116, 97,
  [...]
     97, 58, 50, 65, 52, 67,228, 76, 73, 78,  8, 59,208, 24, 73,246, 36,216, 57,194,
     71,196, 33,180,147,213
  );


implementation

end.

Wie wir letztlich diese Array-Daten im Programm nutzen können, sehen wir gleich noch.

2.3. Unit "HauptU.pas" - All in one

Der eigentliche Programm-Code von "OGL_ISS" befindet sich komplett in der Unit "HauptU.pas". Schöner wäre es wohl gewesen, mehrere Units zu verwenden, etwa eine für die Steuerung, eine für die Spielfunktionen und eine für das Rendern. Jedoch ist der Quellcode insgesamt so knapp ausgefallen, dass das gar nicht nötig war.

Die Applikation begnügt sich zudem mit nur einer TForm, auf der sich alles abspielt. Hier ein Screenshot der "hauptf":

Delphi-Tutorials - OpenGL ISS - Delphi main form plus components
Delphi-Form 'thauptf': Die Hauptform von 'OpenGL ISS'. Hier spielt sich alles ab. Edits, Bildcontainer, Panels, Labels, Timer und alles Restliche, was zur Anzeige nötig ist.

3. Programmierung von "OGL_ISS"

3.1. Alle nötigen Units

Die in das Projekt eingebundenen Units sind:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
unit hauptu;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs,ExtCtrls, StdCtrls, ComCtrls,
  XPMan,Math,JPEG,IniFiles,MMSystem,
  DGLOpenGL,
  DanGeoU,SoundsU;

Neben den Standard-Units gibt es "XPMan" (XPManifest) für die schönere Optik, "Math" für trigonometrische Funktionen, "JPEG" für JPG-Bilder, "IniFiles" für Konfigurationsdateien und "MMSystem" für die Sound-API.

Die OpenGL-Funktionalität wird über "DGLOpenGL" eingebunden. Anders als bei meinen vorherigen OpenGL-Projekten werden die Units "SDL" und "SDL_Image" hier nicht benötigt.

Zuletzt werden dann noch meine eigenen, weiter oben beschriebenen Units "DanGeoU" und "SoundsU" hinzugefügt.

3.2. Konstanten und Variablen

Die verwendeten Konstanten, Typen und "globalen" Variablen sind:

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
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
const
  _cap='OGL_ISS V1.0 2008 by Daniel Schwamm';
  _inifn='ogl_iss.ini';

  //earth----------------------------------------
  _earth_r      =6368000;
  _earth_y      =-_earth_r-345000;
  _earth_drot   =360/(90*60*10); //one rotation in 90 min

  //home parameter
  _home_pos_z=200;
  _iss_len=94;

  //space dimension--------------
  _space_r      =2*_earth_r;

  //speed factors-------------------
  _pstep=0.6; //flight speed adder
  _rstep=0.6; //rotation speed adder

  _pspeed_max=30;
  _rspeed_max=10;

  //depth buffer
  _NearClipping=1;
  _FarClipping=-1;

  //game
  _game_level_train   =1;
  _game_level_rookie  =2;
  _game_level_advanced=3;
  _game_level_profi   =4;
  _game_level_mad     =5;

  //number of same-time shots-----------
  _shot_c=50;

  //meteor----------------
  _meteor_max=50;
  _meteor_r=20;
  _meteor_state_none   =0;
  _meteor_state_fly    =1;
  _meteor_state_explode=2;
  _meteor_alarm=100;

  //path-------------------
  _path_c=2000;

type
  //meteor record-----------
  pmeteor=^tmeteor;
  Tmeteor=record
    posv:tv;
    radius,speed:single;
    state:byte;
    count:integer;
  end;

  //shot-record--------------
  pshot=^tshot;
  Tshot=record
    posv,poslastv,dirv:tv;
    count:integer;
  end;

  Thauptf = class(TForm)
    [...]
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
    homedir:string;

    //opengl environment
    handle_dc:HDC;
    handle_rc:HGLRC;

    //my position and axis-angles
    posv,dposv:tv;
    rotv:tv;

    //speed values: fly and axis-rotation
    pspeed,rx_speed,ry_speed,rz_speed:single;

    //some flags & counters
    draw_visor_ok:bool;
    draw_path_ok:bool;
    flyhome_c:integer;

    //game parameters-----------
    game_iss_meteor,
    game_meteor_dim,
    game_points,
    game_hs_rookie,
    game_hs_advanced,
    game_hs_profi,
    game_hs_mad:integer;
    game_level,
    game_meteor_c:byte;
    game_meteor_speed:single;
    game_status:string;

    //sounds
    sound_shot:TMemoryStream;
    sound_explode:TMemoryStream;
    sound_alarm:TMemoryStream;
    sound_c:integer;
    sound_ok:bool;

    //time-counters-----------
    frequ_low,frequ_fast:single;

    //key holder----------------
    akey:word;
    actrl:bool;

    //grapcic-mode
    graphic_mode:byte;
    graphic_txok,
    graphic_earthok,
    graphic_spaceok:bool;

    //space----------------
    space_quad:PGLUquadric;
    space_tx:gluint;

    //earth----------------
    earth_quad:PGLUquadric;
    earth_tx:gluint;
    earth_rot:single;

    //iss---------------------
    iss_quad:PGLUquadric;
    iss_startv,iss_endv:tv;
    iss_big_tx:gluint;
    iss_big_sail_tx:gluint;
    iss_small_tx:gluint;
    iss_small_sail_tx:gluint;
    iss_small_sail_rot:single;
    iss_big_sail_rot:single;

    //shot--------------------------------
    shot_a:array[0.._shot_c]of tshot;
    shot_quad:PGLUquadric;
    shot_tx:gluint;
    shot_c:integer;

    //meteor------------------------------
    meteor_a:array[0.._meteor_max-1]of tmeteor;
    meteor_imga:array[0.._meteor_max-1]of timage;
    meteor_quad:PGLUquadric;
    meteor_tx:gluint;
    meteor_explode_tx:gluint;

    //path----------------------------------
    path_a:array[0.._path_c]of tv;
    path_c:integer;

    [...]
  end;

Eine Erläuterung erspare ich mir. Die Liste dient nur als Referenz-Liste, wenn bei der Source-Beschreibung weiter unten diese Bezeichner auftauchen.

3.3. Form-Zeugs

3.3.1. Erzeugung

Wie bei Delphi üblich, wird nach dem Programmstart automatisch die Prozedur "FormCreate" 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
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
//create form-------------------------------------------------------
procedure Thauptf.FormCreate(Sender: TObject);
var
  r:integer;
  p:tpanel;
  img:timage;
begin
  homedir:=extractfilepath(application.exename);
  caption:=_cap;
  randomize;
  frequ_low:=0;
  frequ_fast:=0;
  sound_c:=0;

  //preparing sounds-------------------
  sound_shot:=TMemoryStream.Create;
  sound_shot.writeBuffer(_sound_shot,sizeof(_sound_explode));
  sound_explode:=TMemoryStream.Create;
  sound_explode.writeBuffer(_sound_explode,sizeof(_sound_shot));
  sound_alarm:=TMemoryStream.Create;
  sound_alarm.writeBuffer(_sound_alarm,sizeof(_sound_alarm));

  //set panels all same style
  for r:=0 to componentcount-1 do begin
    if not(components[r] is tpanel)then continue;
    p:=tpanel(components[r]);
    p.parentbackground:=false;
    p.BevelOuter:=bvnone;
    p.color:=clblack;
    p.font.color:=clyellow;
  end;

  //arrange components
  backp.align:=alclient;
  viewp.align:=alclient;
  footimg.align:=alclient;
  gamestartimg.Align:=alclient;
  gamestartimg.picture.assign(footimg.Picture);
  gamestartp.visible:=false;
  gameoverimg.Align:=alclient;
  gameoverimg.picture.assign(footimg.Picture);
  gameoverp.visible:=false;
  meteorimg.align:=alclient;

  //create meteor-counter-images
  for r:=0 to _meteor_max-1 do begin
    img:=timage.Create(meteorcp);
    //img.Proportional:=true;
    img.stretch:=true;
    img.Picture.assign(meteorimg.picture);
    img.align:=alleft;
    img.Width:=meteorcp.Width div _meteor_max;
    img.height:=meteorcp.Height;
    img.parent:=meteorcp;
    img.Visible:=true;
    meteor_imga[r]:=img;
  end;

  //intialize opengl
  handle_DC:=GetDC(viewp.Handle);
  if not InitOpenGL then Application.Terminate;
  handle_RC:=CreateRenderingContext(handle_DC,[opDoubleBuffered],24,32,0,0,0,0);
  ActivateRenderingContext(handle_DC,handle_RC);
  glenable(GL_DEPTH_TEST);

  //read ini parameters
  with tinifile.create(homedir+_inifn) do begin
    draw_visor_ok:=readbool('param','draw_visor_ok',true);
    draw_path_ok:=readbool('param','draw_path_ok',false);
    footp.visible:=readbool('param','draw_cockpit_ok',true);
    sound_ok:=readbool('param','sound_ok',true);
    graphic_mode:=readinteger('param','graphic_mode',7);
    game_hs_rookie:=readinteger('param','game_hs_rookie',0);
    game_hs_advanced:=readinteger('param','game_hs_advanced',0);
    game_hs_profi:=readinteger('param','game_hs_profi',0);
    game_hs_mad:=readinteger('param','game_hs_mad',0);
    frequencytb.position:=readinteger('param','frequencytb',frequencytb.position);
    free;
  end;

  //create objects
  initobjects;

  //start action
  self.WindowState:=wsmaximized;
  frequencyt.enabled:=true;
  game_start_show;
end;

Schauen wir uns mal an, was bei dieser Prozedur so alles passiert.

3.3.2. WAV-Arrays in die Boxen gedrückt

Zu Beginn werden die konstanten WAV-Arrays aus der "SoundsU.pas" in TMemoryStream-Objekte eingelesen. Diese lassen sich später an die Prozedur "dosound" übergeben, wo sie letztlich über die API-Funktion "sndPlaySound" abgespielt werden:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
//-----------------------------------------------------
procedure thauptf.dosound(ms:tmemorystream);
var
  flags:cardinal;
begin
  if not sound_ok then begin
    sndPlaySound(nil,0);
    exit;
  end;

  if sound_c>0 then exit;
  if gameoverp.visible then exit;

  flags:=SND_MEMORY or SND_ASYNC;

  if ms=nil then begin
    sndPlaySound(nil,0);
    exit;
  end;

  sndPlaySound(ms.memory,flags);
  sound_c:=3;
end;

"sound_c" ist ein Zähler, der angibt, wie lange (das heisst, wie viele "Zeiteinheiten") ein Sound mindestens abgespielt wird, bevor er durch einen anderen ersetzt werden darf. Für Multi-Sound-Effekte hätte ich mich sonst mit der DirectSound-API herumschlagen müssen - und dann wäre der Source gewiss umfangreicher ausgefallen.

3.3.3. Designarbeiten

Anschliessend werden alle TComponents der TForm "hauptf" durchlaufen und nach TPanels durchsucht. Wird ein TPanel gefunden, so wird es u.a. schwarz eingefärbt und die Eigenschaft "parentbackground" wird auf "false" gesetzt. Das ist notwendig wegen eines lästigen Bugs des XP-Manifests - ohne diese Korrektur "merkt" sich TPanel sonst seine Background-Farbe nicht.

Über eine weitere Schleife wird das TImage-Array "meteor_imga" gefüllt. Dazu wird pro Meteor ein TImage erzeugt, mit dem TPicture aus TImage "meteorimg" versehen und auf dem TPanel "meteorcp" abgelegt. Dort sind später alle noch im Spiel vorhandenen Meteore aufgelistet. Wird ein Meteor abgeschossen, so verschwindet sein zugehöriges Bild einfach vom TPanel.

Jetzt wird die OpenGL-Umgebung initialisiert. Kommt es dabei zu Problemen, bricht das Programm ab. Als Ziel der Grafikausgabe wird das TPanel "viewp" definiert.

Es folgt die Routine zum Einlesen der Programm-Parameter aus dem Konfigurationsfile "ogl_iss.ini". Hier wird u.a. festgehalten, welcher Grafik-Modus aktiv ist, ob Sound zu hören ist oder nicht, oder ob das Cockpit angezeigt werden soll. Des Weiteren sind hier die Highscores der vier Spiel-Level "rookie", "advanced", "profi" und "mad" abgelegt.

Danach wird die Prozedur "initobjects" aufgerufen, die wir uns gleich näher ansehen werden.

Mittels "self.WindowState:=wsmaximized" wird das Programm-Fenster maximiert. Das hat einige weitere Designarbeiten zur Folge. Ausserdem wird dabei die OpenGL-Umgebung auf die neuen Bildausgabe-Verhältnisse angepasst:

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
//----------------------------------------------------------------
procedure Thauptf.viewpResize(Sender: TObject);
begin
  if not visible then exit;

  //center cockpit
  cockpitp.Left:=(footp.width-cockpitp.Width)div 2;
  cockpitp.top:=(footp.height-cockpitp.height)div 2;

  //center game start panel
  gamestartp.Left:=(viewp.width-gamestartp.Width)div 2;
  gamestartp.top:=(viewp.height-gamestartp.height)div 2;

  //center game over panel
  gameoverp.Left:=(viewp.width-gameoverp.Width)div 2;
  gameoverp.top:=(viewp.height-gameoverp.height)div 2;

  //ogl-adaptionen
  glViewport(0,0,viewp.ClientWidth,viewp.ClientHeight);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity;
  gluPerspective(
    50,
    viewp.ClientWidth/viewp.ClientHeight,
    _NearClipping,
    _FarClipping
  );
  glMatrixMode(GL_MODELVIEW);
  Draw_Scene;
end;

Anschliessend wird der TTimer "frequencyt" aktiviert, wodurch die sich periodisch wiederholende Grafikausgabe begonnen wird.

Und zuletzt wird dann noch die Prozedur "game_start_show" aufgerufen.

3.3.4. Form-Zerstörung

Das Gegenstück zu "FormCreate" ist "FormDestroy", welches beim Beenden des Programms ebenfalls automatisch aufgerufen wird. Hier werden alle Ressourcen wieder freigegeben und einige Programm-Parameter sowie die (aktualisierten) Highscore-Listen in die Konfigurationsdatei geschrieben:

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
//clear program-------------------------------------------------------
procedure Thauptf.FormDestroy(Sender: TObject);
var
  r:integer;
begin
  //write ini parameters
  deletefile(homedir+_inifn);
  with tinifile.create(homedir+_inifn) do begin
    writebool('param','draw_path_ok',draw_path_ok);
    writebool('param','draw_cockpit_ok',footp.visible);
    writebool('param','draw_visor_ok',draw_visor_ok);
    writebool('param','sound_ok',sound_ok);
    writeinteger('param','graphic_mode',graphic_mode);
    writeinteger('param','game_hs_rookie',game_hs_rookie);
    writeinteger('param','game_hs_advanced',game_hs_advanced);
    writeinteger('param','game_hs_profi',game_hs_profi);
    writeinteger('param','game_hs_mad',game_hs_mad);
    writeinteger('param','frequencytb',frequencytb.position);
    free;
  end;

  //free textures
  glDeleteTextures(1,@space_tx);
  glDeleteTextures(1,@earth_tx);
  glDeleteTextures(1,@iss_big_tx);
  glDeleteTextures(1,@iss_big_sail_tx);
  glDeleteTextures(1,@iss_small_tx);
  glDeleteTextures(1,@iss_small_sail_tx);
  glDeleteTextures(1,@meteor_tx);
  glDeleteTextures(1,@meteor_explode_tx);

  //free quads
  gluDeleteQuadric(earth_quad);
  gluDeleteQuadric(iss_quad);
  gluDeleteQuadric(space_quad);
  gluDeleteQuadric(meteor_quad);

  //free meteor-counter-images
  for r:=0 to _meteor_max-1 do meteor_imga[r].Free;

  //free OpenGL environment
  DeactivateRenderingContext;
  DestroyRenderingContext(handle_RC);
  ReleaseDC(viewp.Handle,handle_DC);

  //free sounds
  sound_alarm.Free;
  sound_shot.Free;
  sound_explode.free;
end;

3.4. Objekte und Texturen initialisieren

3.4.1. Objekte

Wie wir gesehen haben, wird in "FormCreate" die Prozedur "initobjects" 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
//initialize objects----------------------------------------------------
procedure thauptf.initobjects;
begin
  //space-----------------------------------------------
  space_quad:=gluNewQuadric;
  gluQuadricTexture(space_quad,TGLboolean(true));
  space_tx:=img2tx(spaceimg);

  //earth-----------------------------------------------
  earth_quad:=gluNewQuadric;
  gluQuadricTexture(earth_quad,TGLboolean(true));
  earth_tx:=img2tx(earthimg);

  //ISS-------------------------------------------------
  iss_quad:=gluNewQuadric;
  gluQuadricTexture(iss_quad,TGLboolean(true));
  iss_big_tx:=img2tx(issbigimg);
  iss_big_sail_tx:=img2tx(issbigsailimg);
  iss_small_tx:=img2tx(isssmallimg);
  iss_small_sail_tx:=img2tx(isssmallsailimg);
  iss_startv:=v_fill(0,0,-_iss_len/2);
  iss_endv:=v_fill(0,0,_iss_len/2);
  iss_small_sail_rot:=0;
  iss_big_sail_rot:=0;

  //meteors-----------------------------------------------
  meteor_quad:=gluNewQuadric;
  gluQuadricTexture(meteor_quad,TGLboolean(true));
  meteor_tx:=img2tx(meteorimg);
  meteor_explode_tx:=img2tx(meteorexpimg);
end;

Hier werden "gluQuadric"-Objekte erzeugt. Solche Objekte lassen sich mit bestimmten Eigenschaften versehen und später an diverse OpenGL-Funktionen übergeben.

3.4.1.1. Weltallkugel

Das "gluQuadric"-Objekt "space_quad" wird zum Beispiel später als Kugel dargestellt, an deren Mittelpunkt sich die ISS befindet. Sie ist so gross, dass sie als Art "Himmelszelt" fungiert. Egal, in welche Richtung wir später auch schauen, wir sehen stets die Innenwände dieser Kugel im Hintergrund. Über die Funktion "img2tx" wird eine passende Textur an das Objekt gebunden. Wie das genau funktioniert, sehen wir uns gleich an.

Delphi-Tutorials - OpenGL ISS - Texture of the space
Textur von 'space_quad': Ein 256 x 128 Pixel grosses JPG-Bild für den Weltraum.
Delphi-Tutorials - OpenGL ISS - Demonstration of the texture of the space
Einsatz der Space-Textur: Der Weltraum um uns herum ist nicht einfach schwarz, sondern von blau leuchtenden Nebelbändern durchzogen.
3.4.1.2. Erdkugel

Ähnlich wie "space_quad" ist auch "earth_quad" konzipiert. Hierbei handelt es sich um den "Zeiger" auf die Erdkugel, die 345 km unter der ISS um ihre eigene Achse rotiert. Sie hilft uns bei der Orientierung im Raum, wenn wir später um die ISS herumfliegen. Obgleich natürlich solche Dinge wie oben und unten im schwerelosen Zustand nicht wirklich eine Rolle spielen.

Delphi-Tutorials - OpenGL ISS - Texture of the Earth
Textur von 'earth_quad': Ein geometrisch angepasste Grafik mit den Kontinenten und Ozeanen, 512 x 512 Pixel gross - und damit die mit Abstand grösste verwendete Textur.
Delphi-Tutorials - OpenGL ISS - Demonstration of then texture of the Earth
Einsatz der Earth-Texture: Die blaue Heimat, rotierend unter der ISS, mit seinen einigermassen korrekt wiedergegebenen Strukturen.
3.4.1.3. ISS

Die ISS selbst verfügt über das "gluQuad"-Objekt "iss_quad". Dieses wird später beim Rendern mehrfach verwendet, für jedes Einzelteil der ISS erneut. Ausmasse und Texturen werden dabei jeweils zuvor angepasst. Die verwendeten Texturen sind:

Delphi-Tutorials - OpenGL ISS - Texture of the big body of the BIG
Textur 'iss_big_tx': Grafik für den Hauptkörper der ISS.
Delphi-Tutorials - OpenGL ISS - Texture of the big sails of the ISS
Textur 'iss_big_sail_tx': Grafik für die grossen Segel der ISS.
Delphi-Tutorials - OpenGL ISS - Texture of the small gondolas of the ISS
Textur 'iss_small_tx': Grafik für die kleinen Gondeln der ISS.
Delphi-Tutorials - OpenGL ISS - Texture of the small sails of the ISS
Textur 'iss_small_sail_tx': Grafik für die kleinen Gondeln der ISS.
Delphi-Tutorials - OpenGL ISS - Demonstration of the textures of the ISS
Einsatz der ISS-Texturen: Die ISS von Aussen und Innen. Nur vier Texturen genügen, um unserer Raumstation ein hinreichend realistisches Aussehen zu verleihen.

Zusätzlich wird der Positionsvektor der ISS definiert. Sie wird so platziert, dass ihr Zentrum genau auf dem Ursprung (0,0,0) liegt.

3.4.1.4. Meteore

Zuletzt werden dann noch die Meteore mit zwei Texturen versehen:

Delphi-Tutorials - OpenGL ISS - Texture of the meteors
Textur 'meteor_tx': Grafik für die Meteore, die die ISS umschwirren.
Delphi-Tutorials - OpenGL ISS - Texture of an exploding meteor
Textur 'meteor_explode_tx': Grafik für explodierende Meteore.
Delphi-Tutorials - OpenGL ISS - Demonstration of the textures of the meteors
Einsatz der Meteor-Texturen: Meteore steuern auf die ISS zu - und explodieren bisweilen ...

3.4.2. Texturen

3.4.2.1. Bilder über spezielle OpenGL-DLLs

Bei den Quellcodes zu OpenGL, die man im Web so findet, werden Texturen üblicherweise nach dem Programmstart von der Festplatte nachgeladen. Die Units "sdl" und "sdl_image" bieten dafür auch in Delphi die nötige Funktionalität.

Die Methode hat jedoch den Nachteil, dass für die Installation neben dem Programm stets alle Bilder extra mitgeliefert werden müssen (und unter Umständen auch die Bibliotheksdateien "sdl.dll" und "sdl_image.dll").

3.4.2.2. Bilder ohne DLLs und Plattenzugriffe

Bei "OGL_ISS" ist das nicht nötig, denn hier können die Texturen direkt aus TImage-Bildern generiert werden. Das Ganze wurde dabei von mir so gelöst, das die Bibliotheksdateien "SDL"- und "SDL_image.dll" ebenfalls nicht vorhanden sein müssen.

Betrachten wir zunächst die Funktion "img2tx":

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
//return texture-pointer from timage --------------------
function thauptf.img2tx(img:timage):TGLuint;
var
  data:Array of LongWord;
  w,x:integer;
  h,y:integer;
  bmp:tbitmap;
  c:longword;
  line:^longword;
begin
  bmp:=TBitmap.Create;
  bmp.Assign(img.Picture.Graphic);

  //ogl braucht 32bit-pics
  bmp.PixelFormat:=pf32bit;
  w:=bmp.width;
  h:=bmp.height;
  SetLength(data,w*h);

  for y:=0 to h-1 do begin
    line:=bmp.scanline[h-y-1];
    for x:=0 to w-1 do begin
      c:=line^ and $FFFFFF; // Need to do a color swap
      data[x+(y*w)]:=
        (((c and $FF)shl 16)+
        (c shr 16)+
        (c and $FF00)) or $FF000000;
      inc(line);
    end;
  end;

  bmp.free;
  result:=CreateTexture(w,h,addr(Data[0]));
end;

An die Funktion wird das TImage-Objekt "img" übergeben, welches ein beliebiges, vorgeladenes Bild enthält. Es kann sich dabei ebenso um ein GIF, wie ein JPG, eine Bitmap oder irgendein anderes Bildformat handeln. Denn im ersten Schritt wird das enthaltene TPicture sowieso in eine Bitmap "bmp" mit 32 bit Farbtiefe umgewandelt.

Anschliessend wird das LongWord-Array "data" erzeugt, welches gross genug ist, um alle anfallenden Bilddaten komplett aufnehmen zu können.

Das Bild wird nun pixelweise durchlaufen, der Farbwert jedes Pixels auf geheimnisvolle Art umgerechnet - den Teil habe ich mir im Web geklaut - und in das "data"-Array eingetragen.

Das "data"-Array wird daraufhin an die Funktion "createTexture" übergeben, welche letztlich einen von OpenGL verwendbaren "Textur-Index-Wert" zurück liefert:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
//return texture-pointer from image-memory-data --------------------
function thauptf.CreateTexture(Width,Height:Word;pData:Pointer):Integer;
var
  Texture:TGLuint;
begin
  glGenTextures(1,@Texture);
  glBindTexture(GL_TEXTURE_2D,Texture);

  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST_MIPMAP_LINEAR);

  gluBuild2DMipmaps(GL_TEXTURE_2D,GL_RGB,Width,Height,GL_RGBA,GL_UNSIGNED_BYTE,pData);
  result:=Texture;
end;

3.5. Programm-Taktung: Timer-gesteuerte Action

3.5.1. Computer sind verschieden

Als ich begann, "OGL_ISS" mit einem kleinen Spiel zu versehen, wurde schnell klar, dass man nur dann spielbare Ergebnisse erzielen würde, wenn das Ablauftempo des Ganzen halbwegs "normiert" wird. Computer sind nämlich bekanntlich verschieden schnell - und einige besitzen 3D-Grafikkarten, andere nicht. Bedingt dadurch wird die Geschwindigkeit, mit der OpenGL die Objekte rendern und letztlich zur Anzeige bringen kann, für den Programmierer schwer abzuschätzen.

3.5.2. Timer- statt OnIdle-Ereignisse

Aus diesem Grund wird die Grafikverarbeitung in "OGL_ISS" nicht über das OnIdle-Ereignis der Applikation gelöst, wie sonst meist üblich, weil es die beste Performanz bringt. Stattdessen gibt es einen TTimer "frequencyt", der diesen Job in regelmässigen, definierten Zeitabständen erledigt.

Wann immer dieser TTimer "feuert" - und dies geschieht alle paar Millisekunden, je nach Einstellung der TTrackBar "frequencytb" -, wird die folgende Prozedur ausgeführt:

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
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
//float 2 short string---------------------
function thauptf.f2s_cut(d:single):string;
begin
  result:=format('%.2f',[d]);
end;

//------------------------------------------------------------------
procedure Thauptf.frequencytTimer(Sender: TObject);

  function flight(vz:integer):tv;
  begin
    pspeed:=pspeed+vz*_pstep;
    if pspeed> _pspeed_max then pspeed:= _pspeed_max;
    if pspeed<-_pspeed_max then pspeed:=-_pspeed_max;
    result:=getlookv;
  end;

  procedure rotate_axis(vz:integer;var rspeed:single);
  begin
    rspeed:=rspeed+vz*_rstep;
    if rspeed> _rspeed_max then rspeed:= _rspeed_max;
    if rspeed<-_rspeed_max then rspeed:=-_rspeed_max;
  end;

  procedure slowspeed(var speed:single);
  begin
    if speed=0 then exit;
    if speed>0 then speed:=speed-0.2;
    if speed<0 then speed:=speed+0.2;
    if abs(speed)<0.2 then speed:=0;
  end;

begin
  activecontrol:=nil;
  frequ_fast:=deg_norm(frequ_fast+1);
  frequ_low:=deg_norm(frequ_low+0.1);

  //rotation earth
  earth_rot:=deg_norm(earth_rot+_earth_drot);

  //rotation small solar sail
  if trunc(frequ_low)mod 5=4 then
    iss_small_sail_rot:=deg_norm(iss_small_sail_rot+1);

  //rotation big solar sail
  if trunc(frequ_low)mod 8=7 then
    iss_big_sail_rot:=deg_norm(iss_big_sail_rot+random(2));


  //change position/direction?
  if akey<>0 then begin

    if actrl then begin
      //rotation x-axis
      if      akey=vk_up    then rotate_axis( 1,rx_speed)
      else if akey=vk_down  then rotate_axis(-1,rx_speed);

      //rotation z-axis
      if      akey=vk_left  then rotate_axis( 1,rz_speed)
      else if akey=vk_right then rotate_axis(-1,rz_speed);

    end

    //flying
    else if akey=vk_right then rotate_axis( 1,ry_speed)
    else if akey=vk_left  then rotate_axis(-1,ry_speed)

    else if akey=vk_up   then dposv:=flight( 1)
    else if akey=vk_down then dposv:=flight(-1);
  end;

  if pspeed<>0 then begin
    if dposv.x<>0 then posv.x:=posv.x+dposv.x*pspeed;
    if dposv.y<>0 then posv.y:=posv.y+dposv.y*pspeed;
    if dposv.z<>0 then posv.z:=posv.z+dposv.z*pspeed;
    if draw_path_ok then path;
  end
  else begin
    dposv:=v_fill(0,0,0);
  end;

  if rx_speed<>0 then rotv.x:=deg_norm(rotv.x-rx_speed);
  if ry_speed<>0 then rotv.y:=deg_norm(rotv.y-ry_speed);
  if rz_speed<>0 then rotv.z:=deg_norm(rotv.z-rz_speed);


  if flyhome_c<=0 then begin
    slowspeed(pspeed);
    slowspeed(rx_speed);
    slowspeed(ry_speed);
    slowspeed(rz_speed);
  end;

  //cockpit info
  if cockpitp.Visible then begin
    pxp.caption:=f2s_cut(posv.x);rotxp.caption:=f2s_cut(rotv.x)+'°';
    pyp.caption:=f2s_cut(posv.y);rotyp.caption:=f2s_cut(rotv.y)+'°';
    pzp.caption:=f2s_cut(posv.z);rotzp.caption:=f2s_cut(rotv.z)+'°';

    if flyhome_c>0 then
      game_statusp.caption:='*** FLY HOME '+inttostr(flyhome_c)+' ***'
    else
      game_statusp.caption:=game_status;

    game_pointsp.caption:=inttostr(game_points);
  end;

  draw_scene;

  if flyhome_c>0 then begin
    dec(flyhome_c);
    if flyhome_c=0 then begin
      pspeed:=0;
      rx_speed:=0;ry_speed:=0;rz_speed:=0;

      //fly home because of iss destroying?
      if game_iss_meteor<>-1 then begin
        //let the dirty meteor explode
        meteor_a[game_iss_meteor].state:=_meteor_state_explode;
        meteor_a[game_iss_meteor].count:=30;
        dosound(sound_explode);
      end;
    end;
  end;

  dec(sound_c);if sound_c<0 then sound_c:=0;
end;
3.5.2.1. Zähler-Update

Am Anfang werden die Rotationszähler "frequ_fast", "frequ_low" und "earth_rot" auf verschieden starke Weise erhöht. Sie dienen dazu, periodisch einige Elemente der ISS zu bewegen, etwa die grossen Solar-Segel, oder die Erde genau so schnell rotieren zu lassen, dass in 90 Minuten eine komplette Rotation erfolgt (so viel bzw. wenig Zeit benötigt nämlich auch die echte ISS für die Erdumrundung).

3.5.2.2. User-Eingaben-Check

Anschliessend wird geprüft, ob der Anwender eine der Flugsteuerungstasten betätigt hat. Ist dies der Fall, werden sie entsprechend abgearbeitet. Wurde zusätzlich die "STRG"-Taste ("actrl") gedrückt, dann gilt:

  • Cursor hoch/runter: Rotation um x-Achse (Rotations-Delta: "rx_speed")
  • Cursor recht/links: Rotation um z-Achse (Rotations-Delta: "rz_speed")

Ansonsten gilt:

  • Cursor hoch/runter: Flug geradeaus nach vorne/hinten (Positions-Delta: "pspeed" und "dposv")
  • Cursor recht/links: Rotation um y-Achse (Rotations-Delta: "ry_speed")
3.5.2.3. Kalkulation von Rotations- und Positions-Delta

Über die internen Funktionen "rotate_axis" und "flight" wird dazu jeweils das zugehörige Rotations- bzw. Positionsdelta berechnet (siehe obige Liste).

Das Rotationsdelta wird einfach um einen konstanten Wert erhöht oder gemindert. Wird dabei der Rotationswinkel von 360 Grad überschritten, beginnt es wieder bei 0 Grad. Die Stärke der Änderung wird auf "_rspeed_max" begrenzt, damit wir am Ende nicht völlig orientierungslos und in in rasender Geschwindigkeit durch den Raum rotieren.

Das Positionsdelta ist komplizierter zu berechnen. Dafür müssen wir nämlich feststellen, in welche Richtung aktuell geschaut wird, damit wir exakt geradeaus steuern können. Hier kommen die Rotationsmatrizen zum Einsatz, die wir in "DanGeoU.pas" definiert haben. Dem Thema widmen wir uns weiter unten im Kapitel "Fliegen im dreidimensionalem Raum".

Je länger eine Steuerungstaste betätigt wird, desto grösser wird das Rotations- bzw. Positionsdelta pro Timer-Aufruf; man beschleunigt also. Wird die Taste wieder losgelassen, sorgt die interne Prozeduren "slowspeed" dafür, dass die verschiedenen Delta-Variablen langsam wieder auf null zurückgehen; man bremst also ab.

Ist der automatische Heimflug aktiviert ("flyhome_c" ist grösser Null), dann werden übrigens alle Steuerungseingaben ignoriert. Der Pilot muss in diesem Fall warten, bis der Ursprungsort erreicht ist, bevor er wieder selbst aktiv werden kann.

3.5.2.4. Cockpit-Update

Nachdem unsere Position und Geschwindigkeit im Raum neu gesetzt wurde, wird weiter geprüft, ob das Cockpit sichtbar ist. Ist dem so, dann werden jetzt diverse Statusmeldungen auf dem Bildschirm ausgegeben.

Delphi-Tutorials - OpenGL ISS - Cockpit of our spaceship
Cockpit: Das Cockpit unseres Raumschiffs mit den Status-Informationen (oben). Es lässt sich auch jederzeit optional abschalten, was den Sichtbereich vergrössert (unten).

Anschliessend wird über die Prozedur "draw_scene" die aktuelle Grafikausgabe initiiert.

Zuletzt wird dann noch einmal geprüft, ob wir uns im automatischen Heimflug befinden, und ob wir das Ziel bereits erreicht haben. Wenn ja, werden alle Deltas auf null gesetzt - unser Raumschiff steht still. Weiterhin wird geklärt, ob der automatische Heimflug freiwillig geschah oder erzwungen wurde, weil die ISS von einem Meteor getroffen worden ist. In letzterem Fall wird eine schöne, grosse, finale Explosion vorbereitet :-)

3.6. Fliegen im dreidimensionalem Raum

Die Programmierung von "OGL_ISS" lief eigentlich wie am Schnürchen. Die einzige Geschichte, bei der ich wirklich zu kämpfen hatte, war der vermeintlich einfache Flug geradeaus.

Bei "OGL_Henrys" und "OGL_Planets" wurde bereits eine Steuerung zum Navigieren durch einen 3-dimensionalen Raum realisiert. Dort konnte man jedoch nur senkrecht zu der y-Achse nach oben und unten fliegen, nicht jedoch in "schräger" Weise gleichzeitig nach vorne und nach unten.

3.6.1. Probleme mit der Fadenkreuz-Navigation

Einfacher gesagt: Was ich diesmal wollte, war eine Steuerung, die das Raumschiff immer exakt in Richtung des im Fadenkreuz ausgewiesene Zieles fliegen liess, egal, wie sehr die Raum-Achsen auch verstellt waren.

Ich malte also viele kleine Bildchen, kombinierte Sinus- und Kosinus-Werte, spielte mit dem Vorzeichen herum und versuchte mich am Ende sogar an der Verwendung der Tangens-Funktion.

Delphi-Tutorials - OpenGL ISS - Formulas for the navigation in space
Mathematische Gehversuche: Die blöde Rotation zur zielgenauen Navigation im dreidimensionalen Raum muss doch irgendwie in dem Griff zu bekommen sein ...

Nach drei Tagen (!) gab ich auf. Musste in Mathematik Nachsitzen. Las Vektoren- und Matrizen-Tutorials. Baute dabei die "DanGeoU.pas" zusammen. Und knackte so schliesslich das Problem mit etwas Web-Hilfe.

3.6.2. Positionsänderung

Schauen wir uns noch einmal den - etwas vereinfachten - Code zur Modifikation des Positionsdeltas aus der "frequencytTimer"-Prozedur an:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
procedure Thauptf.frequencytTimer(Sender: TObject);

  function flight(vz:integer):tv;
  begin
    pspeed:=pspeed+vz*_pstep;
    result:=getlookv;
  end;

begin
  [...]

  if      akey=vk_up   then dposv:=flight( 1)
  else if akey=vk_down then dposv:=flight(-1);

  posv.x:=posv.x+dposv.x*pspeed;
  posv.y:=posv.y+dposv.y*pspeed;
  posv.z:=posv.z+dposv.z*pspeed;

  [...]
end;

Das eigentliche Positionsdelta "pspeed" wird ähnlich wie die Rotationsdeltas einfach nur um einen konstanten Wert erhöht oder gemindert. Damit wissen wir schon einmal, wie stark unsere Position im Raum geändert werden soll.

3.6.3. Richtungsänderung

Damit ist aber noch nicht geklärt, in welcher Richtung sich die Änderung auswirken soll. Für diesen Job muss der aktuelle Richtungsvektor "dposv" bestimmt werden.

Sind alle Achsen-Winkel auf null, dann ist die Richtung klar: Da wir als Piloten in den Raum hineinschauen, müssen wir uns nur entlang der z-Achse ins Negative bewegen, um nach vorne zu fliegen. Der Richtungsvektor ist in diesem Fall einfach v=(0,0,-1). Die x- und y-Positionen bleiben völlig unverändert.

Delphi-Tutorials - OpenGL ISS - Direction vector before rotation
Richtungsvektor vor der Rotation: Der ursprüngliche Richtungsvektor 'v' zeigt exakt nach vorne, in die gewünschte Richtung.

Rotieren wir nun aber 45 Grad um die y-Achse (positive Rotationen erfolgen übrigens immer entgegen dem Uhrzeigersinn - auch so eine Sache, die ich Stoffel erst nach einer ganzen Weile merkte), dann bedeutet ein Flug entlang der z-Achse, dass wir nicht mehr geradeaus fliegen, sondern schräg nach links hinten. Um die korrekte Richtung einzuhalten, muss in diesem Fall nämlich auch die x-Position geändert werden.

Delphi-Tutorials - OpenGL ISS - Direction vector after rotation about the y-axis
Richtungsvektor nach der Rotation: Der Richtungsvektor 'v' wurde um 45 Grad um die y-Achse rotiert. Nun zeigt er nach schräg nach links hinten. Tatsächlich wollen wir uns aber in die neue Richtung 'vneu' bewegen. Wie finden wir diesen Vektor?

Noch komplizierter wird es natürlich, wenn zudem um die x-Achse rotiert wird. Spätestens jetzt gilt es, alle Koordinaten-Werte, also x, y und z, durch den Richtungsvektor "dposv" anzupassen, um in die vom Fadenkreuz vorgegebene Richtung zu fliegen.

Und wie erhalten wir nun den gesuchten Richtungsvektor "vneu" (="dposv")?

Indem wir den ursprünglichen Richtungsvektor, nämlich v=(0,0,-1), alle vorgegebenen Achsen-Rotationen nachmachen lassen. Denn dann sollte dieser wieder genau in die Richtung weisen, in die wir selbst gerade schauen.

Der Source-Code dazu ist:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
//give back rotated look vector about all axis-----------------------
function thauptf.getlookv;
var
  v:tv;
  mx,my,mz,m:tm;
begin
  //fill rotation matrices
  mx:=m_fill_rot_x_deg(rotv.x);
  my:=m_fill_rot_y_deg(rotv.y);
  mz:=m_fill_rot_z_deg(rotv.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;

3.6.4. Hilfe durch Rotations-Matrizen

Wir bilden uns zuerst die generalisierte Rotationsmatrix, indem wir die drei Achsen-Rotationsmatrizen bestimmen und diese dann miteinander multiplizieren. Wie in "DanGeoU.pas" beschrieben, "merkt" sich die so entstandene Matrix "m" alle Rotationen ihrer Teiler-Matrizen "mx", "my" und "mz".

Jetzt muss man nur noch den ursprünglichen Richtungsvektor mit der generalisierten Rotationsmatrix "m" multiplizieren. Als Ergebnis erhält man den denjenigen Vektor, der in unserem Modell exakt nach vorne weist. Multipliziert mit dem Positionsdelta "pspeed" und aufaddiert zum Positionsvektor "posv" erhalten wir schliesslich die neue Position im Raum.

Eigentlich doch ganz einfach ...

3.7. Spiel-Auswahl

3.7.1. Die Qual der Wahl

Bevor der TTimer "frequencyt" in "FormCreate" aktiviert wird, wird "game_start_show" aufgerufen (siehe weiter oben).

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
//--------------------------------------------------
procedure thauptf.game_start_show;
var
  r:integer;
begin
  cockpitp.Color:=clblack;
  game_pointsp.caption:='';
  detect_issp.Caption:='';
  detect_mep.Caption:='';
  meteorp.color:=clblack;
  meteorcp.caption:='';

  gameoverp.visible:=false;
  gamestartp.visible:=true;
  game_points:=0;
  game_level:=_game_level_train;

  game_status:='Select Dirty Game Mode!';

  //clear shots-----------------------------------------------
  for r:=0 to _shot_c-1 do shot_a[r].dirv:=v_fill(0,0,0);
  shot_c:=0;

  //clear meteors-images----------------------------------------
  for r:=0 to _meteor_max-1 do meteor_imga[r].Visible:=false;

  //clear path-----------------------------------------------
  for r:=0 to _path_c-1 do path_a[r]:=v_fill(0,0,0);
  path_c:=0;

  //go home position
  posv:=v_fill(0,0,_home_pos_z);dposv:=v_fill(0,0,0);
  rotv:=v_fill(0,0,0);

  //no movement or key action
  akey:=0;
  actrl:=false;
  pspeed:=0;
  rx_speed:=0;
  ry_speed:=0;
  rz_speed:=0;
  game_iss_meteor:=-1;
end;
3.7.1.1. Level-Auswahl

Hier werden einige Programm-Parameter zurückgesetzt und das TPanel "gamestartp" sichtbar gemacht. Über die Tastatur kann dann u.a. der gewünschte Spiel-Level ausgewählt werden.

Delphi-Tutorials - OpenGL ISS - Game opening screen
Spielstart-Fenster: Erklärung der Tastaturkürzel und die Auswahl verschiedener Programm-Optionen.
3.7.1.2. Taktfrequenz-Auswahl

Mittels der TTrackBar "frequencytb" lässt sich einstellen, mit welcher Taktfrequenz die "frequencytTimer"-Prozedur aufgerufen wird (und damit auch "draw_scene"). Das Intervall reicht von einer bis bis 100 Millisekunden. Je nach Rechnergeschwindigkeit kann der Anwender hier einen für sich passenden Wert einstellen.

3.7.2. Wenn der User drückt - Tastatur-Ereignisse abfangen

Alle Tastatur-Eingaben des Benutzers werden über die Prozeduren "FormKeyDown" und "FormKeyp" verwaltet.

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
//--------------------------------------------------
procedure Thauptf.FormKeyDown(
  Sender: TObject;
  var Key:Word;
  Shift:TShiftState
);
begin
  akey:=0;

  if flyhome_c>0 then exit;

  //shift & ctrl state
  actrl:=(ssctrl in shift);

  //flight key pressed?
  if
    (key=vk_up)or
    (key=vk_down)or
    (key=vk_left)or
    (key=vk_right)
  then begin
    akey:=key;
    exit;
  end;

  if gameoverp.Visible then begin
    if key=vk_return then game_start_show;
    exit;
  end;

  if gamestartp.Visible then begin
    if      key=ord('1')  then game_start(_game_level_train)
    else if key=ord('2')  then game_start(_game_level_rookie)
    else if key=ord('3')  then game_start(_game_level_advanced)
    else if key=ord('4')  then game_start(_game_level_profi)
    else if key=ord('5')  then game_start(_game_level_mad)
    else if key=vk_escape then begin
      close;
      exit;
    end;
  end;

  if key=ord('G') then begin
    graphic_mode:=graphic_mode+1;
    if graphic_mode>7 then graphic_mode:=0;
  end
  else if key=ord('C')  then footp.Visible:=not footp.Visible
  else if key=ord('F')  then flyhome
  else if key=ord('P')  then draw_path_ok:=not draw_path_ok
  else if key=ord('S')  then sound_ok:=not sound_ok
  else if key=ord('V')  then draw_visor_ok:=not draw_visor_ok
  else if key=vk_space  then shot
  else if key=vk_escape then game_over_show;
end;

//set key holder back-----------------------------------------------------
procedure Thauptf.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  akey:=0;
  actrl:=false;
end;

Zunächst wird geprüft, ob der automatische Heimflug aktiviert ist. In diesem Fall werden alle Benutzer-Eingaben ignoriert.

Der Status der "Strg"-Taste wird in der Variablen "actrl" gesichert. Sollte eine der Flugsteuerungstasten betätigt worden sein, so wird "akey" gesetzt und die Prozedur verlassen. Wie wir gesehen haben, erfolgt deren Abarbeitung in der Prozedur "frequencytTimer".

Wird das TPanel "gameoverp" mit der aktuellen Highscore-Liste angezeigt, befinden wir uns am Spiel-Ende. In diesem Fall wird nur auf die "Return"-Taste reagiert.

Befinden wir uns am Spiel-Start, dann ist das TPanel "gamestartp" sichtbar. Jetzt reagieren wir insbesondere auf die Tasten "1" bis "5", mit denen sich der Spiel-Level auswählen lässt. Dazu wird in der Folge die Prozedur "game_start" aufgerufen.

Sind übrigens die TPanels "gamestartp" und "gameoverp" nicht sichtbar, dann befinden wir uns im laufenden Spiel.

Die "G"-Taste wechselt durch Erhöhung des "graphic_mode"-Zählers den Grafik-Modus, mit dem in "draw_scene" das Modell gerendert wird.

Die Tasten "C", "P", "S" und "V" aktivieren bzw. deaktivieren die Anzeige des Cockpits, die Sichtbarkeit des Flug-Pfades, die Sound-Effekte und das Visier mit dem Fadenkreuz.

Mittels "F"-Taste wird der automatische Heimflug in Gang gesetzt.

Über "Space" werden Schüsse abgegeben.

Und "Escape" bricht letztlich das laufende Spiel ab oder beendet das Programm.

3.8. Spiel-Start

3.8.1. Programm-Parameter setzen

Haben wir ein Spiel-Level ausgewählt, wird die Prozedur "game_start" 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
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
//--------------------------------------------------------
procedure thauptf.game_start(level:byte);
var
  r:integer;
begin

  //home position, no movement
  flyhome_c:=0;
  posv:=v_fill(0,0,_home_pos_z);dposv:=v_fill(0,0,0);
  rotv:=v_fill(0,0,0);
  rx_speed:=0;
  ry_speed:=0;
  rz_speed:=0;
  gamestartp.visible:=false;
  game_level:=level;
  game_iss_meteor:=-1;


  if game_level=_game_level_train then begin
    game_status:='TRAINING';
    game_meteor_c:=0;
  end
  else if game_level=_game_level_rookie then begin
    game_status:='ROOKIE (Highscore '+inttostr(game_hs_rookie)+')';
    game_meteor_c:=10;
    game_meteor_dim:=5000;
    game_meteor_speed:=0.5;
  end
  else if game_level=_game_level_advanced then begin
    game_status:='ADVANCED (Highscore '+inttostr(game_hs_advanced)+')';
    game_meteor_c:=20;
    game_meteor_dim:=2000;
    game_meteor_speed:=1;
  end
  else if game_level=_game_level_profi then begin
    game_status:='PROFI (Highscore '+inttostr(game_hs_profi)+')';
    game_meteor_c:=30;
    game_meteor_dim:=2000;
    game_meteor_speed:=2;
  end
  else begin
    game_status:='MAD (Highscore '+inttostr(game_hs_mad)+')';
    game_meteor_c:=_meteor_max;
    game_meteor_dim:=2000;
    game_meteor_speed:=3;
  end;

  //set/show active meteors
  for r:=0 to game_meteor_c-1 do begin
    meteor_imga[r].width:=round(meteorcp.Width/game_meteor_c);
    meteor_imga[r].Visible:=true;
    meteor_set(r);
  end;
end;

Der Benutzer wird an die Startposition im Spiel transportiert, indem die Achsen-Rotation auf null gesetzt und der Positions-Vektor "posv" auf 200 m ("_pos_home_z") vor der ISS initialisiert wird.

Je nach Spiel-Level wird nun die Anzahl der Meteore "game_meteor_c" differiert, die auf die ISS zufliegen werden. Ihre maximale Entfernung von der ISS zu Beginn des Spieles steht in "game_meteor_dim". Und über "game_meteor_speed" wird eingestellt, wie schnell die Meteore später fliegen dürfen.

Es gilt: Je höher der Spiel-Level, desto grösser die Anzahl der Meteore, desto kleiner die Entfernung von der ISS, und desto schneller die Meteore. Während es beim Spiel-Level "_game_level_train" gar keine Meteore gibt, sind es bei "_game_level_mad" deren sehr viele, sehr schnelle und sehr nahe :-)

Alle im Spiel aktiven Meteore werden anschliessend auf dem TPanel "meteorcp" sichtbar gemacht, anschliessend die Start-Bedingungen über die Prozedur "meteor_set" gesetzt.

Indem das TPanel "gamestartp" unsichtbar gemacht wird, wechseln wir automatisch in in den aktiven Spiel-Modus.

3.8.2. Meteore verteilen

Der Spiel-Level gibt vor, wie viel Meteore im Spiel vorkommen und abgeschossen werden müssen. Auch ihre Verteilung im Raum und ihre Geschwindigkeit hängt davon ab. Die nötigen Einstellungen nimmt die Prozedur "meteor_set" für jeden einzelnen Meteor extra vor:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
//---------------------------------------------------------------------
procedure thauptf.meteor_set(r:integer);
var
  d:single;
  pm:pmeteor;
begin
  pm:=@meteor_a[r];

  //set meteors away from iss
  repeat
    pm.posv.x:=random(game_meteor_dim)-random(game_meteor_dim);
    pm.posv.y:=random(game_meteor_dim)-random(game_meteor_dim);
    pm.posv.z:=random(game_meteor_dim)-random(game_meteor_dim);
    d:=v_distance(iss_startv,pm.posv);
  until d>game_meteor_dim/2;

  pm.count:=0;
  pm.radius:=5+random(_meteor_r)/10;
  pm.state:=_meteor_state_fly;
  pm.speed:=0.05+random(trunc(game_meteor_speed*100))/100;
  meteor_imga[r].Visible:=true;
end;

Zuerst wird per Zufall eine Position des Meteors gesetzt. Danach wird geprüft, ob die Distanz des Meteors zur ISS dem Spiel-Parameter "game_meteor_dim/2" genügt (das wird über die "DanGeoU"-Funktion "v_distance" berechnet). Ist der Meteor zu nahe, dann wird eine neue, zufällige Position vergeben.

Anschliessend erhält der Meteor einen zufallsbedingt grossen Radius, wird auf den Status "_meteor_state_fly" gesetzt, und bekommt eine bestimmte Flug-Geschwindigkeit verpasst.

3.9. Spiel-Ablauf

3.9.1. Szenen-Rendering

Das TPanel "gamestartp" ist unsichtbar, der TTImer "frequencyt" ist aktiviert, die Spiel-Parameter alle gesetzt, alle Meteore initialisiert - das Spiel hat begonnen.

Wie oben gezeigt, wird durch die Prozedur "frequencytTimer" in regelmässigen Abständen die zentrale Prozedur "draw_scene" aufgerufen, die wir uns jetzt näher anschauen wollen.

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
//------------------------------------------------------------------
procedure thauptf.draw_scene;
begin
  //get graphic mode
  graphic_txok   :=(graphic_mode and 4)>0;
  graphic_earthok:=(graphic_mode and 2)>0;
  graphic_spaceok:=(graphic_mode and 1)>0;

  if graphic_txok then glPolygonMode(GL_FRONT_AND_BACK,GL_FILL)
                  else glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);


  glClear(GL_COLOR_BUFFER_BIT OR GL_DEPTH_BUFFER_BIT);
  glLoadIdentity;

  //rotation & positioning
  glRotatef(360-rotv.x,1.0,0,0);
  glRotatef(360-rotv.y,0,1.0,0);
  glRotatef(360-rotv.z,0,0,1.0);
  glTranslatef(-posv.x,-posv.y,-posv.z);

  if graphic_spaceok then draw_space;
  if graphic_earthok then draw_earth;

  //iss is gone away?
  if
    (game_iss_meteor<>-1)and
    (meteor_a[game_iss_meteor].state=_meteor_state_explode)and
    (meteor_a[game_iss_meteor].count<20)
  then begin
  end
  else draw_iss;

  //scene is active?
  if
    not gamestartp.visible and
    not gameoverp.Visible or
    (gameoverp.Visible and(game_iss_meteor<>0))
  then begin
    if game_level<>_game_level_train then draw_meteors;
    draw_shots;
  end;

  if draw_path_ok then draw_path;

  if draw_visor_ok then draw_visor;

  SwapBuffers(handle_DC);
end;

Zu Beginn wird anhand des Parameters "graphic_mode" bestimmt, welche Elemente der Szenerie gemalt werden sollen und welche nicht. Zudem wird eingestellt, ob Texturen Verwendung finden oder stattdessen nur ein Gittermodell angezeigt wird.

3.9.1.1. Acht Grafik-Modi

Insgesamt gibt es acht verschiedene Grafik-Modi, die die Ausgabe über die booleschen Variablen "graphic_txok", "graphic_earthok" und "graphic_spaceok" regulieren. Es gilt:

Grafik-Modus
graphic_mode
Texturen
graphic_txok
Erde
graphic_earthok
Weltall
space_txok
Beispiel
0 Nein Nein Nein
Delphi-Tutorials - OpenGL ISS - Grafikmodus 0
1 Nein Nein Ja
Delphi-Tutorials - OpenGL ISS - Grafikmodus 1
2 Nein Ja Nein
Delphi-Tutorials - OpenGL ISS - Grafikmodus 2
3 Nein Ja Ja
Delphi-Tutorials - OpenGL ISS - Grafikmodus 3
4 Ja Nein Nein
Delphi-Tutorials - OpenGL ISS - Grafikmodus 4
5 Ja Nein Ja
Delphi-Tutorials - OpenGL ISS - Grafikmodus 5
6 Ja Ja Nein
Delphi-Tutorials - OpenGL ISS - Grafikmodus 6
7 Ja Ja Ja
Delphi-Tutorials - OpenGL ISS - Grafikmodus 7

Nachdem der Grafik-Modus bestimmt ist, wird mit "glClear" der OpenGL-Speicher gelöscht und mit "glLoadIdentity" zum Ursprungsort transportiert.

3.9.1.2. Wo sind wir im Modell?

Es folgt die Ausrichtung des Modells gemässt des Rotation-Vektors "rotv" sowie unsere Positionierung im Raum gemäss des Position-Vektors "posv".

Je nach Grafik-Modus wird dann das Weltall und/oder die Erde gezeichnet. Diese "draw_"-Routinen sehen wir uns gleich näher an.

Danach wird geprüft, ob die ISS noch vorhanden oder bereits durch einen Meteor zerstört wurde. Gemalt wird sie natürlich nur, wenn sie noch existiert.

Anschliessend werden die Meteore gezeichnet, sofern wir uns nicht gerade im Spiel-Level "_game_level_train" befinden. Die Schüsse, die wir abgegeben haben, werden nur so lange angezeigt, wie die ISS nicht von einem Meteor getroffen wurde.

Der boolesche Parameter "draw_path_ok" gibt vor, ob unsere bisherige Flugroute im Modell optisch gekennzeichnet werden soll.

Ein weiterer boolescher Parameter, "draw_visor_ok", reguliert, ob das Visier vorhanden ist oder nicht.

Mittels der OpenGL-Prozedur "SwapBuffers" wird das Modell letztlich auf dem TPanel "viewp" zur Anzeige gebracht.

3.9.1.3. Der Welten-Raum wird gemalt

Um den Hintergrund etwas interessanter zu gestalten, wird um unserer Position herum eine gewaltige Kugel gezeichnet, innerhalb der wir uns bewegen. Wir können diese Kugel praktisch nicht verlassen und sehen daher stets auf ihre Innenwände.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
//space-------------------------------------------------
procedure thauptf.draw_space;
begin
  glPushMatrix();
    gldisable(GL_DEPTH_TEST);
    glTranslatef(0,0,0);

    glRotatef(90,1,0,0);
    graphic_mode_bind(space_tx,0.1,0.1,0.1);
    gluSphere(space_quad,_space_r,20,20);

    graphic_mode_off;
    glenable(GL_DEPTH_TEST);
  glPopMatrix();
end;

Die Kugel wird mittels der OpenGL-Prozedur "gluSphere" realisiert. Ihr Mittelpunkt ist im Ursprung, ihr Radius "_space_r" doppelt so gross wie der Erdradius ("_earth_r", das sind 6368000 m).

Zuvor wird über die Prozedur "graphic_bind_mode" bestimmt, ob die Kugel mit Texturen versehen oder als Gittermodell gezeichnet werden soll.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
//--------------------------------------------------------
procedure thauptf.graphic_mode_bind(tx:gluint;r,g,b:single);
begin
  if not graphic_txok then begin
    glColor3f(r,g,b);
    exit;
  end;
  glenable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D,tx);
end;

//--------------------------------------------------------
procedure thauptf.graphic_mode_off;
begin
  glColor3f(1,1,1);
  glDisable(GL_TEXTURE_2D);
end;

Falls der Grafik-Modus ein reines Gittermodell vorsieht, bleiben die Texturen deaktiviert. Stattdessen wird jedoch die Farbe angepasst, mit der die Gitterlinien gezeichnet werden sollen.

Die Prozedur "graphic_mode_off" schaltet Texturen generell ab, egal, ob sie zuvor aktiviert wurden oder nicht.

Delphi-Tutorials - OpenGL ISS - Drawing of the space with OpenGL
Prozedur 'draw_space': Der Hintergrund wird gemalt. Eine riesige Sphäre um den Ursprung. it einer dunklen Textur, die das Weltall wiedergibt.
3.9.1.4. Die Erde wird gemalt - Home, sweet home

Auch die Erde wird mittels "gluSphere" gezeichnet. Ihr Mittelpunkt befindet sich zu Beginn des Spiels 345 km (Höhe ISS) plus 6368 km (Erd-Radius) unter dem Ursprung.

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
//----------------------------------------------------------------------
{                       (0/0/0)
                          ISS
                           |
                           |y
                           |
                     (0/-345 km/0)
                        #######
             #############################
     #############################################
  ###################################################
  ###################################################
####################(0/-6368-345/0)####################
  ###################################################
  ###################################################
    ##############################################
             #############################
                       ########

}

//----------------------------------------------------------------------
procedure thauptf.draw_earth;
const
  _scale=100; //needed because of depth buffer problems
begin
  glPushMatrix();
    //keep constant distance so you never reach surface
    //needed because of the scaled hight
    //which is much less then in reality
    glTranslatef(posv.x,posv.y+_earth_y/_scale,posv.z);

    graphic_mode_bind(earth_tx,0.5,0.5,1);
    glRotatef(140,1,0,0);
    glRotatef(85,0,1,0);
    glRotatef(earth_rot,1,0,0);
    gluSphere(earth_quad,_earth_r/_scale,50,50);

    graphic_mode_off;
  glPopMatrix();
end;

Anders als die Weltall-Kugel sehen wir die Erde von aussen. Daraus resultierten - Grrr! - wieder einmal Probleme mit dem Tiefenpuffer, denn die Erd-Kugel erschient stellenweise transparent, sodass man durch sie hindurch ihre "Rückwand" sehen konnte.

Delphi-Tutorials - OpenGL ISS - Drawing-Error - the Earth seems to be transparent
Wiedergabefehler bei der Prozedur 'draw_earth': Aufgrund ihrer immensen Grösse erscheint die Erdkugel zur Hälfte transparent, weil der Tiefenpuffer-Bereich überschritten wird. Dies muss verhindert werden. Doch wie?

Aus diesem Grund wird die Erde um den Faktor "_scale" (=100) verkleinert, ebenso ihr Abstand zur ISS. Damit kam der Tiefenpuffer glücklicherweise zurecht.

Jetzt bestand jedoch die "Gefahr", dass man in die Erd-Kugel eintauchte, wenn ein paar Minuten senkrecht nach unten geflogen wurde. Um dies zu verhindern, wandert der Mittelpunkt der Sphäre parallel zu unserer Raum-Position mit, sodass stets ein konstanter Abstand eingehalten wird. Egal, wie lange wir auch fliegen, wir erreichen den Erdboden nie.

Bisschen billig, die Lösung, aber sie funzt.

3.9.1.5. Die ISS wird zusammengebaut

Die ISS ist komplizierter aufgebaut als Weltall und Erde. Sie setzt sich aus mehreren, sich teilweise wiederholenden, Elementen zusammen. Die ASCII-Grafiken über dem Funktions-Rumpf deuten an, wo wann was wie gross gemalt wird.

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
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224
00225
00226
00227
00228
00229
00230
00231
00232
00233
00234
00235
00236
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
//----------------------------------------------------------------------
{
         z-Axis (+)
            |
            |
            |

   #########*##########   part_bigsolarsail
   #########*##########   part_bigsolarsail
         ---#             part_smallsolarsail
            *   +
       +++++*++++         part_center
            *   *
         ---#             part_smallsolarsail
   #########*##########   part_bigsolarsail
   #########*##########   part_bigsolarsail
 -----------+------------------------------------------ x-Axis (+)
         (0/0/0)
            |
            |
         z-Axis (-)
}

//----------------------------------------------------------------------
procedure thauptf.draw_iss;
const
  _cylr=2.5;
  _cyls=20;

  _bigsailw  =4;_bigsailh  =70;
  _smallsailw=1;_smallsailh=20;

  //------------------------------------------
  {
               y-Axis (+)
                 |
                 |
               #####
               #####
               #####
               #####
               ##### h
               #####
               #####
      -----------+--------------------- x-Axis (+)
                 |
                 w
  }

  //------------------------------------------
  procedure solarsail(width,height:single;tx:gluint);
  begin
    graphic_mode_bind(tx,1,1,1);
    glbegin(gl_quads);
      gltexcoord2f( 0,0);glvertex3f(0-(width/2),0+(height/2),0);
      gltexcoord2f( 0,1);glvertex3f(0+(width/2),0+(height/2),0);
      gltexcoord2f(10,1);glvertex3f(0+(width/2),0-(height/2),0);
      gltexcoord2f(10,0);glvertex3f(0-(width/2),0-(height/2),0);
    glend;
    graphic_mode_off
  end;

  //------------------------------------------
  {
                     A*
     E###############***#################B
                     ***
     D###############***#################C 4m
           70m       ***
                     5m
  }

  //------------------------------------------
  procedure part_bigsolarsail;
  begin
    glPushMatrix();

      //A) draw body-cylinder
      graphic_mode_bind(iss_big_tx,1,0.5,0.5);
      glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
      graphic_mode_off;

      //positioning for solar sails
      glRotatef(90,1,0,0);                 //tilt back/forward
      glRotatef(90,0,0,1);                 //turn right
      glTranslatef(13-_bigsailw,0,0);      //shift forward
      glTranslatef(0,_bigsailh/2+_cylr,0); //shift right

      //B)big solar sail
      solarsail(_bigsailw,_bigsailh,iss_big_sail_tx);

      //C)big solar sail
      glTranslatef(-_bigsailw-1,0,0); //shift back
      solarsail(_bigsailw,_bigsailh,iss_big_sail_tx);

      //D)big solar sail
      glTranslatef(0,-_bigsailh-_cylr*2,0);    //shift left
      solarsail(_bigsailw,_bigsailh,iss_big_sail_tx);

      //E)big solar sail
      glTranslatef(_bigsailw+1,0,0); //shift forward
      solarsail(_bigsailw,_bigsailh,iss_big_sail_tx);

      //small solar sail under cylinder
      glTranslatef(0,_bigsailh/2+_cylr,0);    //shift right
      glTranslatef(-2.5,0,0);                 //shift back
      glTranslatef(0,0,_smallsailh/2/2+_cylr);  //shift down
      glRotatef(90,1,0,0);
      solarsail(_smallsailw,_smallsailh/2,iss_small_sail_tx);

    glPopMatrix();
  end;

  //------------------------------------------
  {

           z-Axis (+)             y-Axis (+)
              |                       |
             A**                   A*****
             ***           B#######******
    ABC######***           C#######******
        20m  ***           D#######******
             ***                   ******
              |                       |
     ---------+--------    -----------+----- x-Axis (+)

  }

  //------------------------------------------
  procedure part_smallsolarsail;
  begin
    glPushMatrix();

      //A)body cylinder
      graphic_mode_bind(iss_small_tx,1,1,1);
      glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
      graphic_mode_off;

      //positioning for sails
      glRotatef(90,0,0,1);                    //turn right
      glTranslatef(0,0,13/2);                 //shift forward
      glTranslatef(0,-_smallsailh/2-_cylr,0); //shift left
      glTranslatef(1,0,0);                    //shift up

      //B)small solar sail
      solarsail(_smallsailw,_smallsailh,iss_small_sail_tx);

      //C)small solar sail
      glTranslatef(-_smallsailw-0.2,0,0); // shift down
      solarsail(_smallsailw,_smallsailh,iss_small_sail_tx);

      //D)small solar sail
      glTranslatef(-_smallsailw-0.2,0,0); // shift down
      solarsail(_smallsailw,_smallsailh,iss_small_sail_tx);

    glPopMatrix();
  end;

  //------------------------------------------
  {
             z-Axis (+)                    y-Axis (+)
               |                              |
              A**       B**                   |
              ***       ***            20m    |   13m
      D#######***C######***        ##########A**C#######B**
      ########***#######***        ##########***########***
       20m    ***  13m  ***          G**   F**
              ***       ***          ***   ***
               |                     ***E##***
               |                     ***###***
               |   T-Part             U-PART  |
     ----------+--------           -----------+----- x-Axis (+)
  }

  //------------------------------------------
  procedure part_center;
  begin
    glPushMatrix();
      graphic_mode_bind(iss_small_tx,0.5,0.5,0.5);

      //A)body cylinder
      glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);

      //t-part-------------------------------------------

      //B)parallel cylinder right
      glTranslatef(-13-_cylr,0,0); //shift right
      glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);

      //C)vertical connection
      glTranslatef(1,0,0);    //shift left
      glTranslatef(0,0,13/2); //shift forward
      glRotatef(90,0,1,0);    //rotate y-axis
      glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);

      //u-part-------------------------------------------

      //D)cylinder left
      glTranslatef(0,0,20-_cylr-1); //shift left
      glucylinder(iss_quad,_cylr,_cylr,20,_cyls,_cyls);

      //E)cylinder paralell under
      glTranslatef(0,-13,0);  //shift down
      glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);

      //F)horizontal right
      glRotatef(90,1,0,0);
      glTranslatef(0,0,-11);  //shift up
      glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);

      //G)parallel left
      glTranslatef(0,11,0);
      glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);

      graphic_mode_off;
    glPopMatrix();
  end;

begin
  glPushMatrix();
    glTranslatef(iss_startv.x,iss_startv.y,iss_startv.z);

    //parts with big solar sail
    glRotatef(-iss_big_sail_rot*10,0,0,1);
    part_bigsolarsail;glTranslatef(0,0,13);
    glRotatef(-5,0,0,1);

    part_bigsolarsail;glTranslatef(0,0,13);
    glRotatef(+iss_big_sail_rot*10+5,0,0,1);

    //part width small solar sail
    glRotatef(iss_small_sail_rot,0,0,1);
    part_smallsolarsail;glTranslatef(0,0,13);
    glRotatef(-iss_small_sail_rot,0,0,1);

    //part in the middle width t- and u-part
        part_center;glTranslatef(0,0,13);

    //part width small solar sail
    glRotatef(iss_small_sail_rot,0,0,1);
    part_smallsolarsail;glTranslatef(0,0,13);
    glRotatef(-iss_small_sail_rot,0,0,1);

    //parts with big solar sail
    glRotatef(frequ_low*5,0,0,1);
    part_bigsolarsail;glTranslatef(0,0,13);
    glRotatef(5,0,0,1);
    part_bigsolarsail;glTranslatef(0,0,13);

  glPopMatrix();
end;

Die Konstruktion im 3-dimensionalen Raum ist relativ schwierig, weil nach jeder Rotation die Winkel in eine andere Richtung weisen, als man vielleicht vermuten würde.

Statt z.B. ein Element der ISS, etwa ein Solar-Segel, durch Verringerung des "z"-Wertes weiter nach hinten zu platzieren, passiert es dann, dass es nach einer Rotation von 90 Grad um die y-Achse weiter links landet. Tatsächlich müsste jetzt nämlich der "x"-Wert erhöht werden, um den gewünschten Effekt zu erreichen.

Aus diesem Grund habe ich in der Konstruktionsphase einen zusätzliche Positions- und einen zusätzlichen Rotations-Vektor verwaltet. Diese liessen sich über bestimmte Tastatur-Eingaben verändern. Vor der Platzierung eines ISS-Elements liess ich das Modell dann gemäss dieser Vektoren rotieren und transportieren. So war leicht zu erkennen, welcher Wert optisch was bewirkt.

Die Erläuterung, wie die ISS nun genau konstruiert wird, ist müssig und erspare ich uns hier.

Delphi-Tutorials - OpenGL ISS - Drawing of the ISS in the orbit of the Earth with OpenGL
Prozedur 'draw_iss': Wiedergabe der internationale Raumstation ISS in unserem OpenGL-Raum. Hier sieht man sie schräg von unten. Was man nicht sieht: Die grossen und kleinen Segel rotieren in periodischen Abständen stufenweise um den Rumpf.

3.9.2. Meteor-Handling

3.9.2.1. Platzierung der Killer-Meteore

Nach der ISS werden in "draw_scene" die Meteore gezeichnet - auf ihrem zerstörerischen Weg direkt zur Raumstation ...

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
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
//--------------------------------------------------
procedure thauptf.game_points_set;
begin
  game_pointsp.caption:=inttostr(game_points);
end;

//-----------------------------------------------------
procedure thauptf.draw_meteors;

  //-------------------------------------------
  procedure draw_meteor(r:integer);
  var
    pm:pmeteor;
  begin
    pm:=@meteor_a[r];

    glPushMatrix();
      glTranslatef(pm.posv.x,pm.posv.y,pm.posv.z);

      //exploding meteor?
      if pm.state=_meteor_state_explode then begin
        graphic_mode_bind(meteor_explode_tx,1,0.8,0.6);
        pm.radius:=pm.radius*1.3;
        pm.count:=pm.count-1;
        gluSphere(meteor_quad,pm.radius,10,10);
      end
      else begin
        graphic_mode_bind(meteor_tx,0.8,0.8,0.8);
        glRotatef(90+frequ_fast*4,1,1,0);
        gluSphere(meteor_quad,pm.radius,5,5);
      end;

      graphic_mode_off;
    glPopMatrix();

  end;

  //-------------------------------------------
  function gozero(r:integer):tv;
  var
    av,nv:tv;
  begin
    //get meteor position
    av:=meteor_a[r].posv;

    //norm direction
    nv:=v_norm(av);

    //adapt speed of change
    nv:=v_scale(nv,meteor_a[r].speed);

    //calculate new position
    av:=v_sub(av,nv);
    result:=av;
  end;

var
  r,rr:integer;
  diss1,diss2,dissmin,
  dme,dmemin,
  dshot:single;
  pm:pmeteor;
  ps:pshot;
  gameoverok:bool;
begin
  glPushMatrix();

    dissmin:=game_meteor_dim;
    dmemin:=game_meteor_dim;
    gameoverok:=true;

    //walk all meteors
    for r:=0 to game_meteor_c-1 do begin
      pm:=@meteor_a[r];
      if pm.state=_meteor_state_none then continue;

      //at least one active meteor: game is running
      gameoverok:=false;

      //meteor is destroyed?
      if(pm.state=_meteor_state_explode)and(pm.count=0) then begin

        //dirty meteor who destroyed the iss?
        if game_iss_meteor=r then begin
          draw_meteor(r);
          game_over_show;
          exit;
        end;

        //hide meteor
        meteor_imga[r].Visible:=false;
        pm.state:=_meteor_state_none;

        //more distance to iss gives more game_points
        dshot:=v_distance(iss_startv,pm.posv);
        game_points:=game_points+trunc(dshot)*10;
        game_points_set;
        continue;
      end;

      //let meteor fly to iss
      pm.posv:=gozero(r);

      //meteor is solid?
      if pm.state<>_meteor_state_explode then begin

        //mark nearest distance from iss
        diss1:=v_distance(iss_startv,pm.posv);
        if diss1>=pm.radius then diss1:=diss1-pm.radius;
        diss2:=v_distance(iss_endv,pm.posv);
        if diss2>=pm.radius then diss2:=diss2-pm.radius;

        if diss1<dissmin then dissmin:=diss1;
        if diss2<dissmin then dissmin:=diss2;

        //meteor hits iss?
        if LineInSphere(iss_startv,iss_endv,pm.posv,pm.radius,false)then begin
          cockpitp.Color:=clred;
          detect_issp.caption:=f2s_cut(0);
          if game_iss_meteor=-1 then begin
            game_iss_meteor:=r;
            flyhome;
            exit;
          end;
        end;
      end;

      //meteor is hitting by shot?
      for rr:=0 to _shot_c-1 do begin
        ps:=@shot_a[rr];if ps.count=0 then continue;
        if LineInSphere(ps.posv,ps.poslastv,pm.posv,pm.radius,false)then begin
          //yes, change state to explode
          pm.state:=_meteor_state_explode;
          pm.count:=10;
          dosound(sound_explode);
        end;
      end;

      //draw dirty meteor
      draw_meteor(r);

      //detect nearest meteor to me
      dme:=v_distance(posv,pm.posv);
      if dme>=pm.radius then dme:=dme-pm.radius;
      if dme<dmemin then dmemin:=dme;
    end;
  glPopMatrix();

  //iss meteor alarm?
  detect_issp.caption:=f2s_cut(dissmin);
  if dissmin<_meteor_alarm then begin
    meteorp.color:=clred;
    if game_iss_meteor=-1 then dosound(sound_alarm);
  end
  else begin
    meteorp.color:=clblack;
  end;

  //nearest meteor to me
  detect_mep.caption:=f2s_cut(dmemin);

  //all meteors destroyed?
  if gameoverok then game_over_show;
end;

Die maximale Anzahl aktiver Meteore wird Level-abhängig von "game_meteor_c" vorgegeben. In einer Schleife werden alle Meteore des "meteor_a"-Arrays einzeln abgerufen und über den Pointer "pm" angesprochen.

Meteore kennen drei Status ("pm.state"):

  1. "_meteor_state_none": Der Meteor ist inaktiv/zerstört
  2. "_meteor_state_fly": Der Meteor ist unterwegs zur ISS
  3. "_meteor_state_explode": Der Meteor ist gerade am explodieren

Hat der aktuell betrachtete Meteor den Status "_meteor_state_non", dann ist er inaktiv und wird nicht weiter beachtet.

Hat ein Meteor den Status "_meteor_state_explode", dann fliegt er gerade auseinander. Die Dauer der Explosion hängt vom Meteor-eigenem Zähler "pm.count" ab. Dieser wird zu Beginn auf einen bestimmten Wert hochgesetzt und innerhalb jeder Zeiteinheit um eins dekrementiert. Steht der Zähler schliesslich auf null, hat der Meteor ausgedient und verpufft ins Nichts.

Im Explosionsfall wird zusätzlich geprüft, ob die Zerstörung des Meteors durch einen Anwender-Schuss verursacht wurde oder durch Kollision mit seinem Ziel, der ISS.

Bei Explosion durch ISS-Kontakt endet das Spiel an dieser Stelle; es wird dann nur noch die Prozedur "game_over_show" aufgerufen.

Hat dagegen der Anwender den Meteor erfolgreich zerstört, bekommt er Punkte auf seinem Konto gutgeschrieben. Dabei gilt, dass er umso mehr Punkte bekommt, je weiter der Meteor von der ISS entfernt war.

Ist der Meteor dagegen noch fröhlich unterwegs, wird über die interne Prozedur "gozero" seine Position neu berechnet. Diese wird so kalkuliert, dass der Meteor sich der ISS unweigerlich nähert.

Falls der Meteor noch völlig intakt sein sollte ("pm.state ungleich _meteor_state_explode"), wird sein aktueller Abstand zur ISS berechnet. Die dabei gefundene geringste Distanz aller Meteore wird in "dissmin" gemerkt.

Weiterhin bleibt zu prüfen, ob der Meteor von einem Anwender-Schuss getroffen wurde. Dazu werden in einer Schleife alle Schüsse des "shot_a"-Arrays durchgegangen.

Steht der Zähler "ps.count" des aktuell betrachteten Schusses auf null, befindet er sich noch im Magazin und wird daher ignoriert.

Ansonsten wird seine aktuelle Position ("ps.posv") und seine vorherige Position "ps.poslastv" als Anfang und Ende einer Linie interpretiert. Die Funktion "LineInSphere" testet anschliessend, ob diese Linie an irgendeiner Stelle die Meteor-Kugel berührt.

Ist dies der Fall, wurde der Meteor getroffen. Er wechselt auf den Status "_state_meteor_explode" und bekommt seinen Explosions-Zähler "pm.count" hochgesetzt.

Es folgt die Ausgabe des Meteors über die interne Prozedur "draw_meteor". Je nach Status wird eine passende Textur vergeben und im Explosionsfall zusätzlich der Radius vergrössert sowie der Explosions-Zähler dekrementiert.

Delphi-Tutorials - OpenGL ISS - Drawing of some meteors on their collision curse to the ISS
Prozedur 'draw_meteors': Alle Meteore werden über nur eine Prozedur im OpenGL-Raum wiedergegeben. Hier hat einer dieser Gesteinsbrocken gerade die ISS erwischt - und explodiert nun als hell erleuchtete Feuerkugel.

Schliesslich wird noch der Abstand zu unserem Raumschiff berechnet. Die so gefundene geringste Distanz aller Meteore wird in "dmemin" gesichert und später im TPanel ""detect_mep" ausgegeben:

Delphi-Tutorials - OpenGL ISS - Meteor Detector spaceship (ME)
Meteor-Detektor Raumschiff (ME): Die MD-ME-Anzeige zeigt an, wie weit der nächste Meteor von unserem Raumschiff entfernt ist. Man kann diese Anzeige daher nutzen, um die Meteore in den Weiten des Alls aufzuspüren. So kann man sie gezielt vernichten, bevor sie die ISS erreichen.

Nachdem schliesslich alle Meteore abgearbeitet wurden, bleibt nur noch zu prüfen, ob der der ISS nächste Meteor den kritischen Wert von "_meteor_alarm" Metern unterschritten hat. Ist dem so, wird ein Alarmsignal initiiert.

Delphi-Tutorials - OpenGL ISS - Meteor Detector ISS
Meteor-Detektor ISS: Die MD-ISS-Anzeige zeigt an, wie weit weg sich der nächste Meteor von der ISS befindet. Hier ist einer dieser Gesteinsbrocken gerade innerhalb des kritischen Bereichs um die ISS geflogen - und der Detektor heult sofort los.
3.9.2.2. Ballerei - Linie trifft Sphäre

Den folgenden Source habe ich im Web gefunden und nur etwas für meine Zwecke adaptiert.

Die in C++ geschriebene Funktion hiess ursprünglich "LineHitsSphere" und lieferte nur dann "true" zurück, wenn die Linie die Kugel komplett durchquerte.

Mir genügt es, wenn auch nur Teile der Linie irgendwo im Inneren der Kugel auftauchen. Daher heisst das Teil bei mir auch "LineInSphere":

Delphi-Tutorials - OpenGL ISS - Line in sphere case 1
Funktion 'LineInSphere' - Fall 1: Der Vektor 'v' berührt die Sphäre 'S' an keiner Stelle. 'LineInSphere' liefert 'false' zurück.
Delphi-Tutorials - OpenGL ISS - Line in sphere case 2
Funktion 'LineInSphere' - Fall 2: Der Vektor 'v' beginnt im Inneren der Sphäre 'S', durchkreuzt sie also nicht vollständig. 'LineInSphere' liefert in diesem Fall nur dann 'true' zurück, wenn 'crossok' inaktiv ist.
Delphi-Tutorials - OpenGL ISS - Line in sphere case 3
Funktion 'LineInSphere' - Fall 3: Der Vektor 'v' durchquert die Sphäre 'S' vollständig. 'LineInSphere' liefert unabhängig von 'crossok' stets 'true' zurück.
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
// check if a line starts in sphere or cross the sphere.
// crossok is true: result only true if line cross the sphere completly
// http://www.koders.com/cpp/fidF5B34643282700B33C8E7A9A45877FD5F77D9FB8.aspx?s=cdef%3Atree
function thauptf.LineInSphere(v1,v2,spherev:tv;radius:single;crossok:bool):bool;
var
  dirv,diffv:tv;
  linelen,a,b,Discriminant,root,cross:single;
begin
  result:=false;

  // Länge und normalisierte Richtung der Linie berechnen
  //Vector3D LineDir(LineB - LineA);
  //const float LineLength = LineDir.Length();
  //LineDir /= LineLength;

  //direction of line
  dirv:=v_sub(v2,v1);
  linelen:=v_len(dirv);

  //norm line direction
  dirv:=v_norm(dirv);

  // Die zwei Hilfsvariablen a und b berechnen
  //const Vector3D Diff(LineA - Sphere);
  diffv:=v_sub(v1,spherev);

  {
  const float a = 2.0f * (LineDir.x * Diff.x +
                          LineDir.y * Diff.y +
                          LineDir.z * Diff.z);
  }

  a:=2*(dirv.x*diffv.x+dirv.y*diffv.y+dirv.z*diffv.z);


  //const float b = Diff.LengthSq() - Radius * Radius;
  b:=v_len(diffv);
  b:=b*b-radius*radius;

  // Die Diskriminante berechnen (a²/4 - b)
  //const float Discriminant = ((a * a) * 0.25f) - b;
  Discriminant:=((a*a)*0.25)-b;

  // Wenn die Diskriminante kleiner als null ist,
  // dann gibt es keine Schnittpunkte.
  if Discriminant<0 then exit;

  // Die Wurzel und danach die zweite Lösung der Gleichung berechnen
  //const float Root = sqrtf(Discriminant);
  root:=sqrt(Discriminant);

  //const float s = a * -0.5f - Root;
  cross:=-a/2-root;

  if cross>LineLen then exit;

  // Ist dieser Schnittpunkt in Ordnung?
  //nur true, wenn line ganz durchgeht
  //if(cross<0)or(cross>LineLen)then exit;
  //true
  if crossok and(cross<0)then exit;

  result:=true;
end;

Ich habe mir nicht die Mühe gemacht, den Source völlig zu verstehen. Daher gibt es hier auch keine weitere Erklärung dazu.

3.9.2.3. Schuss-Salven

Wie wir im Kapitel "Wenn der User drückt" gesehen haben, bewirkt die "Space"-Taste, dass die Prozedur "shot" aufgerufen wird. Hier werden die Start-Parameter der Schüsse berechnet, die in "draw_scene" mittels Prozedur "draw_shots" ausgegeben werden.

Betrachten wir zunächst "shots":

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
//generate a shot-circle----------------------------------------
procedure thauptf.shot;
const
  _max=10;
var
  v:tv;
  c,r:integer;
  deg:single;
  ps:pshot;
begin
  dosound(sound_shot);

  v:=getlookv;

  for c:=0 to _max-1 do begin

    //next free shot
    ps:=nil;
    for r:=0 to _shot_c-1 do begin
      if shot_a[r].count=0 then begin
        ps:=@shot_a[r];
        break;
      end;
    end;
    if ps=nil then break;

    //direction & start-position
    ps.dirv:=v;
    ps.posv:=posv;
    ps.count:=20;

    if c=0 then begin
      //after first shot adapt angle for circle
      v:=v_rot_y_deg(v,2);
      v:=v_rot_x_deg(v,2);
    end;

    //calculate angles for circle
    deg:=(c+1)*360/_max;
    v:=v_rot_y_deg(v,sin(deg)*5);
    v:=v_rot_x_deg(v,cos(deg)*5);
  end;
  inc(shot_c);

  //every shot costs game_points
  game_points:=game_points-100;
  if game_points<0 then game_points:=0;
  game_points_set;
end;

Ähnlich wie beim Geradeausflug wollen wir, dass die Geschosse exakt in diejenige Richtung fliegen, die unser Blick in den Raum hinein vorgibt.

Dazu holen wir uns den aktuellen Richtungsvektor - also den ursprünglichen Richtungsvektor (0,0,-1) rotiert um alle Achsen - über die bereits beschriebene Funktion "getlookv".

Jetzt wird das Schuss-Array "shot_a" durchlaufen und nach insgesamt "_max" (=10) freien Geschossen durchsucht. Finden wir keine, können wir nichts abfeuern und müssen warten, bis die letzten, noch aktiven Schüsse abgeklungen sind.

Haben wir ein freies Geschoss "ps" gefunden, so erhält es unsere aktuelle Position ("ps.posv") und die berechnete Richtung ("ps.dirv"). Der Zähler "ps.count" wird hochgesetzt und gibt an, wie viele Zeiteinheiten der Schuss unterwegs sein wird.

Der erste Schuss fliegt exakt in die Mitte, die weiteren Schüsse werden so berechnet, dass sie sich kreisförmig um das Zentrum anordnen. Dazu nutzen wir die Formeln für die Vektor-Rotation um x- und y-Achse.

Am Schluss werden dem Spieler noch Punkte abgezogen. Man sollte also nicht zu wahllos herumschiessen, wenn man ernsthaft einen neuen Highscore anvisiert.

Und nun die Ausgabe der Geschosse durch die Prozedur "draw_shots":

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
//draw the shot-circle-------------------------------------------
procedure thauptf.draw_shots;
var
  r:integer;
  v:tv;
  ps:pshot;
begin
  graphic_mode_bind(meteor_tx,1,1,0.5);
  for r:=0 to _shot_c-1 do begin
    ps:=@shot_a[r];

    //shot on the qay?
    if ps.count=0 then continue;

    //save actual position
    v:=ps.posv;
    ps.poslastv:=v;

    //calculate new position
    v:=v_add(v,ps.dirv);
    ps.posv:=v;

    //speed up shot
    ps.dirv:=v_scale(ps.dirv,1.5);

    //decrement activity-counter
    ps.count:=ps.count-1;

    //draw shot
    glTranslatef(v.x,v.y,v.z);
    gluSphere(meteor_quad,1,5,5);
    glTranslatef(-v.x,-v.y,-v.z);

  end;
  graphic_mode_off
end;

Das Schuss-Array "shot_a" wird durchlaufen und nach aktiven Geschossen ("count ungleich 0") durchsucht. Werden keine gefunden, gibt es nichts weiter zu tun und wir verlassen die Prozedur wieder.

Aktive Geschosse erhalten den Pointer "ps". Wir sichern die aktuelle Position des Schusses in "ps.poslastv". Die neue Position ergibt sich durch Vektor-Addition der aktuellen Position mit dem Richtungsvektor, der in der Prozedur "shots" in "ps.dirv" eingetragen wurde.

Die letzte Position und die neue aktuelle Position des Schusses beschreiben eine virtuelle Linie, die das Geschoss innerhalb der letzten Zeiteinheit durchflogen hat. Diese Linie wird verwendet, um festzustellen, ob ein Meteor getroffen wurde (siehe Kapitel "Angriff der Killer-Meteore" und "Linie trifft Sphäre").

Mittels "v_scale", angewendet auf "ps.dirv", sorgen wir weiterhin dafür, dass das Geschoss zunehmend schneller wird.

Der Zähler "ps.count" wird dekrementiert. Hat er Null erreicht, wissen wir später, dass das Geschoss verpufft ist. Es steht dann für den nächsten Schuss wieder frei zur Verfügung.

Zuletzt wird der Schuss-Salve dann über die OpenGL-Prozedur "gluSphere" in den Grafikpuffer gemalt.

Delphi-Tutorials - OpenGL ISS - Drawing the shots of our spaceship
Prozedur 'draw_shots': Wiedergabe der Schüsse, die von unserem Raumschiff ausgehen. Hier ist gerade eine Schuss-Salve unterwegs zur ISS, was glücklicherweise aber keine negativen Auswirkungen auf dieselbe hat.

3.9.3. Spurensuche

Kehren wir zu "draw_scene" zurück. Weltall, Erde und ISS wurden gezeichnet. Die Meteore, so weit vorhanden, neu positioniert. Unsere Schüsse sind unterwegs. Nun folgt die Prozedur "draw_path":

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
//set new path-point----------------------------------------------------
procedure thauptf.path;
begin
  path_a[path_c]:=posv;
  inc(path_c);if path_c>_path_c-1 then path_c:=0;
end;

//draw the path in fast triangle mode------------------------------------
procedure thauptf.draw_path;
var
  r:integer;
begin
  glPushMatrix();
    for r:=0 to _path_c-1-1 do begin
      if(path_a[r].x=0)or(path_a[r+1].x=0) then continue;

      //change color
      if r mod 2=0 then glColor3f(1.0,0.5,0.5)
                   else glColor3f(0.5,1.0,0.5);

      glBegin(GL_TRIANGLE_STRIP);
        glVertex3f(path_a[r].x,path_a[r].y,path_a[r].z);
        glVertex3f(path_a[r].x+1,path_a[r].y+1,path_a[r].z+1);
        glVertex3f(path_a[r+1].x,path_a[r+1].y,path_a[r+1].z);
      glEnd();
    end;
    glColor3f(1.0,1.0,1.0);
  glPopMatrix();
end;

Hier wird eine Art Kondensstreifen aufgebaut, der veranschaulicht, welchen Weg unser Raumschiff zurückgelegt hat. Die Pfadpunkte des Arrays "path_a" werden dazu in der Prozedur "path" gesetzt, welche periodisch über die Prozedur "frequencytTimer" aufgerufen wird.

Zur Anzeige des Pfades wird dann in "draw_path" einfach das "path_a"-Array durchlaufen und aus aktuellem und folgendem Punkt ein Dreieck generiert, was gemäss seiner Koordinaten als OpenGL-Grafik vom Typ "GL_TRIANGLE_STRIP" wiedergegeben wird.

Über einen Modulo-Vergleich mit der Schleifenvariablen "r" wird zusätzlich dafür gesorgt, dass die Dreiecke abwechselnd rot und grün gezeichnet werden.

Delphi-Tutorials - OpenGL ISS - Drawing of the flight path of our spaceship
Prozedur 'draw_path': Der Weg, den unser Schiff innerhalb eines bestimmten Zeitraumes im Raum zurückgelegt hat, kann optional angezeigt werden.

3.9.4. Den Feind ins Visier genommen

Als letzte "draw_"-Aktion in "draw_scene" fehlt jetzt nur noch das Visier mit dem Fadenkreuz, welches angibt, wohin wir beim Geradeaus-Flug steuern bzw. schiessen. Diesen Job übernimmt die Prozedur "draw_visor":

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
//visor-------------------------------------------------------
procedure thauptf.draw_visor;
var
  rd:integer;
begin
  glPushMatrix();
    gldisable(GL_DEPTH_TEST);
    glLoadIdentity;

    glTranslatef(0,0,-20);
    glLineWidth(2);
    rd:=3;

    //cross
    glLineWidth(1);
    glBegin(GL_LINES);
      glVertex3f(-rd,-rd,rd);glVertex3f(rd,rd,rd);
      glVertex3f(rd,-rd,rd);glVertex3f(-rd,rd,rd);
    glEnd();

    //frame
    glLineWidth(2);
    glBegin(GL_LINE_LOOP);
      glVertex3f( rd, rd, rd);
      glVertex3f(-rd, rd, rd);
      glVertex3f(-rd,-rd, rd);
      glVertex3f( rd,-rd, rd);
    glEnd();

    glLineWidth(1);
    glenable(GL_DEPTH_TEST);
  glPopMatrix();
end;

Der Tiefenpuffer wird abgeschaltet, damit das Fadenkreuz nicht von weiter vorne liegenden Objekten verdeckt wird.

Anschliessend wird der OpenGL-"Malstift" 20 Meter vor uns positioniert. Dorthin wird dann das Fadenkreuz nebst einem Rahmen platziert.

Delphi-Tutorials - OpenGL ISS - Drawing the visor of the cockpit of our spaceship
Prozedur 'draw_visor': Wiedergabe des Visiers im Cockpit unseres Raumschiffes. Hier wurde gerade ein Meteor in die Mitte genommen. Bald hat er sein Leben ausgehaucht. Und das möglichst, bevor er die ISS erreicht ...

3.9.5. Automatischer Heimflug

Ein paar Mal wurde der automatische Heimflug bereits erwähnt.

Wenn wir uns zu weit von der ISS entfernt oder die Orientierung verloren haben, dann genügt ein Druck auf die "F"-Taste, und wir werden vom Bordcomputer wieder in die Ausgangsposition direkt vor der ISS gebracht.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
//-----------------------------------------------------------
procedure thauptf.flyhome;
begin
  flyhome_c:=100;

  //delta position
  pspeed:=1;
  dposv.x:=-(posv.x)/flyhome_c;
  dposv.y:=-(posv.y)/flyhome_c;
  dposv.z:=-(posv.z-_home_pos_z)/flyhome_c;

  //delta rotation
  rx_speed:=(rotv.x)/flyhome_c;
  ry_speed:=(rotv.y)/flyhome_c;
  rz_speed:=(rotv.z)/flyhome_c;
end;

Obige Prozedur "flyhome" setzt den Zähler "flyhome_c" hoch.

Abschliessend werden die Positions- und Rotations-Deltas berechnet, die nötig sind, um uns aus aktueller Position und Richtung in "flyhome_c" Zeiteinheiten bis zur Start-Position zurückzuführen.

Um möglichst viele Spiel-Punkte zu ergattern, muss man sich im Spiel möglichst weit von der ISS entfernen und Meteore zerstören. Dabei sollte man gut den Meteor-Detector im Auge behalten. Einige Sekunden, bevor der losheult, kann man dann über den automatischen Heimflug rasch zur ISS zurückkehren.

Man beachte jedoch, dass, während der Bordcomputer das Kommando hat, keinerlei Aktionen des Piloten möglich sind ...

3.10. Spiel-Ende

Das Spiel ist aus, wenn ...

  1. ... die "Escape"-Taste gedrückt wurde
  2. ... alle Meteore zerstört sind
  3. ... die ISS von einem Meteor getroffen wurde

In allen drei Fällen wird die Prozedur "game_over_show" 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
00041
//--------------------------------------------------
procedure thauptf.game_over_show;
var
  hs:integer;
  s:string;
begin
  if game_level=_game_level_train then begin
    //no high score needed
    game_start_show;
    exit;
 end;

  if game_iss_meteor<>-1 then game_status:='THE ISS IS GONE AWAY :-('
                         else game_status:='YOU LUCKY BASTARD WIN :-)';

  if game_level=_game_level_rookie then begin
    if game_points>game_hs_rookie then game_hs_rookie:=game_points;
    s:='Rookie';
    hs:=game_hs_rookie;
  end
  else if game_level=_game_level_advanced then begin
    if game_points>game_hs_advanced then game_hs_advanced:=game_points;
    s:='Advanced';
    hs:=game_hs_advanced;
  end
  else if game_level=_game_level_profi then begin
    if game_points>game_hs_profi then game_hs_profi:=game_points;
    s:='Profi';
    hs:=game_hs_profi;
  end
  else begin
    if game_points>game_hs_mad then game_hs_mad:=game_points;
    s:='Mad';
    hs:=game_hs_mad;
  end;

  game_pointsl.Caption:=inttostr(game_points);
  game_levell.caption:='LEVEL '+s+' HIGHSCORE';
  game_hsl.caption:=inttostr(hs);
  gameoverp.visible:=true;
end;

Ist der Spiel-Level "_game_level_train" aktiv, wird über die Prozedur "game_start_show" direkt zum Start-Panel weitergeleitet.

Ansonsten wird der Level-Highscore ermittelt und gegebenenfalls mit dem aktuellen Punktestand überschrieben.

Zuletzt wird dann einfach das TPanel "gameoverp" sichtbar gemacht.

Delphi-Tutorials - OpenGL ISS - Game over - you win
Prozedur 'game_over_show' - Fall 1: Spielende. Und diesmal war es das Ende einer erfolgreichen Schlacht.
Delphi-Tutorials - OpenGL ISS - Game over - you lose
Prozedur 'game_over_show' - Fall 2: Spielende. Du hast verloren. Und damit auch der ISS ein trauriges Ende bereitet.

4. Spiel-Impressionen

Zum Schluss noch ein paar Screenshots von "OGL_ISS" in Action:

Delphi-Tutorials - OpenGL ISS - Game Impression #01
Delphi-Tutorials - OpenGL ISS - Game Impression #02
Delphi-Tutorials - OpenGL ISS - Game Impression #03
Delphi-Tutorials - OpenGL ISS - Game Impression #04
Delphi-Tutorials - OpenGL ISS - Game Impression #05
Delphi-Tutorials - OpenGL ISS - Game Impression #06
Delphi-Tutorials - OpenGL ISS - Game Impression #07
Delphi-Tutorials - OpenGL ISS - Game Impression #08
Delphi-Tutorials - OpenGL ISS - Game Impression #09
Delphi-Tutorials - OpenGL ISS - Game Impression #10
Delphi-Tutorials - OpenGL ISS - Game Impression #11
Delphi-Tutorials - OpenGL ISS - Game Impression #12
Delphi-Tutorials - OpenGL ISS - Game Impression #13
Delphi-Tutorials - OpenGL ISS - Game Impression #14

5. Träume werden wahr

Ach ja, ich liebe OpenGL!

Als Kind hatte mich auf dem C64 nichts mehr fasziniert, als Vektor-Grafik-Spiele wie "Elite" und "Escape from Mercenary". Monatelang habe ich mich damals damit beschäftigt, deren virtuelle Welten zu erforschen. Nicht in meinen kühnsten Träumen hätte ich mir jedoch zugetraut, je auch nur etwas annähernd Ähnliches selbst programmieren zu können.

Klar, "OGL_ISS" hat bei Weitem nicht die Komplexität oder den Spielwitz der grossen Vorbilder. Aber optisch schlägt es sie allemal.

Ecken und Macken gibt es dennoch genug, obwohl "OGL_ISS" für meine Verhältnisse eigentlich ziemlich fehlerfrei geraten ist.

Die Flug-Steuerung macht manchmal nicht das, was sie tun sollte. Möglich, dass das mit dem berüchtigten "Gimbal Lock" zu tun hat - ein Problem, mit dem offenbar sogar die NASA-Ingenieure zu kämpfen haben.

Des Weiteren purzeln die Sound-Effekte gerne wüst durcheinander.

Und die Highscore-Listen - hüstel! - sind mit nur einem Eintrag je Level wahrlich auch auch mehr als bescheiden ausgefallen ...

So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy. Ist halt nichts 100%iges. But who cares?

6. Download

"OGL_ISS" wurde mit Delphi 7 programmiert. Der komplette Source, die EXE und die Bibliotheksdatei "opengl32.dll" sind alle in diesem ZIP-Archiv verpackt:

OGL-ISS.zip (ca. 2,6 MB)

Das Original-OpenGL-Paket für Delphi habe ich von "http://www.delphigl.com" gesaugt. Das ZIP-File des Installers der Version "DGLSDK 2006.1", die ich verwendet habe, findet ihr hier:

dglsdk-2006-1.zip (ca. 8 MB)

Have fun!