Home

News

Software
  - HTML
  - DHTML
  - Javascript
  - CGI
  - VRML
  - Linux
  - Dirty-Progs
    - VidSplitt
    - OpenGL Henrys
    - PicOfPics
    - OpenGL Planets
    - OpenGL ISS
    - MediaPanelyzer
    - Pixel-Evolution
    - CPU-Eater
    - FLV-CCC
    - Sprite-Painter
  - PHP
    - Hilfsfunktionen
    - Volltext-Suche
    - Src2Textarea

Bilder

Texte

Alles fliesst

Comics

Musik

Leben

Links

Sitemap

Admin


Wissenswertes bei
Wikipedia

OGL_Planets - Ein Ausflug in 's eigene Sonnensystem

OGL_Planets-Tutorial von Daniel Schwamm (31.03.2008)

Aus "Heimat des Dilettantismus"
http://www.henrys.de/daniel/index.php?cmd=software_dirty-progs_opengl-planets_index.htm

OpenGl Planets - Blick über den Rand der Erdscheibe

Alte Liebe rostet nicht

Demo-Movie #1: Vorbeiflug an Erde

Demo-Movie #2: HyperMove-Flug zur Sonne

Demo-Movie #3: HyperMove-Flug zum Saturn

Wenn mich je ein Gebiet konstant in Bann geschlagen hat, dann die Astronomie. Seit frühester Jugend begeistert mich die Welt der Sterne, Planeten, Kometen und Asteroiden. Nicht, dass sich dadurch viel Wissen angesammelt hätte. Muss es auch nicht; bin ja Programmierer, kein Astronom.

Trotzdem stört es mich, wenn bei den weit aus meisten Abbildungen der Planeten die relativen Grössen und Abständen zueinander ignoriert werden. Praktisch immer sind die Planeten viel zu dicht aneinandergedrängt und im Vergleich zur Sonne zu gross dargestellt.

Seit meinen Uni-Tagen - inzwischen über 10 Jahre her - schlepp ich daher den Plan mit mir herum, das Sonnensystem mit mehr Realismus auf Papier zu bringen. Nur um zu sehen, wie die Verhältnisse wirklich sind. Doch konnte ich mich nie dazu aufraffen, dem Gedanken Taten folgen zu lassen.

Papier-Tiger

Um meinen 40sten in Ruhe zu verbringen, nahm ich mir im Januar 2008 Urlaub. Ruhe ist aber öde. So gab ich schnell dem Impuls nach, als ich eine Doku über die Sonne sah: Mit Brockhaus und Lineal bewaffnet begann ich das Sonnensystem schwarz auf weiss auf Papier zu bannen.

Mit Erde und Mond fing es an. Da die Erde einen Radius von 6.400 km hat, bekam meine gemalte etwas über einem Zentimeter ab. Überrascht stellte ich fest, dass der Mond bereits auf's nächstes Blatt auszulagern war. Die 385.000 km Abstand zwischen Erde und Mond ergaben in meiner Skala nämlich fast 40 cm! Der Mond ist echt nicht gerade um's Eck!

OpenGl Planets - Erde und Mond benötigen mehr als ein Blatt

Erde und Mond: Mit einem Blatt ist's nicht getan


Dann guckte ich mir die Daten der Sonne an. Mir war schon klar, dass die Sonne grösser als die Erde ist. Aber dass sie gleich so ein fetter Brocken ist ...

Ihr Radius von 700.000 km zwang mich, noch ein Blatt anzulegen, wenn Erde und Sonne den gleichen Mittelpunkt bekommen sollten. Und die Krümmung der Sonnenkugel konnte nur angedeutet werden; mit einem handelsüblichen Zirkel satte 70 cm aufspannen ist nicht drin.

OpenGl Planets - Erde und Mond und Sonne brauchen Plaaaaatz

Erde und Mond und Sonne: Drei Blätter und doch nur ein Ausschnitt


Okay, die Grössenverhältnisse meiner drei Spieler waren zurechtgerückt. Den Abstand Erde-Mond hatte ich vor Augen. Jetzt suchte ich im Brockhaus nach der durchschnittlichen Strecke zwischen Erde und Sonne. Und merkte gleich, dass ich ein Problem hatte. Ein Riesen-Problem, gewissermassen. Denn die schwindelige Strecke von 150.000.000 km ergab nach Adam Riese lockere 150.000.000/10.000=15.000 cm, also 150 m. So viele Blätter hatte ich im ganzen Haus nicht.

Jetzt wollte ich's wissen. Die Sache begann Spass zu machen. Um die Sonne komplett auf ein Din A4-Blatt zu bringen, schrumpfte ich sie weiter ein, bis sie einen Radius von nur mehr 7 cm hatte. Mit dem Lineal zog ich eine Linie über das erste Blatt, dann ein zweites, ein drittes. Und hatte umgerechnet gerademal 3 Mio km zurückgelegt. 3 von 150 - bis zur Erde fehlten also immer noch 50 Blätter.

Klar, 150 durch 10, macht 15 Meter. Ne, so gross ist mein Wohnzimmer nicht. Ich liess es bleiben.

OpenGl Planets - Der Abstand Erde-Sonne ist gigantisch

Abstand Erde-Sonne: Nur für Leute mit grossen Wohnungen auf Papier zu bringen


Tja ... wie dachte ich am Anfang? Der Mond ist weit weg von der Erde? Das ist ein Katzensprung. Die Sonne ist weit weg von der Erde. Obwohl auch das natürlich relativ ist. Die Erde gehört den sogenannten inneren Planeten an. Richtig, richtig weit weg sind erst die äusseren, ab Jupiter aufwärts. Für Pluto hätte ich einen Berg von 2000 Blättern benötigt. Fast 600 Meter. Bei der ersten Skala also 6 km! Das nun ist definitiv kein Werk mehr für's Wohnzimmer.

Und ich begann zu verstehen, warum in all den Büchern die Planetenabstände nicht realisisch wiedergegeben werden...

Digitaler Ausweg

Mit Blättern wurde das nichts, das war klar. Aber Computer können mehr verdauen. Riesen-Dimensionen spielen dort eigentlich keine Rolle; virtuell ist geduldig. Sollte doch mit dem Teufel zu gehen, wenn ich zu dem Thema nichts im Web finden sollte.

Tja, gefunden habe ich einiges, aber nichts, was mich so richtig überzeugt hätte. Hübsche Animationen bei Youtube etwa, die zeigten, wie gross, grösser, am grössten, schliesslich sogar die Sonne zum Zwerg wird im Vergleich zu dicken Schwestern wie Arcturus oder Rigel.

OpenGl Planets - YouTube-Video: Planets and stars size in scale

YouTube-Video: Planets and stars size in scale


Eine andere Page zeigt die Sonne links im Eck. Mittels Scrollbalken kann man nun ewig weit nach rechts wandern, bis irgendwann die Erde auftauchte. Die Seite behauptet folgerichtig, eine der breitesten im gesamten Web zu sein.

OpenGl Planets - Proportional Representation of the Solar System

Proportional Representation of the Solar System: Wer auf scrollen steht ist hier richtig


Da OGL derzeit ein Steckenpferd von mir ist, googelte ich nach OGL-Demos des Sonnensystems. Ein Flug von Planet zu Planet bietet sich in OpenGL doch geradezu an. Gefunden habe ich aber nur Programme, bei denen Abstände und Grössen der Himmelskörper logarithmisch gestaucht waren - oder überhaupt nichts mit den realen Verhältnisen zu tun hatten.

Der Ausflug in's Web hat sich dennoch gelohnt. Denn dadurch stiess ich auf eines der genialsten Programme, die mir je unter gekommen sind: "Stellarium". Das simuliert ein Planetarium auf freiem Feld. Mit Nachthimmel und realitätsgetreuem Abbild der Sternenkonstallationen. Man kann in alle Richtungen schauen und mittels Mausrad beliebig tief in's All zoomen. Tolle Sache. Hat mich stundenlang gefesselt. Und ist Freeware!

OpenGl Planets - Stellarium: Geniale OGL-Freeware für Sternensüchtige

Stellarium: Geniale OGL-Freeware für Sternensüchtige


Was raus will muss raus

Okay, ich hatte kein brauchbares OGL-Solarsystem im Web gefunden. Also hiess es selbermachen. Hatte ja Urlaub. Und bevor ich's nicht wenigstens versucht hätte, würde ich ohnehin keine Ruhe mehr finden.

Futter für den Delphi-Compiler

Mit der Kombination Delphi und DelphiGL habe ich bei OGL_HENRYs gute Erfahrung gemacht. Also griff ich erneut darauf zurück. Und mit einer TForm begann die Reise, dorthin, wo noch nie ein Mensch war ...

OpenGl Planets - Hauptform in Delphi

Hauptform von "OGL_Planets": Hilfe-Memo, Cockpit-Instrumente & Taktgeber


Es folgte eine laaaange Reihe von Konstanten:

unit hauptu;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs,jpeg, ExtCtrls, StdCtrls, ComCtrls,inifiles,math,XPMan,mmsystem,
  DGLOpenGL,SDL,SDL_Image;

const
  _cap='OGL_PLANETS V1.0 (www.daniel-schwamm.de)';
  _inifn='ogl_planets.ini';
  _einheit='tkm';
  _rstep=10;          //rotationsschritte
  _piover180=pi/180;

  //raumstaub--------------------------------
  _staub_c   =80000; //max anzahl
  _staub_dim =5000;  //verbreitungs-dimension
  _haufen_c  =100;   //anzahl staubhaufen
  _haufen_dim=500;   //dimension jedes staubhaufens

  //asteroiden------------------------------
  _asteroid_c=400;
  _asteroid_dim=400;

  //hypermove--------------------------------
  _hypermove_len=6;

  //autopilot-------------------------------
  _ap_stop    = 0;
  _ap_richtung =1;_ap_richtung_steps =100;
  _ap_direkt   =2;_ap_direkt_steps   =100;_ap_direkt_distance=10000;
  _ap_hmstart  =3;_ap_hmstart_steps  =100;
  _ap_hmflug   =4;_ap_hmflug_steps   =100;
  _ap_hmbremsen=5;_ap_hmbremsen_steps=200;
  _ap_break    =6;_ap_break_steps    =40;
  _ap_ende     =7;

  //sounds----------------------------------
  _snd_stop       =0;
  _snd_start      =1;
  _snd_slow       =2;
  _snd_fast       =3;
  _snd_bremsen    =4;
  _snd_fastbremsen=5;
  _snd_break      =6;
  _snd_teleport   =7;

  //=========================================
  //sonne/planeten/monde---------------------
  //radius und abstand zur sonne in tkm
  _sonne_r       = 700;
  _sonne_z       = 0;

  _merkur_r      = 2.440;
  _merkur_z      = 58000;

  _venus_r       = 6.052;
  _venus_z       = 108200;

  //-----------------------------------------
  _erde_r        = 6.378;
  _erde_z        = 149000;

  _mond_r        = 2.400;
  _mond_z        = _erde_z-384.403;

  //-----------------------------------------
  _mars_r        = 3.375;
  _mars_z        = 227000;

  _deimos_r      = 1;
  _deimos_z      = _mars_z-23.459;

  _phobos_r      = 1;
  _phobos_z      = _mars_z-9.378;

  //-----------------------------------------
  _ceres_r       = 1;
  _ceres_z       = 413940;

  //-----------------------------------------
  _jupiter_r     = 71;
  _jupiter_z     = 778330;

  _kallisto_r    = 2.41;
  _kallisto_z    = _jupiter_z-1882.700;

  _ganymed_r     = 2.631;
  _ganymed_z     = _jupiter_z-1070.400;

  _europa_r      = 1.56;
  _europa_z      = _jupiter_z-670.900;

  _io_r          = 1.8;
  _io_z          = _jupiter_z-421.300;

  //-----------------------------------------
  _saturn_r      = 60;
  _saturn_z      = 1429400;
  _saturn_ring_r = 173;

  _titan_r       = 2.575;
  _titan_z       = _saturn_z-1221.830;

  _rhea_r        = 1;
  _rhea_z        = _saturn_z-527.040;

  //-----------------------------------------
  _uranus_r      = 25;
  _uranus_z      = 2870000;

  _oberon_r      = 1;
  _oberon_z      = _uranus_z-583.519;

  _titania_r     = 1;
  _titania_z     = _uranus_z-463.300;

  //-----------------------------------------
  _neptun_r      = 24;
  _neptun_z      = 4504300;

  _triton_r      = 1;
  _triton_z      = _neptun_z-354.760;

  //-----------------------------------------
  _kuiper_r      = 1;
  _kuiper_z      = 5500000;

  //-----------------------------------------
  _pluto_r       = 1.2;
  _pluto_z       = 5913520;

  _charon_r      = 1;
  _charon_z      = _pluto_z-20;

  //sichtweite--------------------------------------------
  _NearClipping=1;  // sehe objekte ab _einheit enfernung
  _FarClipping=-1;  // sehe bis unendlich


Die Sonne liegt im Ursprung des Modells, auf 0/0/0. Dann folgen wie auf einer Perlenschnur aufgereiht die Planeten mit ihren vorgelagerten Monden. Die Radienwerte und mittleren Abstände zur Sonne habe ich aus Wikipedia gefischt. Enden lassen wir unser Solarsystem mit Pluto. Danach kommt nur noch Leere.

Anfangs habe ich versucht, alle Werte 1:1 in's Modell einzutragen, z.B. der Sonne einen Radius von "700000000" für 700.000.000 m zu geben. Da bekam Delphi allerdings bei den Sonnenabständen der äusseren Planeten Schluckauf. Die sprengen irgendwelche Bitgrenzen. Runterskaliert auf 1:1000000 (tkm) klappte es dann auch mit Pluto. Nutzen wir halt den Nachkomma-Bereich für genauere Spezifikationen.

Die Zahlen sind immer noch riesig. Der Z-Achsen-Wert der Erde (Abstand zur Sonne) beträgt immerhin 149.000 tkm. Und Pluto? Fast 6.000.000 tkm. Das sind keine Alltagszahlen, das sind Zahlenmonster.

Gravitation sei Dank

Irgendwo las ich: Alle grossen Objekte im Universum sind kugelförmig. Monde in der Form von Kaffee-Kanne sind nicht drin, das lässt die Schwerkraft nicht zu. Und bedingt durch den Urknall hat das Weltall selbst wohl ebenfalls eine kugelhafte Ausdehnung.

Das macht's uns einfacher, können wir doch so die meisten Objekte im Modell mit dem gleichen OGL-Typ, nämlich "gluSphere", erzeugen.

Begonnen hatte ich mit den üblichen Verdächtigen: Sonne, Merkur, Mond, Erde, Mars, Jupiter, Uranus, Neptun und - natürlich - Pluto, Zwergplanet hin oder her. Es kamen aber weitere Objekte hinzu. Diverse Monde, Asteroiden und Kometen. Ausserdem konnte man bald zwischen mehreren "All-Blasen" wählen, in denen sich alles abspielt.

Um bei dem Wust den Überblick nicht zu verlieren, deklarieren wir uns einen Index:

//objekt-index---------------------
_inx=(
  _all0,_all1,_all2,_all3,_all4,
  _all5,_all6,_all7,_all8,_all9,

  _sonne,
  _merkur,
  _venus,

  _mond,
  _erde,

  _deimos,
  _phobos,
  _mars,

  _ceres,

  _kallisto,
  _ganymed,
  _europa,
  _io,
  _jupiter,

  _titan,
  _rhea,
  _saturn,

  _oberon,
  _titania,
  _uranus,

  _triton,
  _neptun,

  _kuiper,

  _charon,
  _pluto,
  _c
);


Die Objekte selbst werden über die Klasse "TObj" implementiert. Attribute sind die Koordinaten im Raum, der Radius und der Name des Himmelkörpers. Ausserdem gibt's einen Pointer für das erwähnte "gluQuadric"-Objekt. Die Methoden "init" bzw. "destroy" erzeugen bzw. zerstören das Objekt.

//objekt-klasse-----------------------------
tobj=class
  x,y,z:double;  //koordinaten
  r:double;      //radius
  tx:gluint;     //textur
  p:PGLUquadric; //quadric
  name:string;
  procedure init(nm:string;px,py,pz,pr:double);
  destructor destroy;override;
end;

[...]

implementation

//===========================================================
procedure tobj.init(nm:string;px,py,pz,pr:double);
begin
  name:=nm;
  x:=px;y:=py;z:=pz;r:=pr;
  p:=gluNewQuadric;
  gluQuadricOrientation(p,GLU_OUTSIDE);
  gluQuadricNormals(p,GLU_SMOOTH);
  gluQuadricTexture(p,TGLboolean(true));
  gluQuadricDrawStyle(p,GLU_FILL);
  glEnable(GL_COLOR_MATERIAL);
  hauptf.mktextur('tx_'+nm+'.jpg',tx);
end;

destructor tobj.destroy;
begin
  gluDeleteQuadric(p);
  glDeleteTextures(1,@tx);
  inherited;
end;


Über "gluNewQuadric" wird der OGL-Typ "gluQuadric" erzeugt. Es folgen einige "glu"-Funktionen, die "p" bestimmte Eigenschaften zuweisen. Zum Beipiel wird der Textur-Modus aktiviert. Dadurch wird das Objekt später nicht nur als Gittermodell wiedergegeben. Die Funktion "mktextur" läd ein Bild von der Festplatte, wandelt dieses in eine Textur und bindet es an das "tx"-Attribut von "TObj".

//lade textur von platte------------------------------------
procedure thauptf.mktextur(fn:string;var tx:gluint);
var
  tex:PSDL_Surface;
begin
  tex:=IMG_Load(pchar(hauptf.homedir+fn));
  if assigned(tex) then begin
    glGenTextures(1,@tx);
    glBindTexture(GL_TEXTURE_2D,tx);
    glEnable(GL_TEXTURE_2D);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexParameteri(gl_texture_2d,GL_GENERATE_MIPMAP_SGIS,GL_TRUE);
    glTexImage2D(GL_TEXTURE_2D,0,3,tex^.w,tex^.h,0,GL_RGB,GL_UNSIGNED_BYTE,tex^.pixels);
    SDL_FreeSurface(tex);
  end;
end;


Immer schön variabel bleiben

Folgende Variablen werden "semi-global" in der Hauptklasse "thauptf" deklariert:

//koordinaten floats
tcoordf=Record
  x,y,z:glFLoat;
end;

//koordinaten integer
tcoord=record
  x,y,z:word;
end;

//hauptklasse------------------------------------
Thauptf = class(TForm)
 
 [...]
 
public
  { Public-Deklarationen }

  homedir:string;

  //fuer ogl
  dc:HDC;
  rc:HGLRC;

  //raum-positions-halter
  px,py,pz:double;
  rotx,roty,rotz:double;

  //speicher raum-positionen
  sv_x,sv_y,sv_z,sv_rx,sv_ry,sv_rz:double;

  //rotations-takter, raumzeit-takter
  rott:double;
  taktc:integer;

  //quads-objekte
  leitstrahl:PGLUquadric;
  saturn_ring:PGLUquadric;

  //paneten, raumstaub, asteroiden
  obja:array[0..ord(_c)]of tobj;
  stauba:array[0..ord(_staub_c)]of tcoord;
  asteroida:array[0..ord(_asteroid_c)]of tcoord;

  //aktuelle hoechstgeschwindigkeit
  speed:double;

  //hypermove
  hypermove:array[0..12,0.._hypermove_len] of tcoordf;
  hypermove_tx:gluint;

  //ticker
  ticker:integer;
  tickerblink:byte;
  tickerhint:string;

  //autopilot
  ap_z:double;
  ap_steps:integer;
  ap_err:integer;
  ap_dx,ap_dy,ap_dz:double;
  ap_mark_x,ap_mark_y,ap_mark_z:double;
  ap_hmmove_ok:bool;
  ap_spotx:double;

  //schrift
  displayliste:cardinal;

  //drawoptionen
  titelok:bool;
  leitstrahlok:bool;
  visierok:bool;

  //speed-progress
  speedshiftok:bool;
  speedok:bool;
  speedkey:word;

  //sound
  snd_typ:byte;
  soundok:bool;

 //funktionen
 [...]


Das Array "obja" enthält unsere "TObj"-Objekte, die wir gerade implementiert haben.

Der Raum-Staub besteht aus Punkten, die im Raum angeordnet werden. Die Koordinaten werden im Record-Typ "tcoord" gemerkt. Das muss nicht genau sein, daher reichen Word-Werte als Speicher für x, y und z. Auch für die Positionierung der Asteroiden genügt dieser Typ.

Der Record-Typ "tcoordf" verwendet dagegen Fliesskommazahlen. Er kommt beim HyperMove-Tunnel zum Einsatz. Dieser wird mit Hilfe von Sinus- und Kosinus-Werten berechnet, wie wir später sehen werden. Da dürfen die Nachkommastellen nicht unterschlagen werden.

Anfang und Ende einer Delphi-Form

Das "OnCreate"-Ereignis von "TForm" nutzen wir, um unsere OGL-Umgebung einzurichten, die Programm-Parameter aus der Initialisierungsdatei "_inifn" einzulesen, zwei Application-Ereignisse umzubiegen, und die Cockpit-Scrollbars auf Startposition zu setzen.

Bei Programmende ("FormDestroy") werden die Programm-Parameter auf Platte geschrieben, sowie der Speicher von allen "gluQuad"- und Textur-Objekten gesäubert.

//----------------------------------------------------------------------------
procedure Thauptf.FormCreate(Sender: TObject);
begin
  //echter zufall
  randomize;

  ticker:=0;
  tickerblink:=0;
  tickerhint:='';
  taktc:=0;

  homedir:=extractfilepath(application.exename);
  caption:=_cap;

  //ogl initialisieren
  DC:=GetDC(hauptp.Handle);
  if not InitOpenGL then Application.Terminate;
  RC:=CreateRenderingContext(
    DC,
    [opDoubleBuffered],
    24,     //farbbits
    32,     //tiefentest
    0,0,0,
    0
  );
  ActivateRenderingContext(DC,RC);
  glDepthFunc(GL_LESS);
  glenable(GL_DEPTH_TEST);

  //font und objekte generieren
  buildfont;
  initobjects;

  //ini einlesen
  with tinifile.create(homedir+_inifn) do begin
    brennweitesb.Position:=readinteger('param','brennweitesb',brennweitesb.Position);
    brennweitesbChange(Sender);
    allsb.Position:=readinteger('param','allsb',allsb.Position);
    staubcsb.Position:=readinteger('param','staubcsb',staubcsb.Position);
    rotstaubcsb.Position:=readinteger('param','rotstaubcsb',rotstaubcsb.Position);
    zielsb.Position:=readinteger('param','zielsb',zielsb.Position);
    speedsb.Position:=readinteger('param','speedsb',speedsb.Position);
    helpm.visible:=readbool('param','helpm',helpm.visible);
    titelok:=readbool('param','titelok',true);
    leitstrahlok:=readbool('param','leitstrahlok',true);
    visierok:=readbool('param','visierok',true);
    soundok:=readbool('param','soundok',true);
    sv_x:=readfloat('param','sv_x',0);
    sv_y:=readfloat('param','sv_y',0);
    sv_z:=readfloat('param','sv_z',_erde_z);
    sv_rx:=readfloat('param','sv_rx',0);
    sv_ry:=readfloat('param','sv_ry',0);
    sv_rz:=readfloat('param','sv_rz',0);
    free;
  end;

  //hint-nachrichten umbiegen
  application.OnShowHint:=ApplicationShowHint;

  //idle-handler umbiegen
  Application.OnIdle:=IdleHandler;

  //fenstergroesse setzen
  width:=640;
  height:=480;
  activecontrol:=nil;

  //springe im modell an anfangsposition
  pos_home;

  //aktuelle geschwindigkeit setzen
  speedsbChange(nil);

  //elemente ausrichten
  hauptp.Align:=alclient;
  hauptbp.Align:=alclient;
  cockpitp.ParentBackground:=false;
  zielimgp.ParentBackground:=false;
  brennweitesb.tag:=50;
  speetp.Align:=alclient;
  zielsb.Min:=ord(_sonne);
  zielsb.Max:=ord(_pluto);
  zielimg.align:=alclient;
  ziell.Align:=alclient;

  //taktgeber aktivieren
  taktt.tag:=_ap_stop;
  taktt.enabled:=true;

  self.WindowState:=wsmaximized;
end;

procedure Thauptf.FormDestroy(Sender: TObject);
var
  r:integer;
begin
  //ini sichern
  with tinifile.create(homedir+_inifn) do begin
    writeinteger('param','brennweitesb',brennweitesb.Position);
    writeinteger('param','allsb',allsb.Position);
    writeinteger('param','staubcsb',staubcsb.Position);
    writeinteger('param','rotstaubcsb',rotstaubcsb.Position);
    writeinteger('param','zielsb',zielsb.Position);
    writeinteger('param','speedsb',speedsb.Position);
    writebool('param','helpm',helpm.visible);
    writebool('param','titel',titelok);
    writebool('param','leitstrahlok',leitstrahlok);
    writebool('param','visierok',visierok);
    writebool('param','soundok',soundok);
    writefloat('param','sv_x',sv_x);
    writefloat('param','sv_y',sv_y);
    writefloat('param','sv_z',sv_z);
    writefloat('param','sv_rx',sv_rx);
    writefloat('param','sv_ry',sv_ry);
    writefloat('param','sv_rz',sv_rz);
    free;
  end;

  //ogl-umgebung freigeben
  DeactivateRenderingContext;
  DestroyRenderingContext(RC);
  ReleaseDC(hauptp.Handle,DC);
  glDeleteLists(displayliste,256);

  //objekt freigeben
  for r:=ord(_all1) to ord(_c)-1 do obja[r].destroy;
end;


Tiefgehende Probleme

Beim "OGL_HENRYs"-Projekt wusste ich nichts mit dem Wert für die Tiefenbits bei der OGL-Funktion "CreateRenderingContext" anzufangen. Das Modell dort bewegt sich allerdings auch innerhalb so kleiner Dimensionen, dass die Berechnung, welches Objekt von welchem anderen Objekt in Blickrichtung verdeckt wird, keine Probleme darstellt.

"OGL_Planets" stösst in ganz andere Dimensionen vor. Da man im Weltall quasi unendlich weit sehen kann, war ich gezwungen, den "Nähe-Bereich" einzuschränken, also den Teil des Blickfeldes, ab dem Objekte direkt vor einem zu sehen sind.

So wie ich 's wollte, bekam ich's leider nicht hin. Denn eigentlich sollte man unendlich weit sehen, gleichzeitig aber auch Objekte ab einem Meter Grösse (im Modell ergibt das einen Wert von "0,0000001") erkennen können. Das hätte dann sogar für die ISS im Orbit der Erde genügt.

Eine so feine Auflösung war in OGL aber nicht drin. Damit kommt offenbar der Tiefenpuffer nicht zurecht. Der entscheidet, was vor oder hinter einem Objekt aus Sicht des Betrachters liegt. Bei mir kam es jedoch zu Transparenz-Problemen. Objekt tauchten plötzlich auf oder verschwanden unvermittelt wieder.

Es ist schwer, genauere Infos über den Wertebereich des Tiefenpuffers zu erhalten. Scheint ein Qualitäts-Kriterium von Grafik-Karten zu sein. Und 24 Bit sind die Obergrenze? Keine Ahnung. Meine OnBoard-Grafikkarte scheint weniger zu haben, vermutlich nur 16 Bit. Selbst nachdem das Modell so angepasst wurde, dass Objekte erst ab Grösse "1" (also 1.000 km) aus der Nähe sichtbar werden, erscheint die Sonne z.B. bisweilen durchsichtig.

OpenGl Planets - Tiefenpuffer-Problem: Sonne wird transparent

Tiefenpuffer-Problem: Die Sonnen wird am Rand transparent, wenn man zu weit von ihr entfernt ist


Gib's mir schriftlich!

Erstaunlich mühsam war es, Schriftzüge im Modell einzubauen. OGL scheint dafür keine fertige Funktion zu besitzen. Im Web wurde ich aber fündig, fand dort etwas Source, der in die "BuildFont"-Prozedur einfloss:

//3d-fonts fuer ogl-welt ----------------------------
procedure thauptf.BuildFont;
var
  font:HFONT;
  gmf:array[0..255] of GLYPHMETRICSFLOAT;
begin
  displayliste:=glGenLists(256);
  font:=CreateFont(
    12,                            // Hoehe
    0,                             // Breite
    0,                             // Winkel
    0,                             // Orientierungswinkel
    0,                             // Fett?
    0,                             // Kursiv?
    0,                             // Unterstrichen?
    0,                             // Durchgestrichen?
    ANSI_CHARSET,                  // Zeichensatz
    OUT_TT_PRECIS,                 // Ausgabe-Praezision
    CLIP_DEFAULT_PRECIS,           // Clipping-Praezision (?)
    PROOF_QUALITY,                 // Ausgabe-Qualitaet
    FF_DONTCARE or DEFAULT_PITCH,  // Family And Pitch
    'Arial'                        // Zeichentyp
  );

  SelectObject(DC,font);
  wglUseFontOutlines(
    DC,                // ogl-grafik
    0,                 // Buchstaben von
    255,               // Buchstaben-Anzahl
    displayliste,      // Die Displayliste
    0.0,               // Deviation From The True Outlines
    0.2,               // Tiefe der Schrift
    WGL_FONT_LINES,    // Linien-Style
    @gmf               // Puffer
  );
end;

//ausgabe der 3d-schrift bei aktueller position
procedure thauptf.glPrint(s:string);
begin
  if text='' then exit;
  glPushAttrib(GL_LIST_BIT);
    glListBase(displayliste);
    glCallLists(length(s),GL_UNSIGNED_BYTE,pchar(s));
  glPopAttrib();
end;


Verstanden habe ich das nur teilweise. Offenbar wird hier ein bestimmten Bereich in der Grafikkarte reservieren, in den die Buchstaben einmalig hinein gerendert werden. Bei Abruf von Schrift kann dann schnell darauf zugegriffen werden. Der im Vergleich langsame Hauptspeicher wird nicht benötigt.

Meine OnBoard-Grafikkarte hat damit trotzdem zu kämpfen. So wie Schrift am Horizont auftaucht, ruckelt das Modell. Daher wurde die Qualität der Buchstaben auf ein Minimum reduziert. So wird z.B. ein Liniengitter genutzt, statt die Grafik aufzufüllen. Optional kann die Schrift aber auch deaktiviert werden.

OpenGl Planets - 3D-Schrift in OpenGL

3D-Schrift: Kaum zu Verdauen für billige OnBoard-Grafikkarten


Der Raum wird gefüllt

Nachdem in "FormCreate" die OGL-Umgebung gesetzt und die 3D-Schriften generiert wurden, folgt nun die Initialisierung der Raum-Objekte:

procedure thauptf.initObjects;

  function f(v:integer):integer;
  begin
    if v>_staub_dim then v:=v-_staub_dim;
    result:=v;
  end;

var
  r,hr:integer;
  haufena:array[0.._haufen_c] of tcoord;
  i,j:integer;
begin

  //hypermove-------------------------------------------
  for i:=0 to 12 do begin
    for j:=0 to _hypermove_len do begin
      hypermove[i,j].x:=(3-j/12)*cos(2*pi/12*i);
      hypermove[i,j].y:=(3-j/12)*sin(2*pi/12*i);
      hypermove[i,j].z:=-j*3;
    end;
  end;
  mktextur('tx_hypermove.jpg',hypermove_tx);

  //all-objekte-------------------------------------------------
  for r:=ord(_all1) to ord(_all9) do begin
    obja[r]:=tobj.Create;
    obja[r].init('all'+inttostr(r),0,0,0,1);
  end;
  allsbChange(nil);

  //--------------------------------------------------------
  leitstrahl:=gluNewQuadric;
  gluQuadricOrientation(leitstrahl,GLU_OUTSIDE);
  gluQuadricNormals(leitstrahl,GLU_SMOOTH);
  gluQuadricDrawStyle(leitstrahl,GLU_LINE);

  //---------------------------------------------------------
  obja[ord(_sonne)]:=tobj.Create;
  obja[ord(_sonne)].init('Sonne',0,0,_sonne_z,_sonne_r);

  obja[ord(_merkur)]:=tobj.Create;
  obja[ord(_merkur)].init('Merkur',0,0,_merkur_z,_merkur_r);

  obja[ord(_venus)]:=tobj.Create;
  obja[ord(_venus)].init('Venus',0,0,_venus_z,_venus_r);

  //---------------------------------------------------------
  obja[ord(_mond)]:=tobj.Create;
  obja[ord(_mond)].init('Mond',0,0,_mond_z,_mond_r);

  obja[ord(_erde)]:=tobj.Create;
  obja[ord(_erde)].init('Erde',0,0,_erde_z,_erde_r);

  //---------------------------------------------------------
  obja[ord(_deimos)]:=tobj.Create;
  obja[ord(_deimos)].init('Deimos',0,0,_deimos_z,_deimos_r);

  obja[ord(_phobos)]:=tobj.Create;
  obja[ord(_phobos)].init('Phobos',0,0,_phobos_z,_phobos_r);

  obja[ord(_mars)]:=tobj.Create;
  obja[ord(_mars)].init('Mars',0,0,_mars_z,_mars_r);

  //---------------------------------------------------------
  obja[ord(_ceres)]:=tobj.Create;
  obja[ord(_ceres)].init('Ceres',0,0,_ceres_z,_ceres_r);

  //---------------------------------------------------------
  obja[ord(_kallisto)]:=tobj.Create;
  obja[ord(_kallisto)].init('Kallisto',0,0,_kallisto_z,_kallisto_r);

  obja[ord(_ganymed)]:=tobj.Create;
  obja[ord(_ganymed)].init('Ganymed',0,0,_ganymed_z,_ganymed_r);

  obja[ord(_europa)]:=tobj.Create;
  obja[ord(_europa)].init('Europa',0,0,_europa_z,_europa_r);

  obja[ord(_io)]:=tobj.Create;
  obja[ord(_io)].init('Io',0,0,_io_z,_io_r);

  obja[ord(_jupiter)]:=tobj.Create;
  obja[ord(_jupiter)].init('Jupiter',0,0,_jupiter_z,_jupiter_r);

  //---------------------------------------------------------
  obja[ord(_titan)]:=tobj.Create;
  obja[ord(_titan)].init('Titan',0,0,_titan_z,_titan_r);

  obja[ord(_rhea)]:=tobj.Create;
  obja[ord(_rhea)].init('Rhea',0,0,_rhea_z,_rhea_r);

  obja[ord(_saturn)]:=tobj.Create;
  obja[ord(_saturn)].init('Saturn',0,0,_saturn_z,_saturn_r);
  saturn_ring:=gluNewQuadric;
  gluQuadricOrientation(saturn_ring,GLU_OUTSIDE);
  gluQuadricNormals(saturn_ring,GLU_SMOOTH);
  //gluQuadricDrawStyle(saturn_ring,GLU_LINE);
  gluQuadricTexture(saturn_ring,TGLboolean(true));

  //---------------------------------------------------------
  obja[ord(_uranus)]:=tobj.Create;
  obja[ord(_uranus)].init('Uranus',0,0,_uranus_z,_uranus_r);

  obja[ord(_oberon)]:=tobj.Create;
  obja[ord(_oberon)].init('Oberon',0,0,_oberon_z,_oberon_r);

  obja[ord(_titania)]:=tobj.Create;
  obja[ord(_titania)].init('Titania',0,0,_titania_z,_titania_r);

  //---------------------------------------------------------
  obja[ord(_neptun)]:=tobj.Create;
  obja[ord(_neptun)].init('Neptun',0,0,_neptun_z,_neptun_r);

  obja[ord(_triton)]:=tobj.Create;
  obja[ord(_triton)].init('Triton',0,0,_triton_z,_triton_r);

  //---------------------------------------------------------
  obja[ord(_kuiper)]:=tobj.Create;
  obja[ord(_kuiper)].init('Kuiper',0,0,_kuiper_z,_kuiper_r);

  //---------------------------------------------------------
  obja[ord(_pluto)]:=tobj.Create;
  obja[ord(_pluto)].init('Pluto',0,0,_pluto_z,_pluto_r);

  obja[ord(_charon)]:=tobj.Create;
  obja[ord(_charon)].init('Charon',0,0,_charon_z,_charon_r);

  //---------------------------------------------------------

  //haufen zufaellig im raum
  for r:=0 to _haufen_c-1 do begin
    haufena[r].x:=random(_staub_dim-2*_haufen_dim)+_haufen_dim;
    haufena[r].y:=random(_staub_dim-2*_haufen_dim)+_haufen_dim;
    haufena[r].z:=random(_staub_dim-2*_haufen_dim)+_haufen_dim;
  end;

  for r:=0 to _staub_c-1 do begin
    if random(4)=0 then begin
      //haufen-struktur wieder etwas aufloesen
      stauba[r].x:=random(_staub_dim);
      stauba[r].y:=random(_staub_dim);
      stauba[r].z:=random(_staub_dim);
    end
    else begin
      //staub um zufalls-haufen konzentrieren
      hr:=r mod (_haufen_c);
      stauba[r].x:=f(random(_haufen_dim)+haufena[hr].x);
      stauba[r].y:=f(random(_haufen_dim)+haufena[hr].y);
      stauba[r].z:=f(random(_haufen_dim)+haufena[hr].z);
    end;
  end;
  staubcsbChange(nil);
  rotstaubcsbChange(nil);

  //asteroiden
  for r:=0 to _asteroid_c-1 do begin
    asteroida[r].x:=random(_asteroid_dim);
    asteroida[r].y:=random(_asteroid_dim);
    asteroida[r].z:=random(_asteroid_dim);
  end;
end;


Schnell, schneller, HyperMove

Zuerst wird das Array des HyperMove-Tunnels mit Koordinaten-Werten gefüllt. Dazu wird eine Röhre fixer Länge aus 12 Eckpunkten aufgebaut. Dann wird die Textur geladen, die später am Röhrenmodell entlang laufen wird, wodurch es wirkt, als würde man mit hoher Geschwindigkeit hindurchfliegen.

OpenGl Planets - Hypermove: Überlichtschnell durch die Röhre

HyperMove: Überlichtschnell durch die Röhre


Alternative Universen

Danach werden neun verschiedene "All"-Objekte generiert. Ein "All"-Objekt kann man sich als gigantische "All-Blase" vorstellen, die unser Sonnensystem weitläufig umschliesst. Über die "angeklebten" Texturen kann jede "All-Blase" ein individuelles Aussehen erhalten.

OpenGl Planets - Wechselbare All-Blasen

Wechselbare "All-Blasen": Nicht unbedingt realistisch, aber schön anzuschauen


Man beachte, dass der Index "_all0" nicht mit einem "All"-Objekt belegt wurde. Diese spezielle "All-Blase" ist einfach nur tiefschwarz. Und unendlich gross. Naja, nicht wirklich unendlich. Wenn man lange genug in eine Richtung fliegt, wird OGL wohl irgendwann mit einem Overflow reagieren. Habe ich aber nie ausprobiert.

Ausgelotet habe ich dagegen die Grenzen aktivierter "All-Blasen". Selbst mit imaginären "Wurmloch"-Geschwindigkeit dauert es einige Minuten, bis man den Rand erreicht hat. Das folgende Beispiel zeigt eine "All-Blase" von aussen, aus immerhin 12-facher Pluto-Entfernung. Ein Blick auf den Bordcomputer zeigt aber, dass die Sonne mit Lichtgeschwindigkeit in nur zwei Tagen zu erreichen ist.

OpenGl Planets - Outerspace: Verdammt weit draussen

Outerpace: Verdammt weit draussen - aber was sind schon zwei Lichttage im galaktischem Massstab?


Nur zum Vergleich: Unsere Milchstrasse hat einen Durchmesser 100.000 Lichtjahre.

Und Andromeda-M31, die nächste Spiral-Galaxie vor unserer Haustür, ist 2,7 Mio Lichtjahre von uns entfernt!

Äh ... bevor noch jemand sucht: Hinter den "All-Blasen" ist Schluss in "OGL_Planets". Da kommt nichts mehr. Schont also euren Flieger-Finger.

OpenGl Planets - Andromeda-Nebel

"Nachbar" Andromeda-Nebel: Zu weit weg, um in "OGL_Planets" enthalten zu sein.


An der Leine geführt

Als nächstes wird der Leitstrahl initialisiert. Hierbei handelt es sich um ein Strahlenbündel, welches im Inneren der Sonne beginnt, quer durch alle Planeten geht und schliesslich bei Pluto endet.

Die Dimensionen im Sonnensystem sind nicht zu unterschätzen. Fliegt man unbedarft hinein, findet man schnell seinen Heimat- oder Zielhafen nicht mehr. Das gilt besonders jenseits von Saturn und Co., denn ab hier ist die Sonne zu klein, um noch als Fixpunkt zur Orientierung dienen zu können. Hier hilft der Leitstrahl, auf Spur zu bleiben.

Der Umfang des Leistrahl-Zylinders hat Erdgrösse. So erkennen man leicht, wie gross - oder vielmehr wie klein - der Blaue Planet im Vergleich zu manch anderen Himmelsobjekten ist.

OpenGl Planets - Leitstrahl: Von der Sonne bis zum Pluto

Leitstrahl: Im Zentrum der Sonne geht's los ... fix bis zur Erde ...
Saturn wird auch mitgenommen ... und endlich sind Charon & Pluto erreicht


Rundliche Weltenbevölkerung

Nächste Aufgabe von "initObject": Die Erzeugung von Sonne, Planeten und Monden. Dazu werden "TObj"-Instanzen generiert und mit den konstanten Werten für Radius und Sonnenentferunung gefüllt.

Zusätzlich erhalten die Objekte einen eindeutigen Namen. Dieser wird für die 3D-Beschriftung und die Objekt-Texturen benötigt.

Für Saturn wird ein zusätzliches "gluQuadric"-Objekt angelegt. Das dient später seinen Ringen.

Nicht sonderlich aufregend, die Planetenbauerei. Aber wie gesagt, Dank der Schwerkraft sehen alle Himmelskörper ziemlich gleich aus. Rundlich halt. Bunte Kugeln in den unendlichen Weiten des Raums.

OpenGl Planets - Nur runde Himmelskörper da draussen

Himmelskörper: Egal ob Sonne, Erde, Jupiter oder Saturn - rund dominiert


Another one bites the dust

Okay, unser Solarsystem hat seine grossen Himmelskörper bekommen. Es folgt die Generierung von "Staub" in einem Array. Dieser "Staub" wird später, wenn wir durch's All rasen, unser ständiger Bekleiter sein. Ausserdem kommt er beim Kometenschweif zum Einsatz. Und er rotiert um alle grösseren Raumobjekte, gefangen von deren Gravitation.

Warum wir uns freiwillig unser Modell verdrecken? Weil's realistischer ist. Weil's gut ausschaut. Und weil der Staub in den Tiefen des Alls ein guter Orientierungspunkt ist, um (Eigen)Bewegung festzustellen.

Der erste "OGL_Planets"-Staub war gleichmässig verteilt. Das war mittels Zufallsgenerator leicht zu realisieren. Wirkte aber auf die Dauer öde.

So wurde etwas "Struktur" hineingearbeitet. Der Staub sollte mal dichter, mal lichter erscheinen. Da mir keine "Staub-Verteilungs-Formel" bekannt ist, bastelte ich mir etwas zusammen.

Der "Basis-Raum" des Staubs ist ein Quader von "_staub_dim" Seitenlänge (5000) tkm. Er wird unterteilt in "_haufen_c" (100) Unterquader mit Kantenlänge "_haufen_dim" (500) tkm. Die "Unter-Quader" werden zufällig mit Raum-Staub gefüllt und sind selbst zufällig im "Basis-Raum" verteilt. Um leere Raumbereiche zu vermeiden, wird jedes vierte Staubkorn über den gesamten Bereich "verstreut". Insgesamt werden so "_staub_c" (80.000) Staubkörner generiert. Deren Anzahl kann der Benutzer später on-the-fly bis auf Null runter variieren. Da wird jede Putzfrau neidisch.

OpenGl Planets - Viel Staub im Welten-Raum

Raum-Staub: Örtlich "verklumpt" durch Eigengravitation


Asteroidenfelder

Zuletzt wird in der "initObjects"-Prozedur ein Array mit Asteroiden-Koordinaten angelegt. Ähnlich wie für die Staubpartikel. Asteroiden sind jedoch grösser und ihre Anzahl ist mit "_asteroid_c" (400) geringer. Einen "Verklumpungseffekt" gibt es hier nicht. Die Jungs werden einfach per Zufall über ein Gebiet von "_asteroid_dim" (400) tkm verteilt.

Das Asteroidenfeld befindet sich übrigens auf halber Strecke zwischen Mars und Jupiter. Der grösste Brocken "Ceres" ist Teil des Objekt-Indexes und lässt sich per Autopilot direkt ansteuern.

OpenGl Planets - Asteroiden: Ceres und Co.

Asteroiden: Ceres und ein paar seiner zahlreichen Kumpel


Hints als OGL-Killer

Alle Objekte wurden erzeugt, die "initObjects"-Prozedur ist abgearbeitet. Kehren wir zur aufrufenden Prozedur "FormCreate" zurück.

Hier wird als nächstes das "OnShowHint"-Ereignis von TApplication auf "ApplicationShowHint" umgebogen.

Wir nutzen die Hint-Technik von Windows in gewohnter Weise. Durch obige "Zentralisierung" können wir aber auf einheitliche Weise darauf reagieren. Aufpoppende Hint-Fenster haben sich (bei mir) als OGL-Grafik-Killer erweisen. Kaum taucht eines auf, ruckelt das Modell.

Damit haben andere OGL-Programme ebenfalls zu kämpfen. Beispielsweise "Google Earth". Wobei auch hier die Leistungsfähigkeit der Grafikkarte keine unerhebliche Rolle zu spielen scheint.

OpenGl Planets - Classic Hints mit Informationen

Classic Hints: Hilfreiche Text-Informationen, aber echte OGL-Killer


Hier nun die zentralisierte "ApplicationShowHint"-Prozedur:

//hints abfangen, weil die die ogl-grafiken bremsen
//hint-texte werden stattdessen im ticker ausgegeben
procedure Thauptf.ApplicationShowHint(
  var HintStr:String;
  var CanShow:Boolean;
  var HintInfo:THintInfo
);
begin
  tickerhint:=hintstr;
  canshow:=false;
end;


Da passiert nicht viel. Wir retten den Hint-Text in "tickerhint" und sorgen mit "canshow:=false" dafür, dass der Hint nicht als Hint erscheint. Der Text taucht stattdessen als Laufschrift im Ticker-Band des Bordcomputers auf. Das stört OGL nicht - und sieht cooler aus.

OpenGl Planets - Ticker-Hint im Bord-Computer

Ticker-Hint: Der Bord-Computer übernimmt die Ausgabe aller Hints


Tickt's noch richtig?

Die dafür verwendete "doTicker"-Prozedur, die ständig per Timer aufgerufen wird, sieht folgendermassen aus:

//--------------------------------------------------
procedure thauptf.doticker;
const
  _l=35;

  function fill(s:string):string;
  var
    i,c:integer;
  begin
    i:=(_l-length(s)) div 2;
    for c:=0 to i-2 do s:=' '+s;
    while length(s)<_l do s:=s+' ';
    result:='|'+s;
  end;

var
  s,ss:string;
  r:integer;
  i,ii:int64;
  d:double;
begin
  if not cockpitp.visible then exit;

  if tickerhint<>'' then begin
    //hint-ereignis hat tickerhint gefuellt
    s:=' *** '+fill(tickerhint);
  end
  else begin
    r:=zielsb.Position;
    d:=distance(px,py,pz,obja[r].x,obja[r].y,obja[r].z);
    i:=trunc(d/speed);

    if taktt.tag=_ap_stop then begin
      if i=0 then begin
        s:='Direktflug möglich';
      end
      else begin
        s:='';
        ii:=i div (60*60*24*365);
        if ii>0 then begin
          s:=s+inttostr(ii)+' Jahre ';
          i:=i mod (60*60*24*365);
        end;

        ii:=i div (60*60*24);
        if ii>0 then begin
          s:=s+inttostr(ii)+' Tage ';
          i:=i mod (60*60*24);
        end;

        if pos('Jahre',s)=0 then begin
          ii:=i div (60*60);
          if ii>0 then begin
            s:=s+inttostr(ii)+' Std. ';
            i:=i mod (60*60);
          end;

          if pos('Tage',s)=0 then begin
            ii:=i div 60;
            if ii>0 then begin
              s:=s+inttostr(ii)+' Min. ';
              i:=i mod 60;
            end;

            if pos('Std.',s)=0 then begin
              if i>0 then s:=s+inttostr(i)+' Sek.';
            end;
          end;
        end;
      end;
      s:=
        ' *** '+
        fill(getspeedtxt)+
        ' *** '+
        fill(uppercase(obja[zielsb.Position].name)+': '+s);
    end

    else if taktt.tag=_ap_richtung  then s:='Ausrichtung'
    else if taktt.tag=_ap_direkt    then s:='Direktflug'
    else if taktt.tag=_ap_hmstart   then s:='Beschleunigung'
    else if taktt.tag=_ap_hmflug    then s:='Hypermove'
    else if taktt.tag=_ap_hmbremsen then s:='Abbremsung'
    else if taktt.tag=_ap_break     then s:='ABBRUCH';

    if pos('*',s)=0 then s:=' *** '+fill(uppercase(obja[zielsb.Position].name)+': '+s);
  end;

  ss:=s;
  s:=copy(s,ticker+1,_l);
  if length(s)<_l then s:=s+copy(ss,1,_l-length(s));

  if (s<>'')and(s[1]='|'then begin
    inc(tickerblink);
    if tickerblink>15 then begin
      tickerblink:=0;
      inc(ticker);
    end
    else begin
      s:=stringreplace(s,'|',' ',[rfreplaceall]);
      if tickerblink mod 2=0 then s:='';
      tickere.Text:=s;
    end;
  end
  else begin
    s:=stringreplace(s,'|',' ',[rfreplaceall]);
    tickere.Text:=s;
    inc(ticker);
    if ticker>length(ss)then ticker:=0;
  end;
end;


Zunächst wird geprüft, ob das Cockpit sichtbar ist. Falls nicht, sparen wir uns die Arbeit und verlassen die Prozedur wieder.

Als nächstes prüfen wir, ob in "tickerhint" ein Hint-Text zur Ausgabe vorliegt. Normalerweise ist das nämlich nicht der Fall.

Der Hint-Text wird in modfizierter Form an die Variable "s" übergeben. Der Tickercounter "ticker" hält fest, wo im String wir uns gerade befinden, also ab welchem Buchstabe mit der Ausgabe begonnen werden soll. Mit jedem "Taktschlag" wird auf den nächsten Buchstaben gewechselt. Dadurch läuft der komplette Text von rechts nach links durch "tickere".

Ist der (Hint-)Text vollständig zu sehen, was der Computer an einem bestimmten Startzeichen (eine Pipe "|") im String erkennt, bleibt das Band stehen. Der Text blinkt ein paarmal (geregelt durch "tickerblink"). Dann läuft er weiter und verschwindet im linkem Rand.

End of Hint

Ist der Text ganz durchgescrollt, fängt die Geschichte wieder von vorne an. Es sei denn, ein neues Hint-Ereignis wurde ausgelöst. Sind wir mit der Maus über der OGL-Grafikausgabe gelandet, wird der Hint-Text einfach geleert.

Zeit ist relativ

Nun schaltet "DoTicker" um und gibt Bordcomputer-Informationen aus. U.a. wird geprüft, ob Himmelskörper, die im Autopilot ausgewählt wurden, per Direktflug erreichbar sind oder ob dafür ein Sprung durch den Hyperspace nötig ist. Oder es wird berechnet, wie lange ein Flug mit der aktuell gewählten Geschwindigkeitsstufe dauern würde.

OpenGl Planets - Lichtdauer bis zur Sonne

Lichtdauer bis Sonne: Mit Lichtgeschwindigkeit (300.000 km/s) ist's in 8 Min zu schaffen


OpenGl Planets - Laufdauer bis zur Sonne

Laufdauer bis Sonne: Aber zu Fuss (1 km/h) dauert's ja auch nur geringfügig länger ...


Autopilot in Phase

Ein dritte Anzeigen-Variante ist für den Autopiloten reserviert. Wird ein Ziel per Autopilot angeflogen, werden mehrere Schritte abgearbeitet. Die Ausrichtung des Raumschiffs muss vorgenommen werden. Es gibt eine Beschleunigungsphase. Eventuell kommt es zum Sprung durch den Hyperspace. Abgebremst werden muss am Schluss natürlich auch noch. Der Bordcomputer zeigt stets an, in welcher Phase wir uns befinden.

"Carpe diem" - nutze den Tag!

Wieder zurück in "FormCreate" fangen wir das "OnIdle"-Ereignis von "TApplication" ab. Wann immer es ausgelöst wird, soll "IdleHandler" aufgerufen werden:

// wenn cpu zeit hat, wird diese funktion aufgerufen
procedure Thauptf.IdleHandler(Sender: TObject; var Done: Boolean);
begin
  draw_scene;
  done:=false;
end;


Im wesentlichen wird hier nur "draw_scene" aufgerufen. In dieser Prozedur wird das Modell neu gerendert und zur Anzeige gebracht. Und das so oft wie möglich.

Da ist permanente Action angesagt. Aber wir befinden uns ja auch im Weltall, d.h. über den Wolken. Und da ist bekanntlich keine freie Minuten drin ...

Quatsch! Richtiger Sänger, aber Titel vermengt. Brrr! Es ist Sonntag, ich hatte gerade eine Mütze voll Schlaf und bin noch nicht ganz wach. Sorry Reinhard ...

OpenGl Planets - Unschuldiger Reinhard Mey

Reinhard Mey: Hat nichts, aber auch rein gar nichts mit "OGL_Planets" zu tun


Auf die Plätze, fertig, los!

Und wieder zurück in "formCreate". Dort bringt uns als nächstes "pos_home" auf eine fix definierte Startposition im Modell.

//zurueck zum ursprung------------------
procedure thauptf.pos_home;
begin
  px:=0;py:=0;pz:=_erde_z;
  rotx:=0;roty:=0;rotz:=0;
end;

//springe zur letzten speicher-position
procedure thauptf.pos_load;
begin
  dosound(_snd_teleport,false);
  px:=sv_x;py:=sv_y;pz:=sv_z;
  rotx:=sv_rx;roty:=sv_ry;rotz:=sv_rz;
  sleep(1000);
end;

//merke aktuelle raum-position-------------
procedure thauptf.pos_save;
begin
  dosound(_snd_teleport,false);
  sv_x:=px;sv_y:=py;sv_z:=pz;
  sv_rx:=rotx;sv_ry:=roty;sv_rz:=rotz;
  sleep(1000);
end;


Heimathafen ist, wie sollte es anders sein, unser schöner Blauer Planet. d zwar exakt in dessen Mitte. stört, der kann an den "px", "py" und "pz"-Werten schrauben. Der Eintrag "px:=10;" würde uns z.B. 10.000 km über dem Zentrum schweben lassen. Da die Erde einen Radius von 6.400 km hat, befänden wir uns dann ca. 3.600 km über dem Nordpol.

"pos_home" ist mit der Spacebar verknüpft; ein Druck darauf und wir transferieren das Raumschiff in seinen Heimathafen zurück.

Alternativ lassen sich mit "S" und "L" die Prozeduren "pos_save" und "pos_load" aufrufen. Das speichert die aktuelle Position im Raum bzw. läd die zuletzt gespeicherten Koordinaten.

Mh ... eine nette Idee wäre, statt "sv_x", "sc_y", "sv_z" als Einzelwerte zu deklarieren, Arrays zu verwenden. Dann könnte man z.B. die F-Tasten verwenden, um sich mehrere Positionen im Solarsystem zu merken.

Schiebung im Cockpit

Weil wir schon am Ändern von Positionen sind: In "FormCreate" werden nun die "OnChange"-Ereignisse der Cockpit-Schieberegeler aufgerufen. Das bewirkt folgendes:

//----------------------------------------------------------------------
procedure Thauptf.brennweitesbChange(Sender: TObject);
var
  r:integer;
begin
  r:=180-brennweitesb.position;
  brennweitesb.Hint:='Brennweite: '+inttostr(r)+' Grad';
  brennweitesb.showhint:=true;
  brennweitesb.tag:=r;
  activecontrol:=nil;
  formresize(sender);
end;

procedure Thauptf.allsbChange(Sender: TObject);
var
  r:integer;
begin
  r:=9-allsb.position;
  if r=0 then allsb.Hint:='Kein Hintergrund'
         else allsb.Hint:='Hintergrund tx_all'+inttostr(9-allsb.position)+'.jpg';
  allsb.showhint:=true;
  activecontrol:=nil;
end;

procedure Thauptf.staubcsbChange(Sender: TObject);
var
  r:integer;
begin
  r:=100-staubcsb.position;
  r:=(_staub_c*r) div 100;
  staubcsb.Hint:='Raum-Staub: '+inttostr(r);
  staubcsb.showhint:=true;
  staubcsb.Tag:=r;
  activecontrol:=nil;
end;

procedure Thauptf.rotstaubcsbChange(Sender: TObject);
var
  r:integer;
begin
  r:=100-rotstaubcsb.position;
  r:=(_staub_c*r) div 100;
  rotstaubcsb.Hint:='Rotations-Staub: '+inttostr(r);
  rotstaubcsb.showhint:=true;
  rotstaubcsb.Tag:=r;
  activecontrol:=nil;
end;

function thauptf.getspeedtxt:string;
var
  s:string;
begin
  if      speedsb.Position=9 then begin s:='Laufen';  speed:=0.000001;end
  else if speedsb.Position=8 then begin s:='Schall';  speed:=0.000340;end
  else if speedsb.Position=7 then begin s:='Saturn V';speed:=0.011000;end
  else if speedsb.Position=6 then begin s:='Komet';   speed:=0.042000;end
  else if speedsb.Position=5 then begin s:='Plasma';  speed:=2.400000;end
  else if speedsb.Position=4 then begin s:='';        speed:=50;end
  else if speedsb.Position=3 then begin s:='';        speed:=150;end
  else if speedsb.Position=2 then begin s:='Licht';   speed:=300;end
  else if speedsb.Position=1 then begin s:='Warp';    speed:=1000;end
  else                            begin s:='Wurmloch';speed:=100000;end;
  if s<>'' then s:='('+s+')';
  result:='Speed: '+f2s_cut(speed)+' '+_einheit+'/s '+s;
end;

procedure Thauptf.speedsbChange(Sender: TObject);
begin
  speedsb.showhint:=false;
  speedsb.hint:=getspeedtxt;
  speedsb.showhint:=true;
  activecontrol:=nil;
end;

procedure Thauptf.zielsbChange(Sender: TObject);
var
  r:integer;
begin
  r:=zielsb.Position;
  if r<ord(_mond)then ziell.Font.Color:=clblack
                 else ziell.Font.Color:=clwhite;
  ziell.caption:=obja[r].name;
  try
    zielimg.picture.LoadFromFile(homedir+'tx_'+obja[r].name+'.jpg');
  except
  end;
  activecontrol:=nil;
end;


Tunnelblick und Fisheye

Der Schiebregeler für die Brennweite beinflusst unser Sichtfeld. Das ist sozusagen der eingebaute "Ich-habe-mir-Drogen-eingeworfen-Wow!-Ist-das-alles-bunt-hier"-Simulator der Schwamm'schen Sternenflotte. Wohl auch einer der Gründe dafür, dass sie so beliebt ist.

Bei niedrige Brennweiten wird alles gestaucht, d.h., alle Objekte erscheinen näher. Man hat den Teleskopblick. Dann kann man z.B. die Sonne vom Saturn aus noch sehen. Aber Vorsicht! Fliegen ist jetzt gefährlich. Objekte in der Ferne sieht man zwar, die in unmittelbarer Nähe dagegen nicht. Aber was soll's? "OGL_Planets" kennt ja keine Collision-Detection ...

Umgedreht bedeuten grosse Brennweiten, dass sich das Sichtfeld weitet. Das gibt Überblick bis in die Ecken rein. Die volle Dröhnung sozusagen, den totale Input. Nummer Fünf hätte, wenn er denn hier leben würde, seine Freude daran.

Moment mal ... Eben merke ich selbst, dass ich Schwachsinn absondere. Zunächst mal werden Brennweiten in Millimetern angegeben, nicht in Grad. Das trifft nur auf den Sichtwinkel zu. Ausserdem zeigt jeder Blick auf eine Kamera, dass hohe Brennweiten Teleskop und niedrige Brennweiten Weitwinkel bedeuten. Alles ist gerade falsch herum definiert.

Pah! Das ist mir wurscht! In meiner Welt mach' ich die Regeln!

OpenGl Planets - Variable Brennweite

Variable Brennweite: Ohne die Position im Raum zu ändern, sieht man mal weniger,
mal mehr von der Umwelt. Oben rechts ist übrigens Normal-Sicht eingestellt ("50 Schwamm-Grad")


Nimm die Staubkörner auf's Korn

Die Konzentration von normalem Raum-Staub und von Rotations-Staub kann über zwei Schieberegel im Cockpit gesondert variiert werden.

Gearbeitet wird stets mit dem gleichen Raum-Staub-Array, das wir in "initObjects" gefüllt haben. Es wird nur die Obergrenze geändert, die festlegt, wie viele der 80.000 Staubpartikel jeweils angezeigt werden sollen.

Ursprünglich hatte ich das Staub-Array jedesmal neu erzeugt. Das kostete aber Zeit und änderte das "Staubbild" sprunghaft, bedingt durch die neuen Zufallswerte. So brauchen wir zwar etwas mehr Speicherplatz, die Übergänge sind aber viel fliessender. Ganz so, als würde man sich einfach eine schärfere Brille anziehen.

OpenGl Planets - Verschiedene Staub-Modi

Staub-Modi: Europa keimfrei, mit Raum-Staub und schliesslich auch noch mit Rotations-Staub.


Am Dirigentenpult

Und nochmal in "FormCreate" zurück. Wir machen jetzt den "Zeitgeber" scharf, den "Taktschlag" unseres Universums. Der TTimer "taktt" feuert alle 50 Millisekunden sein "onTimer"-Ereignis ab. Sämtliche Synchronisationsprozesse in "OGL_Planets" laufen darüber ab. Weitere Timer sind nicht nötig.

procedure Thauptf.takttTimer(Sender: TObject);

  function getflugstep:double;
  begin
    result:=speed/speedpb.max;
    result:=2*taktt.interval/1000*result;
    result:=speedpb.position*result;
  end;

var
  w,d:double;
  r,schiefe:integer;
  pstep:double;
begin
  inc(taktc);if taktc>360 then taktc:=0;
  rott:=getangle(rott-0.5);

  doticker;

  if taktt.tag=_ap_stop then begin

    if speedok and(speedpb.Position=0) then begin
      dosound(_snd_start,true);
    end;
    if speedok and(speedpb.Position=speedpb.max) then begin
      dosound(_snd_slow,true);
    end;
    if not speedok and(speedpb.Position>0) then begin
      dosound(_snd_bremsen,false);
    end;

    if speedok then begin
      speedpb.Position:=speedpb.Position+1;
    end
    else begin
      speedpb.Position:=speedpb.Position-1;
    end;

    if speedpb.Position=0 then begin
      dosound(_snd_stop,false);
    end;

    if speedshiftok then begin
      pstep:=speedpb.position*_rstep/speedpb.max;
      if speedkey=vk_up then begin
        rotx:=getangle(rotx-pstep);
      end
      else if speedkey=vk_down then begin
        rotx:=getangle(rotx+pstep);
      end
      else if speedkey=vk_left then begin
        rotz:=getangle(rotz-pstep);
      end
      else if speedkey=vk_right then begin
        rotz:=getangle(rotz+pstep);
      end;
    end
    else if speedkey=vk_up then begin
      pstep:=getflugstep;
      px:=px-sin(roty*_piover180)*pstep;
      pz:=pz-cos(roty*_piover180)*pstep;
    end
    else if speedkey=vk_down then begin
      pstep:=getflugstep;
      px:=px+sin(roty*_piover180)*pstep;
      pz:=pz+cos(roty*_piover180)*pstep;
    end
    else if speedkey=vk_left then begin
      pstep:=speedpb.position*_rstep/speedpb.max;
      roty:=getangle(roty+pstep);
    end
    else if speedkey=vk_right then begin
      pstep:=speedpb.position*_rstep/speedpb.max;
      roty:=getangle(roty-pstep);
    end
    else if speedkey=vk_prior then begin
      pstep:=getflugstep;
      py:=py+pstep;
    end
    else if speedkey=vk_next then begin
      pstep:=getflugstep;
      py:=py-pstep;
    end;

    speedok:=false;
    draw_scene;

    exit;
  end;

  //autopilot-flug-modi
  case taktt.Tag of
    _ap_stop     : ;
    _ap_richtung : ap_richtung;
    _ap_direkt   : ap_direkt;
    _ap_hmstart  : ap_hmstart;
    _ap_hmflug   : ap_hmflug;
    _ap_hmbremsen: ap_hmbremsen;
    _ap_break    : ap_break;
    _ap_ende     : ap_ende;
  end;

  if(taktt.Tag=_ap_hmstart)or(taktt.Tag=_ap_hmflug)then begin
    //autopilot und spot aktiv

    //spot genau in mitte?
    schiefe:=round(rotz);
    if(schiefe=0)or(schiefe=360)then begin
      //neuinitialisierung eines fehlerterms
      if random(2)=1 then d:=1 else d:=-1;
      rotz:=getangle(1*d*_rstep/2);
    end;

    if rotz<180 then begin
      //ebene links unten
      w:=rotz;
      d:=-1
    end
    else begin
      //ebene rechts unten
      w:=360-rotz;
      d:=1;
    end;

    //variiere spot-tempo in gegebener richtung
    d:=(random(10)+1)*w*d/200;
    ap_spotx:=ap_spotx+d;

    //fehler zu gross?
    if abs(ap_spotx)>2 then begin
      //autopilot abbrechen
      ap_steps:=_ap_break_steps;
      taktt.tag:=_ap_break;
      for r:=20 to 30 do begin
        windows.Beep(r*2,r div 5);
      end;
    end
    else if abs(ap_spotx)>1 then begin
      //warnung
      windows.Beep(100,100 div 5);
    end;
  end;
end;


Bei jedem Taktschlag ändern sich zwei globale Zähler, "taktc" und "rott". Beide bewegen sich im Interval 0-360 (Grad). "taktc" kommt beim HyperMove und dem Kometen-Staub zum Einsatz. Über "rott" lassen wir in "draw_scene" die Planeten um ihre eigene Achse rotieren.

Anschliessend rufen wir "doTicker" auf. Die Prozedur kennen wir ja schon.

Wie wir uns durch's All bewegen ...

Wir prüfen weiter, ob "taktt.tag=_ap_stop" gilt. Wenn ja, ist der Autopilot nicht aktiv und das Raumschiff kann über Tastatur gesteuert werden. Die Flug- und Richtungänderungstasten modifizieren globale Variablen, die hier erst interpretiert werden.

Wird etwa die "Cursor hoch"-Taste dauerhaft gedrückt, hat die Globale "speedok" den Wert "TRUE". Das wiederum bewirkt, dass sich die Position der TProgressbar "speedpb" bei jedem Taktschlag solange erhöht - und damit die Fluggeschwindigkeit unseres Raumschiffs -, bis sie ihr Maximum erreicht hat. Wird die "Cursor hoch"-Taste losgelassen, ändert sich "speedok" auf "FALSE". Die Folge ist, dass die "speedpb" absteigende Werte annimmt, das Raumschiff wird allmählich langsamer, bis es schliesslich still steht.

... und warum das eigentlich falsch ist

Mh ... natürlicher wäre es für ein Weltraum-Fluggerät ja gewesen, keine Bremsphase einzubauen. Einmal beschleunigt flöge es, bis es durch eine gegenläufige Beschleunigung wieder abgremst würde. Bei der Rotations das gleiche - einmal angestossen, dreht's sich, dreht's sich, dreht's sich.

Tja, zu spät dran gedacht, der Zug ist abgefahren.

Sekunden sollten Sekunden sein

Ein Lob hat die Steuerung allerdings verdient: Die Änderung der Positionsvariablen in Flugrichtung ist systemunabhängig. Der Universums-Takt arbeitet nämlich auf allen Rechnern mit dem gleichen Tempo. So können Flugsekunden mittels "getflugstep" umgerechnet werden, so dass sie "wirklich" eine Sekunde lang sind. Egal, wie schnell die Tastatur reagiert, wie rasch die Szenerie gerendert wird oder wie oft und fest "Cursor hoch" gedrückt wird, die angegebene Maximal-Geschwindigkeit bleibt davon unbeeinflusst.

Spielen mit dem HyperMove-Spot

Der zweite Teil des Taktgebers kommt zum Einsatz, wann immer der Autopilot aktiv ist. Über "taktt.tag" erfahren wir, in welcher Phase er sich gerade befindet.

Befinden wir uns im HyperMove-Modus, dann wird jetzt die Position des "Spots" zufällig variiert. Der "Spot" befindet sich idealerweise in der Mitte eines "grünen Bereichs". Der kleine Fiesling neigt aber dazu, in den "roten Bereich" zu wandern. Gelingt ihm das, hat das üble Folgen: Der Autopilot bricht ab und unserer Raumschiff schiesst unsanft aus dem HyperMove-Tunnel heraus.

Aufgabe des Piloten ist es also, durch geschickte Links-Rechts-Steuerungen während des HyperMoves den "Spot" möglichst in der Mitte des grünen Bereichs zu halten. Dabei werden auch schon kleine Abweichungen bestraft: Sie erhöhen eine Art "Strafkonto", dessen Wert in die Flugvariablen derart einfliesst, dass das Ziel nicht mehr mit 100%iger Genauigkeit getroffen wird; manchmal rauscht man so leicht ein paar hunderttausend Kilometer am anvisiertem Planeten vorbei.

OpenGl Planets - Spot während HyperMove

"Spot" während HyperMove: Links ist er noch im grünen Bereich,
aber rechts sieht's gar nicht gut aus - gleich fallen wir aus dem HyperMove-Tunnel.


 

Anfänger fallen oft aus dem Hyperspace. Fortgeschrittene kommen zwar meistens durch, verfehlen durch ihr Strafkonto ihr Ziel aber dennoch. Gute Piloten dagegen schaffen es sogar bis in den Zielplaneten hinein!

Exakt in der Mitte bin ich selbst ja noch nie gelandet. Vermutlich ist das gar nicht möglich (Hey, ich hab's programmiert. Sollte ich da soetwas nicht wissen?). Es sei denn, der Zufallsgenerator spuckt zufällig eine Serie von nur Null-Werten aus. Das aber wäre kein Können, sondern Zufall.

FormCreate hat ausgedient

Ein letztesmal kehren wir zur "FormCreate"-Prozedur zurück. Dort bleibt noch ein einziger wichtiger Job zu tun, nämlich die Ausgabeform zu maximieren. Das wiederum löst das "FormResize"-Ereignis aus:

procedure Thauptf.FormResize(Sender: TObject);
var
  v:double;
begin
  //hilfefenster zentrieren
  helpm.width:=hauptp.width div 2;
  helpm.height:=hauptp.height-hauptp.height div 3;
  helpm.Left:=(hauptp.width-helpm.Width)div 2;
  helpm.top:=(hauptp.height-helpm.height)div 2-40;

  //cockpit zentrieren
  cockpitp.Left:=(hauptp.width-cockpitp.Width)div 2;
  cockpitp.top:=(hauptp.height-cockpitp.height)-10;


  //ogl-adaptionen
  glViewport(0,0,hauptp.ClientWidth,hauptp.ClientHeight);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity;
  v:=hauptp.ClientWidth/hauptp.ClientHeight;
  gluPerspective(brennweitesb.tag,v,_NearClipping,_FarClipping);
  glMatrixMode(GL_MODELVIEW);
  Draw_Scene;
end;


Hier werden ein paar Komponenten auf dem Bildschirm zurechtgerückt. Und über den OGL-Befehl "gluPerspective" neben der Brennweite auch angegeben, ab welcher Entfernung Objekte im Modell zu sehen sind ("_NearClipping") bzw. ab wann sie nicht mehr zu sehen sind ("_FarClipping").

Weiter oben hatte ich schon erwähnt, dass ich mit diesen Werten Probleme hatte, da leider so etwas wie "von Null bis Unendlich" technisch nicht möglich zu sein scheint.

Aus der Nähe betrachtet

Der Nähewert "_NearClipping" bekommt den Wert "1". Im Modell bedeutet dies, dass Objekte erst dann sichtbar werden, wenn man mindestens 1.000 km von ihnen entfernt ist. Nähert man sich ihn weiter, werden sie transparent, verschwinden also einfach. Wir sind also gezwungen, alle wichtigen Objekte mindestens 1000 km gross zu machen. Bedingt dadurch entsprechen einige Himmelsobjekte nicht ihren wahren Ausmassen. Dazu gehören die Asteroiden genauso wie eine Reihe von Monden.

OpenGl Planets - Grössen-Unstimmigkeit: Charon zu gross

Grössen-Unstimmigkeit: Charon (rechts) musste fast auf die Grösse von Pluto
anwachsen, damit er sich beim Näherkommen nicht verdrückt.
In Wahrheit ist er nur etwa halb so gross.


Weit, weit weg und nichts dahinter

Im Weltall kann man praktisch unendlich weit sehen. Daher hat "_FarClipping" den Wert "-1" erhalten. Negative Werte sind hier eigentlich nicht erlaubt, aber meine OGL-Version schluckt's klaglos. Mh ... ein Wert von z.B. "10*_pluto_z" wäre allerdings exakter gewesen.

Krude Bit-Mathematik

Die Clipping-Werte beeinflussen offenbar den Tiefenpuffer, der ja prüfen soll, welche Objekte vor welchen anderen Objekten in Sichtrichtung liegen. In "OGL_Planets" haben sich damit so manche Probleme ergeben (siehe Abschnitt "Tiefgehende Probleme").

Im Web (Frustum.htm) fand ich folgende Formel zu den Bits des Tiefenpuffers bei gegebenen Clipping-Werten:

Es sei:

_pluto_z      = 5913520;
_NearClipping = 1;
_FarClipping  = 10*_pluto_z;


Dann gilt:

  lost_bits := log2(_FarClipping/_NearClipping);

  'roughly log2(_FarClipping/_NearClipping) bits of depth buffer precision are lost'

Bei mir ergibt das:  lost_bits = log2(59135200) =7,77 ~ 8


Doch was sagt das aus?

Mh ... mal angenommen, der Tiefenpuffer hat 16 Bits. Dann blieben 16 - 8 = 8 Bits "Präzision" übrig. Damit kann man bekanntlich einen Wertebereich von 0-255 abdecken. Heisst das, dass man über den Tiefentest bei zwei Objekten, die mehr als 255 "Einheiten" voneinander entfernt sind, nicht mehr entscheiden kann, wer in der Z-Ebene vor dem anderen liegt?

Bei 24 Bit Tiefenpuffer stünden 16 Bits "Präzision" zur Verfügung. Das ergibt einen Wertebereich von immerhin 0-65535 "Einheiten".

Aber egal, beide Varianten passen bei "OGL_Planets" nicht so recht. Die ersten Tranparenz-Effekte sieht man z.B. bei der Sonne ab einem Abstand von 3.500 "Einheiten". Das liegt weder in der Nähe des 255er- noch des 65535er-Grenzwertes ...

Da soll nun einer schlau drauss werden.

Aktion und Reaktion

Vergessen wir den Ärger mit dem Tiefenpuffer. Freuen wir uns lieber, dass die "FormCreate"-Prozedur abgearbeitet ist. "OGL_Planets" ist endlich bereit, sichtbare Ergebnisse auf dem Bildschirm auszuwerfen. Nötig ist dazu nur ein wenig Idle-Time der CPU oder ein universaler Taktschlag von "taktt" ...

... es sei denn, der Benutzer ist schneller und drückt eine beliebige Taste. Dann wird erst folgende Prozedur abgearbeitet:

//------------------------------------------------------------------------
procedure Thauptf.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  i:integer;
begin
  speedok:=false;

  //autopilot aktiv?
  if taktt.tag<>_ap_stop then begin
    //ja: tastatur aktiv?
    if
      (taktt.tag<>_ap_richtung)and
      (taktt.tag<>_ap_hmbremsen)
    then begin
      if key=vk_right then begin
        rotz:=getangle(rotz-_rstep);
      end
      else if key=vk_left then begin
        rotz:=getangle(rotz+_rstep);
      end;
    end;
    exit;
  end;

  //shift gedrueckt?
  speedshiftok:=(ssctrl in shift);

  //irgendeine steuertaste gedrueckt?
  if
    (key=vk_right)or(key=vk_left)or
    (key=vk_prior)or(key=vk_next)or
    (key=vk_up)or(key=vk_down)
  then begin
    speedok:=true;
    if speedpb.position=0 then speedkey:=key
    else if speedkey<>key then speedok:=false;
    exit;
  end;

  //speedauswahl per 0-9
  if(key>=ord('0'))and(key<=ord('9'))then begin
    i:=key-ord('0');
    if i=0 then i:=9 else dec(i);
    speedsb.position:=speedsb.max-i;
  end

  //speedauswahl mit + und -
  else if key=107 then begin //+
    speedsb.position:=speedsb.Position-1;
    speedsbChange(Sender);
  end
  else if key=109 then begin //-
    speedsb.position:=speedsb.Position+1;
    speedsbChange(Sender);
  end

  //sonstiges
  else if key=vk_space then pos_home
  else if key=ord('L'then pos_load
  else if key=ord('S'then pos_save

  else if key=ord('M'then soundok:=not soundok
  else if key=ord('T'then titelok:=not titelok
  else if key=ord('H'then helpm.Visible:=not helpm.Visible
  else if key=ord('C'then cockpitp.Visible:=not cockpitp.Visible
  else if key=ord('O'then leitstrahlok:=not leitstrahlok
  else if key=ord('V'then visierok:=not visierok

  else if key=vk_escape then close;
end;


Zu Beginn wird geprüft, ob der Autopilot aktiv ist. Wenn ja, wird geprüft, ob das Raumschiff gerade auf sein Ziel ausgerichtet ("_ap_richtung") oder abgebremst ("_ap_hmbremsen") wird. In diesen Fällen werden Tastatureingaben ignoriert. Ansonsten werden "Cursor links"- und "Cursor rechts"-Ereignisse abgefragt, da diese unseren Flug durch den Hyperspace steuern.

Sei der Autopilot nicht aktiv. Dann wird geprüft, ob eine Taste gedrückt wurde, die eine Positionsänderung des Raumschiffs bewirkt. Wenn ja, werden die passenden globalen Variablen modifiziert. Wir wir bereits gesehen haben, beeinflusst das wiederum den Steuerungsmechanismus in "takttTimer". Anschliessend wird die Prozedur verlassen.

Wurde keine Steuertaste gedrückt, bleibt zu prüfen, ob eine der anderen, im Hilfeschirm beschriebenen Tasten betätigt wurde. Die Taste "H" bringt uns zur Erde zurück, "C" schaltet das Cockpit an bzw. aus, "S" speichert die aktuelle Position auf Platte usw.

Rendering

Kaum hat die CPU "Freizeit", wird über das "OnIdle"-Ereignis die Prozedur "draw_scene" aufgerufen (siehe Abschnitt "'Carpe diem' - nutze den Tag!"). Hier endlich nimmt unser Universum Gestalt an:

//------------------------------------------------------------------
procedure thauptf.draw_scene;

  procedure wrrot(ed:tedit;rot:integer);
  var
    s:string;
  begin
    if rot mod 90=0 then ed.Color:=clgreen
                    else ed.color:=clblack;
    s:=inttostr(rot);
    while length(s)<4 do s:=' '+s;
    ed.Text:=s+'°';
  end;

  procedure wrp(ed:tedit;p:double);
  var
    s:string;
  begin
    s:=f2s(p)+' '+_einheit;
    while length(s)<25 do s:=' '+s;
    ed.Text:=s;
  end;

begin
  //bildpuffer komplett löschen
  glClear(GL_COLOR_BUFFER_BIT OR GL_DEPTH_BUFFER_BIT);
  glLoadIdentity;

  //drehung um x/y/z-achsen
  glRotatef(360-rotx,1.0,0,0);
  glRotatef(360-roty,0,1.0,0);
  glRotatef(360-rotz,0,0,1.0);
  if cockpitp.visible then begin
    wrrot(rotxe,trunc(rotx));
    wrrot(rotye,trunc(roty));
    wrrot(rotze,trunc(rotz));
  end;

  //aktuelle position im raum
  glTranslatef(-px,-py,-pz);
  if cockpitp.visible then begin
    wrp(pxe,px);
    wrp(pye,py);
    wrp(pze,pz);
  end;

  draw_all;
  draw_leitstrahl;
  draw_planets;
  draw_staub;
  draw_hypermove;
  draw_fadenkreuz;
  SwapBuffers(DC);
end;


Zuerst wird der Bild- und Tiefenpuffer gelöscht. Der virtuelle OGL-Zeichenstift wird über "glLoadIdentity" in den Ursprungs-Zustand gebracht.

Danach rotiert und verschiebt sich das Modell zur aktuellen Position. Die zugehörigen Werte stehen in den globalen Rotationsvariablen "rotx", "roty" und "rotz", sowie den globalen Positionsvariablen "px", "py" und "pz". Sie werden im Cockpit in formatierter Weise ausgegeben.

Anschliessend werden diverse "draw_"-Prozeduren aufgerufen, die wir uns gleich ansehen werden.

Zuletzt wird die ganze Szenerie mittels "SwapBuffer" auf den Bildschirm angzeigt.

Malen am Rande des Universums

Wie bereits beschrieben, wird unser Solarsystem von einer "All-Blase" umschlossen. Gerendert wird diese über die Prozedur "draw_all":

//all-------------------------------------------------
procedure thauptf.draw_all;
var
  b:byte;
begin
  //hole aktive all-signatur
  b:=9-allsb.position;if b=0 then exit;
  glPushMatrix();
    //tiefentest fuer all-blase aus, da sonst sonne transparent
    //(vermutlich wird 24bit-bereich des tiefenpuffers ueberschritten)
    gldisable(GL_DEPTH_TEST);

    glTranslatef(obja[b].x,obja[b].y,obja[b].z);
    glRotatef(90,1,0,0);
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D,obja[b].tx);
    gluSphere(obja[b].p,3*_pluto_z,20,20);
    glDisable(GL_TEXTURE_2D);

    //tiefentest wieder aktiv
    glenable(GL_DEPTH_TEST);
  glPopMatrix();
end;


Die Nummer der aktiven "All-Blase" ergibt sich aus "9" minus der Position der Scrollbar "allsb". Dadurch wird der Zahlenbereich von 0 bis 9 bzw. der definierte Index-Bereich "_all0" bis "_all9" abgedeckt. Insgesamt können also 10 verschiedene Hintergründe ausgewählt werden.

Die Umrechnung mit der "9" dient dazu, Minimum und Maximum von "allsb" umzukehren. Anders als von Borland vorgesehen, liefert "allsb" so den kleinsten Wert, wenn der Schieberegler ganz unten ist. Das gefiel mir besser.

Wurde "_all0" gewählt, ist nichts weiter zu tun. Es wird keine "All-Blase" gemalt, der Hintergrund bleibt schwarz, wir verlassen die Prozedur wieder.

Ansonsten wird der Tiefentest deaktiviert. Davon versprach ich mir eine Besserung der Transzparenz-Probleme. Da sich im Normalfall nichts hinter der "All-Blase" befindet, kann sie beim Tiefentest unberücksichtigt bleiben. Viel Besserung hat das aber nicht gebracht.

Der Mittelpunkt der "All-Blase" wird auf "0/0/0" gesetzt. Er ist also identisch mit dem der Sonnen. Nur ist die "gluSphere", die wir dann zeichnen, erheblich grösser.

Zuletzt wird der Tiefentest wieder aktiviert und zu "draw_scene" zurückgekehrt.

OpenGl Planets - Verkleinerte All-Blase um die Sonne

"All-Blase": Die hier gezeigte "All-Blase" bekam einen kleineren Radius verpasst,
um zu verdeutlichen, dass sich ihr Ursprung mit dem der Sonne deckt.


Malen des Himmelspfades

Als nächstes wir der Leitstrahl erzeugt:

//distanz zwischen zwei punkten------------------------------
function thauptf.distance(x1,y1,z1,x2,y2,z2:double):double;
begin
  result:=sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2)+(z1-z2)*(z1-z2));
end;

//leitstrahl-------------------------------------------
procedure thauptf.draw_leitstrahl;
var
  abstand:double;
begin
  if not leitstrahlok then exit;
  
  abstand:=distance(
    px,py,pz,
    obja[ord(_sonne)].x,obja[ord(_sonne)].y,obja[ord(_sonne)].z
  );
  if(abstand>2*_pluto_z) then exit;

  glPushMatrix();
    glTranslatef(obja[ord(_sonne)].x,obja[ord(_sonne)].y,obja[ord(_sonne)].z);
    glColor3f(0.0,0.5,0.0);
    gluCylinder(leitstrahl,_erde_r,_erde_r,_pluto_z,12,1);
    glColor3f(1.0,1.0,1.0);
  glPopMatrix();
end;


Zuerst wird geprüft, ob der Leitstrahl aktiv ist. Ist dem nicht so, verlassen wir die Prozedur gleich wieder.

Anschliessend berechnen wir über die "distance"-Funktion unseren aktuellen Abstand zum Startpunkt des Leistrahls (identisch mit dem Ursprung der Sonne). "Distance" basiert übrigens auf dem in's Dreidimensionale übertragenem Satz von Pythagoras (mehr dazu im Abschnitt "Wie finde ich mein Ziel in den Weiten des Alls?"):

OpenGl Planets - Pythagoras als Copilot

Abstand-Berechnung: Unser Copilot, der Grieche Pythagoras,
hilft uns, den Abstand zwei gegebener Punkte im Raum zu ermitteln.


Befinden wir uns mehr als zwei Pluto-Strecken vom Leitstrahl entfernt, wird er nicht mehr gezeichnet. Wir verlassen die Prozedur.

Ansonsten plazieren wir den OGL-Malstift auf den Ursprung, färben ihn mit "glColor" grün ein, und erzeugen über "gluCylinder" einen zylindrisches Bündel aus 12 Strahlen, die einen Umfang von "_erde_r" haben, sowie eine Länge von "_pluto_z".

OpenGl Planets - Leitstrahl in kompletter Länge

Leitrahl: Bei genügend Abstand ist der Leitstrahl in voller Länge zu sehen.
Abgesehen von der Sonne (unten rechts) sind jedoch alle anderen Himmelsobjekte zu klein,
um dann noch erkannt zu werden.


Gemalte Körper am Himmelszelt

Es folgt die Prozedur "draw_planets". Hier werden alle Himmelskörper in die aktuelle "All-Blase" hineingerendert.

//planeten----------------------------------------------
procedure thauptf.draw_planets;
var
  abstand:double;
  r,rd:integer;
  planet:tobj;
begin

  for r:=ord(_pluto) downto ord(_sonne) do begin
    planet:=obja[r];

    //planet sichtbar?
    abstand:=distance(px,py,pz,planet.x,planet.y,planet.z);
    if(r<>ord(_sonne))and(abstand>_merkur_z-10000) then continue;

    //auch sonne weg bei genuegend abstand
    if(abstand>2*_pluto_z) then continue;

    //zufallsterm fuer gas-planeten: flimmern der huelle
    rd:=0;
    if      r=ord(_sonne)   then rd:=random(6)
    else if r=ord(_jupiter) then rd:=random(3)
    else if r=ord(_saturn)  then rd:=random(2)
    else if r=ord(_uranus)  then rd:=random(2)
    else if r=ord(_neptun)  then rd:=random(2);

    if titelok and(abstand<100) then begin
      glPushMatrix();
        //springe zum planeten-ort
        glTranslatef(planet.x,planet.y,planet.z);
        glRotatef(getangle(10*rott),0,1,0);
        glscalef(0.3,0.3,0.3);
        glColor3f(0.5,0.5,0.5);
        glPrint(planet.name);
      glPopMatrix();
      glColor3f(1,1,1);
    end;

    glPushMatrix();
      //springe zum planeten-ort
      glTranslatef(planet.x,planet.y,planet.z);

      //roation, um textur anzupassen
      glRotatef(-rott,0,1,0);glRotatef(90,1,0,0);

      //textur und sphere
      glEnable(GL_TEXTURE_2D);
      glBindTexture(GL_TEXTURE_2D,planet.tx);
      gluSphere(planet.p,planet.r,trunc(20+rd),trunc(20+rd));
      glDisable(GL_TEXTURE_2D);


      //staub, der um paneten rotiert
      draw_rotstaub(planet);
    glPopMatrix();

    glColor3f(1.0,1.0,1.0);

    //'attribute' der planeten zeichen ----------
    if      r=ord(_ceres) then draw_asteroids
    else if r=ord(_saturn)then draw_saturnring
    else if r=ord(_kuiper)then draw_komet;
  end;
end;


Wir durchlaufen in einer Schleife den Index der Himmelsobjekte in umgekehrter Reihenfolge. Zuerst wid Pluto ("_pluto") gemalt, dann sein Mond Charon, dann der Kuiper-Gürtel, dann Neptun usw. Bis wir schliesslich bei der Sonne ("_sonne") angekommen sind.

Bei jedem Schleifendurchgang wird geprüft, ob es sich lohnt, das aktuelle Himmelsobjekt zu rendern. Ist es zu weit vom Piloten entfernt, ist es nicht zu sehen, wir können uns die Arbeit also sparen. Die "distance"-Funktion liefert uns die passenden Werte.

Die Sonne wird bei obiger Prüfung gesondert behandelt. Sie ist aus noch viel grösserer Entfernung zu sehen als die anderen Planeten. Erst ab 2fachen Pluto-Abstand verschwindet sie komplett aus unserem Sichtbereich.

Als nächstes wird ein Radius-Delta "rd" bestimmt. Handelt es sich beim aktuellen Objekt um die Sonne oder einen Gas-Planeten wie etwa Jupiter, dann bekommt "rd" einen Zufallswert zugewiesen. Wozu, das sehen wir gleich.

Befindet sich der Pilot nahe am Objekt und ist der Titel-Modus aktiv, wird eine rotierende 3D-Schrift gemalt, die den Namen des Himmelkörpers anzeigt.

OpenGl Planets - 3D-Titel im Planeten-Inneren

Titel-Modus: So manchen Himmelskörper erkennt der Nicht-Astronom erst,
wenn er in dessen Inneres fliegt - sofern der Titel-Modus aktiv ist.


Der Zeichenstift wird (wieder) auf die Positionen des aktuellen Himmelobjekts gebracht. Dann wird das Umfeld um "rott" Grad rotiert. Wir erinnern uns, "rott" ist eine globale Variable, die in "takttTimer" jede 50stel Sekunde um 0.5 Grad reduziert wird. Ist sie kleiner als Null, wird sie auf 360 Grad hochgesetzt. Dadurch erhalten wir eine permanente Rotation aller Himmelsobjekte um ihre eigene Achse.

 

Damit die Texturen besser passen, wird das Modell nochmal um 90 Grad gekippt. Mh ... effektiver wäre es ja gewesen, die Texturen vorher anzupassen und sich diesen Schritt zu sparen. Hole ich beim nächsten Universum nach ...

OpenGl Planets - Textur-Adaption: An Rotation der Erde angepasst

Textur-Adaption: Ohne nachmalige Rotation um 90 Grad auf der Y-Achse stimmen die
Texturen nicht mehr. Die Erde etwa würde - wie hier gezeigt - um den Äquator
rotieren, statt um die Pole.


Über "gluSphere" wird das Himmelsobjekt generiert. Dank des "rd"-Wertes, den wir eben für die Gas-Planeten ermittelt haben, kann dabei der "Feinheitsgrad" der Kugel leicht variieren, wodurch eine Art Flimmereffekt der Athmosphäre simuliert wird.

Es bietet sich an, nun auch gleich die "Attribute" des aktuellen Himmelköpers abzuarbeiten. Beim Saturn etwa müssen die Ringe nachgetragen werden. Exoten wie Kometen und Asteroiden bedürfen eine Extra-Behandlung. Und der Rotations-Staub, der alle grossen Himmelsobjekte umgibt, muss auch noch generiert werden.

Malen der Dreckschleuder

Fangen wir mit dem Rotations-Staub an. Gemeint ist damit eine Masse von "Punkten", die sich um die Himmelskörper bewegen. Im Gegensatz zum grauen "Normal"-Staub ist der Rotations-Staub rötlich eingefärbt.

//rotations-staub um planeten----------------------------------
procedure thauptf.draw_rotstaub(planet:tobj);
var
  d,abstand,
  rot,mx,my,mz:double;
  i,r:integer;
begin
  //rotationsstaub aktiv?
  if rotstaubcsb.tag=0 then exit;

  //rotationsstaub um planeten sichtbar?
  abstand:=distance(px,py,pz,planet.x,planet.y,planet.z);
  if abstand>3*_staub_dim then exit;

  //anzahl staub nimmt ab, je weiter planet weg
  i:=rotstaubcsb.tag-1;
  d:=1;
  if abstand>_staub_dim/2 then begin
    d:=(abstand-_staub_dim/2)/100;
    d:=(d*abstand)/(_staub_dim)+1;
  end;
  i:=trunc(i/d);

  glpushmatrix();
    //staubrotation um fixen wert, damit
    //planet- und mond-staub nicht synchron laufen
    rot:=trunc(planet.z) mod 360;
    glRotatef(rot,1,1,1);

    glpointsize(1);
    glColor3f(1.0,0.8,0.5);

    //berechnete staubanzahl ausgeben
    for r:=0 to i do begin
      mx:=stauba[r].x-_staub_dim/2;
      my:=stauba[r].y-_staub_dim/2;
      mz:=stauba[r].z-_staub_dim/2;
      glBegin(GL_POINTS);
        glVertex3f(mx,my,mz);
      glEnd();
    end;
  glpopmatrix();
end;


Zunächst wird geprüft, ob die Anzahl darzustellender Staubkörner, die im "Tag"-Attribut der Cockpit-Scrollbars "rotstaubsb" steht, nicht auf Null gesetzt wurde. In diesem Fall geht's gleich wieder raus aus der Prozedur.

Dann wird geprüft, wie weit weg wir uns vom Zentrum des Rotations-Staubes befinden. Sind wir zu weit weg, sparen wir uns die Malaktion.

Jetzt folgt eine Formel, die die maximale Anzahl der zu malenden Staubkörner aus dem aktuellen Abstand zum Staubzentrum berechnet. Zweck der Übung ist, um so weniger Staubkörner erscheinen zu lassen, je weiter wir uns von dem zentralen Himmelskörper wegbewegen.

Idealerweise hätte man ja den Abstand zu jedem Staubkorn extra berechnet. Denn durch die Rotation bewegen sich die Staubkörner ja unter Umständen auf uns zu bzw. weg. Leider war ich jedoch nicht in der Lage, dazu eine ordentliche - und vor allem schnelle - Funktion zu programmieren. So geht's aber auch.

OpenGl Planets - Rotations-Staub-Dichte

Rotations-Staub-Dichte: Je näher wir an Jupiter herankommen,
umso mehr ist von seinem Rotations-Staub (und dem seiner Monde) zu sehen.


Da wir uns noch im "pushmatrix"-Block von "draw_planets" befinden und dort bereits eine Rotation um die Y-Achse vorgenommen haben, müssen wir die Staubkörner eigentlich nicht nocheinmal extra rotieren.

Wir berechnen allerdings trotzdem einen weiteren Rotationswert. Teilweise befinden sich nämlich mehrere Himmelskörper in unmittelbarer Nähe (z.B. bei Planeten mit ihren Monden). Bei exakt gleichem Rotationsgrad würden alle Rotations-Staubkörner synchron laufen. Das aber sieht unschön aus.

OpenGl Planets - Rotations-Staub in Phase

Rotations-Staub in Phase: Synchron rotierende Staubkörner (hier um Jupiter) mit
verschiedenen, ein-achsig verschobenen Zentren bilden unnatürliche Linien


Gut, die Rotation hätten wir. Fehlen noch die Staubkörner an sich. Die zeichnen wir, indem das Staub-Array "stauba" bis zum berechneten Maximum durchlaufen wird. Die Positionenwerte werden umgerechnet, so dass der aktuelle Planet den Mittelpunkt bildet. Ausgegeben werden die Staubkörner letztlich über "glVertexf" als "dreidimensionale" Punkte.

Übrigens hat die Grösse eines Himmelobjekts in "OGL_Planets" keinen Einfluss auf die Dichte oder Aussdehnung des Rotations-Staubs. Dem kleinsten Mond haftet genauso viel Dreck an wie der Sonne. Da habe ich's mir einfach gemacht. Entsprechend verdoppelt sich die Staubmenge mit jedem weiteren Himmelskörper. In "mondreichen" Gebieten wie etwa dem Jupiter kommt somit eine ganze Menge Unrat zusammen.

OpenGl Planets - Rotations-Staub um Jupiter

Rotations-Staub um Jupiter: Wie im wahren Leben rotiert's auch in "OGL_Planets"
heftig um diesen dicken Gesellen mit seinen zahlreichen Monden


Malen von Ringen und Ringen

Haben wir in "draw_planets" den Saturn gezeichnet, folgen nun seine Ringe. Ringstrukturen, die bei anderen Planeten gefunden wurden, wie etwa dem Uranus (oder war's Neptun?), werden in "OHL_Planets" dagegen nicht berücksichtigt.

//--------------------------------------------------------------
procedure thauptf.draw_saturnring;
begin
  glPushMatrix();
    glTranslatef(obja[ord(_saturn)].x,obja[ord(_saturn)].y,obja[ord(_saturn)].z);

    //ring-disk passend drehend
    glRotatef(80,1.0,0,0);
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D,obja[ord(_mond)].tx);

    //innerer ring
    gluDisk(saturn_ring,_saturn_r+20,_saturn_r+50,18,8);

    //aeusserer ring
    gluDisk(saturn_ring,_saturn_r+60,_saturn_ring_r,18,8);

    glDisable(GL_TEXTURE_2D);
    glRotatef(-80,1.0,0,0);
  glPopMatrix();
end;


Zur Generierung der Ringe werden zwei Quadrics-Objekte vom Typ "gluDisks" verwendet, eine für einen inneren und eine für einen äusseren Ring. In Wahrheit hat Saturn mehr Ringe, aber so genau müssen wir 's ja nicht nehmen.

Eine weitere Vereinfachung ist, dass für die Ringe keine eigene Textur spendiert wurde. Stattdessen nahmen wir die des Erde-Mondes. Das spart Speicherplatz und sieht dennoch passabel aus.

Ach ja, die Ringe wurden etwas um die Y-Achse gekippt, so dass sie nicht ganz plan sind mit der Planeten-Rotations-Ebene. Das sieht nicht nur besser aus, sondern entspricht so in etwa auch der Realität.

OpenGl Planets - Prachtbursche Saturn

Saturn: Trotz einiger Hüftringe ein echter Hingucker im Solarsystem


Malen eines Ex-Planeten

Zwischen Mars und Jupiter klafft eine Lücke, in der einst ein weiterer Planet seine Bahnen zog, wie einige Wissenschaftler mutmassen. Ein Indiz dafür ist, das sich dort zahlreiche Gesteinsbrocken finden lassen - die Asteroidenfelder. Womöglich sind diese die letzten Überbleibsel jenes Planeten, der vor Urzeiten bei einer kosmischen Katastrophe zerstört worden sein mag.

Nachdem in "draw_planets" Ceres gezeichnet wurde, folgen nun die restlichen Asteroiden.

//--------------------------------------------------------------
procedure thauptf.draw_asteroids;
var
  x,y,z:double;
  vz,r:integer;
  planet:tobj;
begin
  planet:=obja[ord(_ceres)];
  for r:=0 to _asteroid_c-1 do begin
    glPushMatrix();
      //asteroiden um ceres herum plazieren
      x:=planet.x-_asteroid_dim/2+asteroida[r].x;
      y:=planet.y-_asteroid_dim/2+asteroida[r].y;
      z:=planet.z-_asteroid_dim/2+asteroida[r].z;
      glTranslatef(x,y,z);

      //rotation, damit nicht alle asteroiden
      //identisch aussehen
      vz:=1;if r mod 2=1 then vz:=-1;
      glRotatef(getangle(vz*rott*((r mod 20)+1)+r),1,1,1);

      //textur und sphere, weniger fein als planeten
      //und in verschiedenen groessen
      glEnable(GL_TEXTURE_2D);
      glBindTexture(GL_TEXTURE_2D,planet.tx);
      gluSphere(planet.p,0.1*((r mod 20)+1),5,5);
      glDisable(GL_TEXTURE_2D);
    glPopMatrix();
  end;
end;


Als Mittelpunkt des Asteroidenfeldes definieren wir die Position von Ceres. Die Array-Werte von "asteroida" werden entsprechend umgerechnet.

Mh .. mir fällt gerade auf, dass die Umrechnung sinnvollerweise schon in "initObjects" erledigt worden wäre. Nur einmal, statt bei jeder Malaktion neu. Naja, sei' drum.

Über einen Modulo-Wert wechseln wir die Rotationsrichtung einzelner Asteroiden. Auch der Rotationsgrad bliebt nicht einheitlich. So drehen sich einige Brocken schnell um die Y-Achse, während andere sich langsam mehr um die X-Achse drehen. Das bringt etwas Abwechslung hinein.

Die "gluSphere" bekommt mit dem Wert "5" einen geringeren "Feinheitsgrad" als die Planeten. Dadurch gleichen die Asteroiden weniger exakten Kugeln, werden eckiger, realistischer - und von OGL wohl auch schneller abgearbeitet.

OpenGl Planets - Mitten im Asteoridenfeld

Mitten im Asteoridenfeld: Da vermisst man fast eine asteroidenbrechende Bordkanone


Malen einer Weltraum-Fackel

Jenseit von Neptun vermutet man zahlreiche weitere Himmelsobjekte, die aber einen Tick zu klein sind, um noch von der Erde aus gesehen werden zu können. Pluto scheint dort nur ein grösseres Objekt unter unzähligen weiteren zu sein. Weshalb der arme Kerl ja erst kürzlich zum Zwergplaneten degradiert wurde.

In diesem sogenannten Kuiper-Gürtel vermutet man auch den Ursprungsort vieler Kometen. Diese "schmutzigen Schneebälle" stürzen immer wieder einmal in's Innere des Sonnensystems. Dabei lösen sich Eispartikel und bilden den charakteristschen Schweif. Das sieht zu spektakulär aus, als dass wir es in "OHL_Planets" ignorieren könnten.

Wenigstens einen dieser uralten Gesellen lassen wir durch folgende Prozedur generieren:

//--------------------------------------------------------------
procedure thauptf.draw_komet;
var
  abstand,x,y,z,d:double;
  r,anzahl:integer;
begin
  glPushMatrix();
    //zum kuiper-guertel sprungen
    glTranslatef(obja[ord(_kuiper)].x,obja[ord(_kuiper)].y,obja[ord(_kuiper)].z);

    //eispartikel, blau gefaerbt
    glpointsize(3);
    glColor3f(0.5,0.8,1.0);

    //schweif rotiert um z-achse
    glRotatef(rott,0,0,1);

    //kometenstaub nimmt ab mit abstand
    abstand:=distance(
      px,py,pz,
      obja[ord(_kuiper)].x,obja[ord(_kuiper)].y,obja[ord(_kuiper)].z
    );
    anzahl:=_staub_c-1;
    d:=1;
    if abstand>_staub_dim/200 then begin
      d:=(abstand-_staub_dim/200);
      d:=(d*abstand)/(_staub_dim)+1;
    end;
    anzahl:=trunc(anzahl/d);

    //auffaecherungs-grad
    d:=0.02/_staub_dim;

    //hole anzahl eispartikel aus staub-array
    for r:=0 to anzahl do begin

      //berechne koordinaten so, dass der
      //kometen-koerper den kopf bildet
      x:=stauba[r].x-_staub_dim/2;
      y:=stauba[r].y-_staub_dim/2;
      z:=stauba[r].z-obja[ord(_kuiper)].r;

      //lasse staub in z-achse fliessen
      //wenn ueber der schweifgrenze,
      //dann wieder nach vorne holen
      z:=(trunc(z)+taktc*(10))mod _staub_dim;

      //faechere schweif auf mit wachsendem z
      x:=x*(0.001+(z*d));
      y:=y*(0.001+(z*d));

      //verdichte partikel auf z-achse
      z:=z/5;

      //gib patikel aus
      glBegin(GL_POINTS);
        glVertex3f(x,y,z);
      glEnd();
    end;
    glpointsize(1);
  glPopMatrix();
end;


 

Zunächst positionieren wir den OGL-Malstift auf den Kuiper-Gürtel, so auf Halbe Strecke zwischen Neptun und Pluto. Wir setzen die Stiftbreite auf "3" Pixel und ändern die Farbe auf blau. Damit malen wir gleich den Partikelstrom, der aus dem Kometen "fliesst".

OpenGl Planets - Kometen-Partikel

Komten-Partikel: Ein Strom von Eiskristallen bricht aus dem Kometen heraus


Wir benutzen die globale Rotationsvariable "rott", um den Parikelstrom fortlaufend um die Z-Achse rotieren zu lassen.

Ähnlich wie beim Rotations-Staub berechen wir einen Maximumwert "anzahl" für das Array "stauba", der mit steigendem Abstand zum Kometen abnimmt.

Zusätzlich wird der Auffächerungsgrad "d" berechnet. Der dient dazu, den Schweif des Kometen mit zunehmenden Abstand immer breiter werden zu lassen.

Nun durchlaufen wir das Staub-Array "stauba" von "0" bis "anzahl". Die Koordinaten im Array werden auf die Positionen des Kometen umgerechnet. Null-Werte der Z-Achse entsprechen der Entfernung des Kometen zur Sonne. Dadurch landen grösseren Werte automatisch hinter dem Kometen, weiter weg von der Sonne, was so ja auch den natürlichen Gepflogenheiten von Kometenschweifen entspricht.

Um nun Bewegung in die Sache zu bekomen - ausser der Schweifrotation, die OGL für uns berechnet - benutzen wir den globalen Taktzähler "taktc", um die Z-Werte des Staub-Arrays zu inkrementieren. Bei jedem "Taktschlag" rücken so die Partikel weiter vom Kometen ab. Gleichzeitig nutzen wir unser "d", um die X- und Y-Werte der Parktikel durch Multiplikation mit dem Z-Wert zunehmend zu vergrösseren, wodurch der Schweif nach hinten hin immer breiter wird.

OpenGl Planets - Kometen-Fächer

Kometen-Fächer: Bei grossem "d" (Bild unten) wird der Kometenschweif breiter


Durch die Modulo-Division bei der Berechnung des Z-Wertes sorgen wir dafür, dass uns das Material nicht ausgeht. Jeder Z-Wert des Staub-Arrays wandert dadurch immer wieder von seinem Usprungswert bis auf ein Maximum ("staubdim"), springt dann auf Null um, wächst wiederum bis zum Ursprungswert an und weiter bis zu Maximum - und wiederholt dann das ganze von vone.

Mathematik ist mit Worten schwer zu beschreiben. Zumal ich hinterher oft selbst nicht weiss, was ich da so treibe. Es wird experimentiert, bis es passt. Am Kometenschweif, so wie er jetzt ist - und der wahrlich verbesserungswürdig wäre -, habe ich bestimmt ein, zwei Stunden gesessen.

OpenGl Planets - Der Schwammsche Komet

Der Schwamm'sche Komet: Ein einsames Leuchtfeuer am Rande des Solarsystems


Noch mehr Staub in's All gepinselst

Die Prozedur "draw_planets" haben wir abgearbeitet. Wir kehren zu "draw_scene" zurück. Dort ist jetzt "draw_staub" an der Reihe:

//staub----------------------------------------------
procedure thauptf.draw_staub;

  procedure set_xyz(p_xyz,mm_xyz,md_xyz:double;var m_xyz:double);
  begin
    if m_xyz=0 then exit;

    if p_xyz<0 then begin
      if m_xyz<abs(mm_xyz) then
        m_xyz:=(md_xyz-1)*_staub_dim-m_xyz
      else
        m_xyz:=(md_xyz-0)*_staub_dim-m_xyz;
      m_xyz:=m_xyz+(_staub_dim div 2);

    end
    else begin
      if m_xyz<mm_xyz then
        m_xyz:=(md_xyz+1)*_staub_dim+m_xyz
      else
        m_xyz:=(md_xyz+0)*_staub_dim+m_xyz;
      m_xyz:=m_xyz-(_staub_dim div 2);
    end;
  end;

var
  mmx,mmy,mmz,
  mdx,mdy,mdz,
  mx,my,mz:double;
  r:integer;
begin
  if staubcsb.tag=0 then exit;
  glPushMatrix();
    glColor3f(0.8,0.8,0.4);
    glpointsize(1);

    mmx:=(trunc(px) mod _staub_dim);
    mmy:=(trunc(py) mod _staub_dim);
    mmz:=(trunc(pz) mod _staub_dim);
    mdx:=(trunc(px) div _staub_dim);
    mdy:=(trunc(py) div _staub_dim);
    mdz:=(trunc(pz) div _staub_dim);
    for r:=0 to staubcsb.tag-1 do begin
      mx:=stauba[r].x;set_xyz(px,mmx,mdx,mx);
      my:=stauba[r].y;set_xyz(py,mmy,mdy,my);
      mz:=stauba[r].z;set_xyz(pz,mmz,mdz,mz);
      glBegin(GL_POINTS);
        glVertex3f(mx,my,mz);
      glEnd();
    end;
  glPopMatrix();
end;


Raum-Staub, der überall im Modell auftauchen soll, ist schwerer zu realisieren, als man vermuten sollte.

Der erste Versuch ging daneben: Ein grosses Array, bestehend aus einer Millionen Punkt-Koordinaten, die sich über den kompletten Raum der "All-Blase" verteilten.

Eine Millionen Punkte, das klingt nicht wenig. Doch einmal mehr wurde deutlich, wie gross das Solarsystem ist. Denn praktisch kein Staubkorn war zu sehen. Sie gingen in den Weiten des virtuellen Alls völlig unter. Genausogut hätte ich auch nur 10 Punkte verteilen können.

Mir kam die naheliegende Idee, einen kleineren Raum mit Staubkörnern abzudecken, diesen Raum aber quasi mit mir mitzuschleppen, während ich mich durch die "All-Blase" bewege. Genauer: Der Raum-Staub befindet sich in einem Quader von "_staub_dim" (5000) tkm. Der Pilot hockt in der Mitte. In alle Richtungen hat's also ordentlich Staub. Nun bewegt man sich auf der Z-Achse tiefer in den Raum hinein. Um die Bewegung zu simulieren, müssen die Koordinaten-Punkte des Staub-Arrays folgerichtig um den gleichen Wert nach "vorne", also aus dem Bildschirm heraus, verschoben werden.

 

Es war klar, dass man so früher oder später die Grenze des Arrays erreichen und einem dann die Staubkörner "vor einem" ausgehen würden. Deshalb musste dafür gesorgt werden, dass die "aus dem Bildschirm gefallenen" Staubkörner auf der anderen Seite, sprich, vor einem, wieder auftauchen würden. Typischer Fall für einen von der Piloten-Position abhängigen Modulo-Wert, der irgendwie auf die Koordinaten-Werte der Staubkörner aufzurechnen war.

Mein lieber Schwan, hatte es dieses "irgendwie" in sich! Kostete mich zwei Tage, drei Schachteln Zigaretten, eine Kanne Kaffee und bestimmt eine Millionen geplatzter Neuronen. Und ich lernte auf die harte Tour: Unterschätz nie ein "irgendwie"!

Als mir dann - mit obiger Prozedur - der Raum-Staub erstmals in unendlicher Wiederholung wie geplant um die Ohren flog, in jeder Richtung, egal wo, egal wie lange, war's, als hätte ich höchstselbst die Anti-Gravitions-Formel geknackt.

Eine Erklärung gibt's nicht. Weiss nicht mehr, warum das Ding funktioniert. Es tut's jedenfalls. Und ich werde kein verdammtes Byte mehr daran ändern. Punkt.

OpenGl Planets - Schwierige Formel zur Darstellung von Raum-Staub

Staub-Formeln: Letzte Reste einer qualvollen Annäherung. Nur echt mit Kaffeeflecken ...


Malen des Feuertunnels

Angesicht der riesigen Entfernungen im Solarsystem gibt's auch in "OGL_Planets" den aus der SciFi-Literatur propagierten Königsweg des Hyperspaces, um Distanzen schneller hinter sich bringen zu können.

 

Nicht das es wichtig wäre, aber dennoch: Man kann sich den Hyperspace als einen mit Tachyonen angefüllten Raum denken. Diese Teilchen bewegen sich permanent mit Überlichtgeschwindigkeit und können nur mit unendlichem Energieaufwand auf Lichtgeschwindigkeit abgebremst werden. Transferiert in Tachyonen rast man mit aberwitziger Geschwindigkeit durch den Raum und materialisiert sich am Zielort wieder.

Eine weitere Vorstellung ist die, dass sich im Hyperspace ein Schwarzes Loch befindet. In dessem Zentrum gelten die physikalischen Gesetze der "normalen" Welt nicht mehr. Singularitäts-Zustand. Division by Zero. Hier ist im Prinzip alles möglich, unendliche Materiebildung aus dem Vakuum ebenso wie unendliche Geschwindigkeit.

Eine dritte Variante stellt sich den Hyperspace als von einem Wurmloch gebildet vor. Das vermag die vier-dimensionale Raumzeit derart zu krümmen, dass ein Körper darin nicht über die Oberfläche der in's drei-dimensionale übertragenen Raum-Zeit-Kugel wandern muss, um auf die anderer Seite zu kommen, sondern den kürzeren Weg "mitten durch" nehmen kann.

Wie dem auch sei, unser Hyperspace zumindestens wird mit Bits und Bytes realisiert:

//hypermove-tunnel--------------------------------------
procedure thauptf.draw_hypermove;
const
  _TEXTURE_SPEED=1/20;
var
  i,j:integer;
  angle,j1,j2:glFLoat;
begin
  //hypermove aktiv?
  if taktt.tag<>_ap_hmflug then exit;

  glPushMatrix();
    gldisable(GL_DEPTH_TEST);
    glTranslatef(px,py,pz);
    glRotatef(roty-ap_spotx,0,1.0,0);
    glRotatef(rotz,0,0,1);
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D,hypermove_tx);
    glLineWidth(1);
    angle:=taktc;
    glColor3f(1,1,1);
    for j:=0 to _hypermove_len-1 do begin
      j1:=(j  )/12+angle*_TEXTURE_SPEED;
      j2:=(j+1)/12+angle*_TEXTURE_SPEED;
      glBegin(GL_QUADS);
        For i:=0 to 11 do begin
          glTexCoord2f((i-3)/12,j1);
          glVertex3f(hypermove[i,  j  ].X,hypermove[i,  j  ].Y,hypermove[i,  j  ].Z);
          glTexCoord2f((i-2)/12,j1);
          glVertex3f(hypermove[i+1,j  ].X,hypermove[i+1,j  ].Y,hypermove[i+1,j  ].Z);
          glTexCoord2f((i-2)/12,j2);
          glVertex3f(hypermove[i+1,j+1].X,hypermove[i+1,j+1].Y,hypermove[i+1,j+1].Z);
          glTexCoord2f((i-3)/12,j2);
          glVertex3f(hypermove[i,  j+1].X,hypermove[i,  j+1].Y,hypermove[i,  j+1].Z);
        end;
      glEnd();
    end;
    glDisable(GL_TEXTURE_2D);
    glLineWidth(1);
    glenable(GL_DEPTH_TEST);
  glPopMatrix();
end;


Wir stellen zunächts fest, ob wir uns im HyperMove-Modus befinden. Ist dem nicht so, geht's gleich wieder raus aus der Prozedur.

Wir schieben und drehen das Röhren-Ende so zurecht, dass es etwa in der Mitte des Bildschirms erscheint. Geringfügige Abweichungen nach links und rechts sind möglich, abhängig von der Position des "Spots" (siehe Abschnitt "Spielen mit dem HyperMove-Spot").

Dann wird der Tunnel, dessen "Eckpunkte" zuvor in einem zwei-dimensionalen Array eingetragen wurden, der Länge nach durchlaufen. Der Wert von "angle" wird mit jedem "Taktschlag" des Universum erhöht und fliesst - mit "_TEXTUR_SPEED" multipliziert - in die Koordinaten-Werte "j1" und "j2" der HyperMove-Textur ein.

In einer inneren Schleife werden die 12 Randpunkte des jeweiligen Längenabschnitts über "glVertex2f" gesetzt. Darauf wird jeweils ein Stück Textur "geklebt", welches jedoch über die eben berechneten "j1"- und "j2"-Werte verschoben erscheint. Die Textur wandert so fortlaufend am Rande des Tunnel auf den Betrachter zu, was den Eindruck einer rasenden Fahrt mitten hindurch vermittelt.

OpenGl Planets - Buffy als HyperMove-Textur

HyperMove-Textur: In diesem alternativen HyperMove-Tunnel für Vampirjäger
kommt uns das hübsche Gesicht von "Buffy" näher und näher und näher.


Auf's Visier gemalt

Das All wurde gezeichnet, ebenso alle Planeten nebst Attributen, der Raum-Staub ist verteilt, der HyperMove-Tunnel abgearbeitet. Fehlt noch ein Fadenkreuz, welches einem den Zielpunkt markiert, auf den gerade zugeflogen wird. Diesen Job übernehmen folgende Prozeduren:

//fadenkreuz-------------------------------------------------------
procedure thauptf.draw_fadenkreuz;
var
  rd:integer;
begin
  glColor3f(1.0,1.0,1.0);
  if(taktt.tag=_ap_stop)and not visierok then exit;

  glPushMatrix();
    gldisable(GL_DEPTH_TEST);
    glLoadIdentity;

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

    if(taktt.tag=_ap_hmstart)or(taktt.tag=_ap_hmflug)then begin

      //instabil-rahmen
      glColor3f(1,0.5,0.0);
      glBegin(GL_LINE_LOOP);
        glVertex3f( rd, rd, -rd*3);
        glVertex3f(-rd, rd, -rd*3);
        glVertex3f(-rd,-rd, -rd*3);
        glVertex3f( rd,-rd, -rd*3);
      glEnd();

      //ok-rahmen
      glColor3f(0.0,1.0,0.0);
      glBegin(GL_LINE_LOOP);
        glVertex3f( rd, rd, -rd*15);
        glVertex3f(-rd, rd, -rd*15);
        glVertex3f(-rd,-rd, -rd*15);
        glVertex3f( rd,-rd, -rd*15);
      glEnd();
    end;

    //kreuz
    glColor3f(1.0,1.0,1.0);
    glLineWidth(1);
    glBegin(GL_LINES);
      glVertex3f(-rd,-rd,rd);glVertex3f(rd,rd,rd);
      glVertex3f(rd,-rd,rd);glVertex3f(-rd,rd,rd);
    glEnd();

    //rahmen
    glLineWidth(2);
    if taktt.tag=_ap_hmflug then glColor3f(1.0,0.0,0.0)
                            else glColor3f(1.0,1.0,1.0);
    glBegin(GL_LINE_LOOP);
      glVertex3f( rd, rd, rd);
      glVertex3f(-rd, rd, rd);
      glVertex3f(-rd,-rd, rd);
      glVertex3f( rd,-rd, rd);
    glEnd();

    draw_spot;

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

//spot-----------------------------------------
procedure thauptf.draw_spot;
var
  rd:integer;
begin
  if(taktt.tag<>_ap_hmstart)and(taktt.tag<>_ap_hmflug)then exit;
  glPushMatrix();
    gldisable(GL_DEPTH_TEST);
    glLoadIdentity;
    glRotatef(rotz,0.0,0,1);
    glTranslatef(ap_spotx,0,-20);
    glLineWidth(2);
    rd:=3;
    glScalef(0.1,0.1,0.1);
    glColor3f(1.0,1.0,1.0);
    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;


Wurde das Visier deaktiviert und befinden wir uns nicht im Autopilot-Modus, gibt's nichts weiter zu tun und wir verlassen die Prozedur.

Ansonsten schalten wir den Tiefentest aus, damit unser Visier nicht von sich nähernden Objekten verdeckt wird, und transferieren den OGL-Zeichenstift 20 tkm vor uns.

Während der HyperMove-Phase werden zwei Rechtecke gezeichnet. Einmal ein kleines grünes (der "grüne Bereich"), und einmal ein etwas grösseres, rötlich gefärbt, welches die Grenzen vorgibt, die der "Spot" nicht überschreiten darf (der "kritische Bereich").

Anschliessend wird - egal, ob HyperMove aktiv ist oder nicht - über den Bildschirm ein weisses Kreuz gelegt, welches die Bildschirmmitte schneidet. Eingerahmt wird das ganze durch ein weiteres Rechteck.

OpenGl Planets - Ins Visier genommen

Fadenkreuz: Ein Komet exakt in's Visier genommen


Zuletzt wird "draw_spot" aufgerufen, der Tiefentest wieder aktiviert und die Prozedur verlassen.

In "draw_spot" wird jener kleine "Ball" gemalt, den der Pilot in der Mitte halten muss, solange der HyperMove-Modus aktiv ist.

Die Position des "Spots" wird dabei zum einen vorgegeben durch den Rotationsgrad um die Z-Achse, "rotz", zum anderen durch die in "takttTimer" ständig neu ermittelte Links-Rechts-Abweichung "ap_spotx".

Das kleine Helferlein ist auch dabei

Gechafft! Unser Solarystem wird dargestellt. Wir können uns darin bewegen. Der Bordcomputer zeigt uns ständig, wo wir uns befinden oder wie lange die Reise zu einem anvisiertem Ziel bei aktueller Höchstgeschwindigkeit dauern würde. Die Umgebung lässt sich in ihrem Aussehen variieren. Bildschirm-Elemente können an- und ausgeschaltet werden.

Was uns jetzt noch fehlt, ist ein wenig Unterstützung für den Piloten, damit er die gewünschten Himmelskörper leichter finden kann. Obwohl ja die "OGL_Planets"-Körper im Gegensatz zur Realität nicht in festen Bahnen um die Sonne kreisen, sondern völlig still stehen (mal abgesehen von ihrer Eigenrotation).

Im Prinzip muss man nur dem Leitstrahl folgen, dann findet man früher oder später jedes gesuchte Objekt. Meistens aber eher später. Denn - ich erwähnte es bereits ein-, zweimal - die Dimensionen im Sonnensystem sind gewaltig. Ergo wird man bevorzugt die maximale Höchstgeschwindigkeit wählen. Das wiederum hat aber zur Folge, dass man leicht über's Ziel hinausschiesst. Steuert man etwa von der Sonne aus mit Lichtgeschwindigkeit auf die Erde zu, ist diese nach über 8 Minuten Dauerflug nur für einige wenige Sekunden zu sehen, bevor wir sie durchquert und weit hinter uns gelassen haben.

Ein Autopilot musste also her, jemanden, der die Navigation für uns übernimmt. Und damit die Fliegerei schneller geht, kann man zusätzlich den Weg über den Hyperspace nehmen, sofern die Strecke nur gross genug ist.

Start des Autopiloten

Ein Klick auf den Button "apb" aktiviert den Autopiloten. Er bringt uns zu dem Ziel, welches im Ziel-Monitor des Cockpits ausgewählt wurde. Beliebige Koordinatenpunkte können dagegen nicht angegeben werden.

procedure Thauptf.apbClick(Sender: TObject);
begin
  if apb.Caption='STOPP' then begin
    ap_steps:=_ap_break_steps;
    taktt.tag:=_ap_break;
    activecontrol:=nil;
  end
  else begin
    apb.Caption:='STOPP';
    activecontrol:=nil;
    cockpitp.Color:=clred;
    ap_spotx:=0;
    ap_err:=0;

    ap_z:=_sonne_z;
    case zielsb.position of
      ord(_sonne) :ap_z:=_sonne_z;
      ord(_merkur):ap_z:=_merkur_z;
      ord(_venus) :ap_z:=_venus_z;
      ord(_mond)  :ap_z:=_mond_z;
      ord(_erde)  :ap_z:=_erde_z;

      ord(_deimos):ap_z:=_deimos_z;
      ord(_phobos):ap_z:=_phobos_z;
      ord(_mars)  :ap_z:=_mars_z;

      ord(_ceres):ap_z:=_ceres_z;

      ord(_kallisto):ap_z:=_kallisto_z;
      ord(_ganymed) :ap_z:=_ganymed_z;
      ord(_europa)  :ap_z:=_europa_z;
      ord(_io)      :ap_z:=_io_z;
      ord(_jupiter) :ap_z:=_jupiter_z;

      ord(_titan) :ap_z:=_titan_z;
      ord(_rhea)  :ap_z:=_rhea_z;
      ord(_saturn):ap_z:=_saturn_z;

      ord(_oberon) :ap_z:=_oberon_z;
      ord(_titania):ap_z:=_titania_z;
      ord(_uranus) :ap_z:=_uranus_z;

      ord(_triton):ap_z:=_triton_z;
      ord(_neptun):ap_z:=_neptun_z;

      ord(_kuiper):ap_z:=_kuiper_z;

      ord(_charon):ap_z:=_charon_z;
      ord(_pluto) :ap_z:=_pluto_z;
    end;

    //direktflug oder hypermove?
    if distance(
      px,py,pz,
      obja[zielsb.position].x,obja[zielsb.position].y,obja[zielsb.position].z
    )<=_ap_direkt_distance
    then ap_gpb.max:=2  //direktflug: ap_richtung, ab_direkt
    else ap_gpb.max:=4; //hypermove : ap_richtung, b_hmstart, ap_hmflug, ap_hmbremsen
    ap_gpb.Position:=ap_gpb.max;

    //aktiviere richtungsbestimmung
    ap_steps:=_ap_richtung_steps;
    taktt.tag:=_ap_richtung;
  end;
end;


Zunächst prüfen wir anhand der Button-Caption, ob wir uns bereits im Autopilot-Modus befinden. Ist dies der Fall, beenden wir diesen, indem das "taktt"-Tag auf "_ap_break" gesetzt wird. Dadurch wird ab dem nächsten Taktschlag in "takttTimer" die Abbruchsphase eingeleitet.

Ansonsten ermitteln wir, wie weit das Ziel entfernt ist ("ap_z"). Das lässt sich über die Konstanten der Z-Achsen-Werte der Himmelskörper erfahren, von dem wir später unsere eigene Z-Position abziehen (bzw. aufaddieren). Die Position der Cockpit-Scrollbar "zielsb", des Ziel-Monitors, gibt uns dabei den Index des Planeten vor, den wir erreichen wollen.

Als nächstes wird entschieden, ob das Ziel nah genug für einen Direktflug ist oder doch ein Hyperspace-Flug nötig ist. Die Werte liefert uns die bereits bekannte Distanz-Funktion. Ein Direktflug per Autopilot lässt sich in zwei Phasen unterteilen, ein HyperMove-Flug benötigt dagegen deren vier. Festgehalten wird dies in "ap_gpb.max".

Ganz am Schluss setzen wir noch das "taktt"-Tag auf "_ap_richtung". Dies bewirkt, dass beim nächsten Aufruf von "takttTimer" die Richtungsphasen-Prozedur des Autopiloten abgearbeitet wird.

Wie finde ich mein Ziel in den Weiten des Alls?

Darauf bin ich nun wirklich stolz. Kraft meiner Gedanken habe ich es geschafft, einem Computer die Fähigkeit zu geben, ein Raumschiff so auzurichten, dass es, wo immer es sich befinden mag, mit der Spitze exakt auf ein eventuell Millionen von Kilometern entferntes Ziel weist!

Geholfen hat wiederum Pythagoras. Denn neben der allgemein bekannten Formel zur Berechnung der Länge der Hypothenuse "c" eines rechtwinkligen Dreiecks bei gegebener Länge zweier Seiten "a" und "b" ...

c2 = a2 + b2

OpenGl Planets - Rechtwinkliges Dreieck

Rechtwinkliges Dreieck: Die Länge der Hypothenuse "c"
lässt sich über die Länge von "a" und "b" mit Hilfe des Satzes
von Pythagoras ermitteln. Quelle: Wikipedia


... gibt es auch eine erweiterte Form derselben Formel, den "Kosinussatz", der für beliebige Dreiecke gilt - und in den insbesondere auch die inneren Winkel des Dreiecks einfliessen:

c2 = a2 + b2 - 2ab cos(g)

OpenGl Planets - Kosinussatz

Kosinussatz: Der Kosinussatz ist die
Verallgemeinerung des Satzes von Pythagoras
für nicht rechtwinklige Dreiecke. Quelle: Wikipedia


Prima Sache. Denn kippen wir das Dreieck so zurecht, dass es für unseren Fall gilt, und lösen nach Gamma auf, ergibt sich:

c2 = a2 + b2 - 2ab cos(g)

2ab cos(g) = a2 + b2 - c2

cos(g) = (a2 + b2 - c2) / (2ab)

g = arccos( (a2 + b2 - c2) / (2ab) )

OpenGl Planets - Kosinussatz zur Positionsbestimmung im Raum


Damit hätten wir alles, was wir brauchen. Gamme ist der gesuchte Winkel. Die Länge der Strecke "a" ergibt sich aus dem Abstand des Piloten zur Sonne. Die Länge "b" aus dem Abstand des Piloten zum Ziel. Und die Länge "c" ist die ebenfalls bekannte Strecke Sonne zum Ziel.

Der berechnete Winkel muss um die Eigenrotation des Raumschiffs um die Y-Achse korrigiert werden. So ergibt sich jenes Winkel-Delta, das zur vorliegenden Ausrichtung des Raumschiffs fehlt, um exakt den Zielpunkt anzuvisieren.

Ha! Mein Mathematik-Grundstudium hat sich also doch gelohnt. Obwohl ich zugeben muss, dass mir noch nie zuvor der Arcus Cosinus über den Weg gelaufen ist. Meines Wissen nach benutze ich den hier zum erstenmal in meinem Leben.

Hier nun den der Ausrichtungs-Source in Delphi:

//berechnung wert fuer lineare beschleunigen/abbremsung
//liefert werte zwischen 0 und 1 zurueck
//0 bei x=xmin, 1 bei x=haelfte, 0 bei x=max
function thauptf.aufab_linear(xmin,x,xmax:double):double;
var
  len:double;
begin
  //len: 0 - 0.5 - 0
  len:=xmax-xmin;len:=x/len;if len>0.5 then len:=1-len;
  //len: 0 0,02 0,04 0,06 0,08 ... 1 ... 0,02 0
  result:=2*len;
end;

//autopilot: automatische ausrichtung auf ziel-------------
procedure thauptf.ap_richtung;
  {

       * zx/zy
       |\
       |  \
       |    \
     a |      \ c
       |        \
       |          \
       |       alpha\
       |-------------*x/y
       |      b      |
       |             |
       |             |
   ----*---------------------->
      0/0

     a^2 = b^2 + c^2 - 2*b*c*cos(alpha)
     ==>

                    b^2 + c^2 - a^2
     alpha = arccos( --------------- )
                        2*b*c

  }


  function diffdeg2target(x,y,zx,zy,rot:double):double;
  var
    a,b,c:double;
    alpha,diffw:double;
  begin
    a:=abs(zy-y);
    b:=abs(zx-x);
    c:=sqrt(a*a+b*b);

    alpha:=degtorad(180-90)-arccos((b*b+c*c-a*a)/(2*b*c));
    rot:=degtorad(rot);

    if y>=zy then begin
      //davor
      if x<=zx then diffw:=alpha+rot  //davor rechts
               else diffw:=rot-alpha; //davor links
    end
    else begin
      //dahinter
      if x<=zx then diffw:=degtorad(180)+rot-alpha  //dahinter recht
               else diffw:=degtorad(180)+rot+alpha; //dahinter links
    end;

    diffw:=-radtodeg(diffw);
    diffw:=getangle(diffw);
    result:=diffw/ap_steps;
  end;

  procedure calcwinkel;
  var
    zx,zz:double;
  begin
    zx:=obja[zielsb.position].x;
    zz:=obja[zielsb.position].z;
    ap_dx:=0;
    ap_dy:=0;
    ap_dz:=0;
    ap_dy:=diffdeg2target(px,pz,zx,zz,roty);

    if rotx>=180 then ap_dx:=(360-rotx)/ap_steps
                 else ap_dx:=-rotx/ap_steps;
    if rotz>=180 then ap_dz:=(360-rotz)/ap_steps
                 else ap_dz:=-rotz/ap_steps;
  end;

var
  d:double;
begin
  //start
  if ap_steps=_ap_richtung_steps then begin
    //berechne winkel zum ziel
    calcwinkel;

    //y-aenderung ergibt sich aus flugschritten
    ap_mark_y:=py/ap_steps;

    //turbinen an
    dosound(_snd_start,true);
  end;

  //ende erreicht?
  if
    (ap_steps<=0)or
    (abs(ap_dx+ap_dy+ap_dz)=0)
  then begin
    ap_gpb.Position:=ap_gpb.Position-1;

    //direktflug moeglich
    if ap_gpb.max=2 then begin
      //aktiviere direktflug
      ap_steps:=_ap_direkt_steps;
      taktt.Tag:=_ap_direkt;
    end
    else begin
      //aktiviere hypermove
      ap_steps:=_ap_hmstart_steps;
      taktt.Tag:=_ap_hmstart;
    end;
    ap_spotx:=0;
    exit;
  end;

  //linear beschleunigt/abgremst richtung fixieren
  d:=aufab_linear(0,_ap_richtung_steps-ap_steps,_ap_richtung_steps);d:=d*2;

  //mittelspeed erreicht?
  if d>1 then dosound(_snd_slow,true);

  //abbremsen?
  if(d<1)and(ap_steps<_ap_richtung_steps div 2) then dosound(_snd_bremsen,true);


  rotx:=getangle(rotx+d*ap_dx);
  roty:=getangle(roty+d*ap_dy);
  rotz:=getangle(rotz+d*ap_dz);

  //gleichzeitige anpassung der hoehe
  py:=py-d*ap_mark_y;

  dec(ap_steps);ap_pb.Position:=ap_steps;
end;


Okay, die Prozedur "ap_richtung" wird jede 50tel Sekunde aufgerufen, und zwar durch unseren Universums-Takt "takttTimer". Die globale Variable "ap_steps" wird dabei jedesmal um eins dekrementiert, solange, bis sie Null ist. Das ist nach "_ap_richtung_steps" Schritten der Fall.

Am Anfang wird geprüft, ob dies der erste Aufruf der Funktion ist. Wenn ja, dann lassen wir die interne Funktion "calcwinkel" die Winkel-Änderungen um alle Achsen berechnen, die nötig sind, damit das Raumschiff am Ende auf das Ziel ausgerichtet ist.

Am schwierigsten ist das Winkel-Delta um die Y-Achse zu berechnen. Die Theorie dazu ist oben ja bereits beschrieben worden.

Mh ... ich merke gerade, dass ich mich nicht so ganz an diese Theorie gehalten habe. Wie man in "diffdeg2target" erkennt, wird die Arcus Cosinus-Formel um einen Zusatz-Term korrigiert ... Und was übergebe ich denn da eigentlich für komische Längenwerte?

 

Tja, offenbar habe ich die Längenwerte auf ein rechtwinkliges Dreieck übertragen. Und dann Gamma berechnet? Das haut seltsamerweise hin. Die Winkelsumme im Dreieck hat 180 Grad, im rechtwinkligen Dreieck hat's einmal 90 Grad und der Rest ergibt sich aus ... Ach, was weiss ich? Das Ganze scheint mir unnötig verkompliziert worden zu sein. Aber es funktioniert. Wir erhalten am Schluss das gesuchte Gamma-Delta "ap_dy". 1000 Flüge können sich nicht irren. Belassen wir's dabei.

Wesentlich einfacher funktioniert die Winkel-Delta-Berechnung bei den Rotationswinkel um die X- und Z-Achse, denn die müssen in "_ap_richtung_steps" Schritten einfach nur auf Null gebracht werden. Eine simple Division erledigt den Job.

Wieder zurück in der Hauptprozedur wird als nächstes geprüft, ob das Endekriterium für die "ap_richtung"-Funktion erfüllt ist. Wenn ja, dann wird die nächste Autopilot-Phase eingerichtet, entweder einen Direktflug oder einen HyperMove-Sprung.

Ansonsten nutzen wir "aufab_linear", um den Beschleunigungs-Faktor "d" zu erhalten. Dieser Faktor "d" hat die Eigenschaft, dass er bis zur Hälfte der "_ap_richtung_steps" stetig anwächst und danach wieder stetig abfällt. Die Funktion ist dabei so "austariert", dass die Summe aller "d" genau wieder "1" ergeben - oder so ähnlich.

Jedenfalls wird "d" in der Folge mit den zuvor kalkulierten Winkel-Deltas mulipliziert und auf die aktuellen Winkel "rotx", "roty" und "rotz" aufaddiert. Auf diese Weise richtet sich das Raumschiff mit erst zunehmender, dann absinkender Geschwindigkeit aus. Ganz am Schluss stehen die Winkel "rotx" und "rotz" auf Null, während "roty" exakt auf das anzusteuernde Ziel verweist.

Warum Umwege nehmen, wenn's auch direkt geht?

Bei der verleichsweise geringen Entfernung zum Ziel "_ap_direkt_distance" (10.000 tkm) nimmt der Autopilot nicht den Umweg über den HyperSpace, sondern fliegt das Ziel direkt an. Der "Spot" kommt hier nicht zum tragen, infolgedessen gibt's auch keine Flugabweichungen; die Autopilot steuert das Raumschiff exakt in die Mitte des ausgewählten Himmelskörpers.

Profipiloten wie ich verfahren daher gerne folgendermassen: Per HyperMove wird das Ziel grob angeflogen und der Rest wird per Direktflug zurückgelegt. Ziemlich bequeme Sache.

//autopilot: direktflug---------------------------------
procedure thauptf.ap_direkt;
var
  d:double;
begin
  //start
  if ap_steps=_ap_direkt_steps then begin
    ap_gpb.Position:=ap_gpb.Position-1;
    ap_gpb.Max:=2;
    ap_steps:=_ap_direkt_steps;
    ap_dx:=px/ap_steps;
    ap_dy:=py/ap_steps;
    ap_dz:=(ap_z-pz);ap_dz:=ap_dz/ap_steps;
    ap_spotx:=0;
    ap_pb.max:=ap_steps;ap_pb.position:=ap_steps;
    dosound(_snd_start,true);
  end;

  if ap_steps<=0 then begin
    ap_gpb.Position:=ap_gpb.Position-1;
    taktt.tag:=_ap_ende;
    exit;
  end;

  //bewegung linear beschleunigen/abbremsen
  d:=aufab_linear(0,_ap_direkt_steps-ap_steps,_ap_direkt_steps);d:=d*2;

  //mittelspeed erreicht?
  if d>1 then dosound(_snd_slow,true);

  //abbremsen?
  if(d<1)and(ap_steps<_ap_richtung_steps div 2) then dosound(_snd_bremsen,true);

  px:=px-d*ap_dx;
  py:=py-d*ap_dy;
  pz:=pz+d*ap_dz;

  dec(ap_steps);ap_pb.Position:=ap_steps;
end;


Nach "ap_richtung" wird obige Funktion alle 50tel Sekunden über den Universums-Takt aufgerufen. Wieder wird zuerst geprüft, ob die Funktion in dieser Autopilot-Runde frisch aktiviert wurde. Ähnlich wie zuvor die Winkel-Deltas werden nun die Bewegungs-Deltas "ap_dx", "ap_dy" und "ap_dz" berechnet, so dass die aktuellen Positionswerte in "_ap_direkt_steps" Schritten auf Null ("px" und "py") bzw. auf "ap_z-pz" ("pz"; Strecke bis zum Ziel) gebracht werden.

Sind alle Schritte abgearbeitet, wird in die Phase "_ap_ende" gewechselt.

Ansonsten wird mit "aufab_linear" der aus "ap_richtung" bekannte "d"-Faktor kalkuliert, so dass der Direkflug eine Beschleunigungs- und Abbremsphase hat.

Auftakt zum Höllenritt

Bei grösseren Entfernungen zum Ziel schaltet der Autopilot in den HyperMove-Modus. Damit die Geschwindigkeitssteigerung nicht zu krass ausfällt - das würde kein Raumschiff, geschweige denn menschlicher Pilot aushalten - wird unser Gefährt in der "ap_hmstart"-Phase erstmal auf Touren gebracht.

//autopilot: hypermove beschleunigung--------------------
procedure thauptf.ap_hmstart;
var
  vz:integer;
  r:integer;
begin
  //start
  if ap_steps=_ap_hmstart_steps then begin
    ap_mark_x:=px;
    ap_mark_y:=py;
    dosound(_snd_start,true);
  end;

  //in welcher z-achsen-richtung unterwegs?
  if(ap_z-pz)>0 then vz:=1 else vz:=-1;

  //ende erreicht
  if ap_steps<=0 then begin
    ap_steps:=_ap_hmflug_steps;
    taktt.tag:=_ap_hmflug;
    exit;
  end;

  //sich steigerndes ruetteln
  px:=ap_mark_x;px:=px+random(3)/(ap_steps+1)-random(3)/(ap_steps+1);
  py:=ap_mark_y;py:=py+random(3)/(ap_steps+1)-random(3)/(ap_steps+1);

  //beschleunigung nimmt zu
  r:=_ap_hmstart_steps-ap_steps;
  pz:=pz+vz*(r*r*r)/10000;

  //turbinen auf touren?
  if ap_steps<_ap_hmstart_steps-20 then dosound(_snd_slow,true);

  dec(ap_steps);ap_pb.Position:=ap_steps;
end;


Es wird geprüft, ob die Funktion das erstemal aufgerufen wurde. In diesem Fall merkt sich der Computer die aktuelle "px"- und "py"-Position. Die brauchen wir später, vor Eintritt in den Hyperspace, für die "Dämpfungsfilter".

Dann wird ermittelt, in welche Richtung wir unterwegs sind, ob auf die Sonne zu oder von ihr weg.

Ist das Ende der "ap_hmstart"-Phase erreicht, hat das Raumschiff also genügend Tempo gewonnen, dann kann der Autopilot den Sprung in den Hyperspace veranlassen.

Ansonsten wird "pz" exponentiell erhöht und dadurch das Raumschiff sehr stark beschleunigt. Bedingt durch die Schubkraft unserer Triebwerke kommt es dabei zu anwachsenden Positionsabweichungen in X- und Y-Richtung; da wird unser Pilot durchgeschüttelt, bis die Zähne klappern.

Trip auf der wilde Weltraum-Achterbahn

Der Autopilot schaltet in den HyperMove-Modus um, sowie die "ap_hmstart"-Phase abgeschlossen wurde. Der "Spot" purzelt verstärkt im Visierbereich herum und die rasende Fahrt durch den Tunnel beginnt mit "ap_hmflug":

//autopilot: hypermove bremsstrecke berechnen --------------------
function thauptf.ap_bremsen_step_strecke(step:double):double;
begin
  result:=(step*step*step)/10000;
end;

function thauptf.ap_spotx_err:double;
begin
  result:=ap_spotx*100;
end;

function thauptf.ap_spoty_err:double;
begin
  result:=ap_spotx*sin(rotz)*100;
end;

//autopilot: hypermove flug durch tunnel-------------------------
procedure thauptf.ap_hmflug;

  function randvz:integer;
  begin
    if random(2)=0 then result:=1 else result:=-1;
  end;

var
  r,vz:integer;
begin
  //start
  if ap_steps=_ap_hmflug_steps then begin
    ap_gpb.Position:=ap_gpb.Position-1;

    //ruettler zuruecknehmen
    px:=ap_mark_x;
    py:=ap_mark_y;
    ap_steps:=_ap_hmflug_steps;
    ap_dx:=px/ap_steps;
    ap_dy:=py/ap_steps;

    //in welcher z-achsen-richtung unterwegs?
    if(ap_z-pz)>0 then vz:=1 else vz:=-1;

    ap_dz:=0;
    for r:=0 to _ap_hmbremsen_steps do begin
      ap_dz:=ap_dz+ap_bremsen_step_strecke(r);
    end;
    ap_dz:=ap_dz+(abs(ap_spotx_err)+abs(ap_spoty_err))*50;
    ap_dz:=(ap_z-pz-vz*ap_dz)/ap_steps;

    taktt.tag:=_ap_hmflug;
    ap_pb.max:=ap_steps;ap_pb.position:=ap_steps;

    dosound(_snd_fast,true);
    ap_hmmove_ok:=true;
  end;

  //ende
  if ap_steps<=0 then begin
    ap_steps:=_ap_hmbremsen_steps;
    taktt.tag:=_ap_hmbremsen;
    ap_hmmove_ok:=false;
    exit;
  end;

  //flug-spruenge + zufaellige flug-schwankungen
  px:=ap_mark_x-ap_dx+randvz*ap_spotx_err;ap_mark_x:=px;px:=px+random(5)-random(5);
  py:=ap_mark_y-ap_dy+randvz*ap_spoty_err;ap_mark_y:=py;py:=py+random(5)-random(5);
  pz:=pz+ap_dz;

  dec(ap_steps);ap_pb.Position:=ap_steps;
end;


Beim ersten Aufruf der Funktion durch den Universums-Takt wird die Flugposition des Raumschiffs etwas nachkorrigiert. Dann wird berechnet, wie weit uns der Sprung maximal an's Ziel heranführen kann, um noch genügend Zeit zum Abbremsen zu haben. Das ist wichtig. Ohne eine solche Abbremsphase würde der Kandidat nämlich unweigerlich an der Frontscheibe kleben ...

Ist das Ende des HyperMove-Trips erreicht, wird dem Autopilot folgerichtig mitgeteilt, dass er mit der Abbremsphase beginnen soll.

Während der Fahrt durch den Tunnel wird das Raumschiff weiter durchgerüttelt. Erschwerend kommt hinzu, dass der "Spot" im grünen Bereich gehalten werden muss. Schon kleinste Flugfehler werden bestraft, indem "px" und "py" in unkalkulierbarer Weise modifiziert werden, was unter Umständen - auf die gesamte Flugstrecke gesehen - ziemlich weit in's Nirvana führen kann.

Steig in die Eisen!

Sollten wir den HyperMove-Flug durchgehalten haben, ohne unsaft hinausgeworfen worden zu sein, verlassen wir den Hyperspace auf reguläre Weise, und der Autopilot beginnt mit der Abbremsphase.

//autopilot: hypermove abbremsen-------------------------
procedure thauptf.ap_hmbremsen;
var
  vz:integer;
begin
  //start
  if ap_steps=_ap_hmbremsen_steps then begin
    ap_gpb.Position:=ap_gpb.Position-1;
    px:=ap_mark_x;
    py:=ap_mark_y;
    ap_pb.max:=ap_steps;ap_pb.position:=ap_steps;
    dosound(_snd_fastbremsen,false);
  end;

  //ende
  if ap_steps<=0 then begin
    ap_gpb.Position:=ap_gpb.Position-1;
    taktt.tag:=_ap_ende;
    ap_pb.position:=0;
    exit;
  end;

  //vorzeichen: ziel auf sonne zu oder weg?
  if(ap_z-pz)>0 then vz:=1 else vz:=-1;

  //abbremsen
  ap_dz:=vz*ap_bremsen_step_strecke(ap_steps);
  pz:=pz+ap_dz;

  //turbinen gedrosselt?
  if ap_steps<40 then dosound(_snd_bremsen,false)

  //turbinen nich überdreht?
  else if ap_steps<_ap_hmbremsen_steps-20 then dosound(_snd_slow,true);

  dec(ap_steps);ap_pb.Position:=ap_steps;
end;


Wieder werden zuerst ein paar Korrekturen an unserer Flugposition vorgenommen.

Dann wird geprüft, ob das Raumschiff zum Stillstand gekommen ist. Ist dem so, wird die Endphase des Autopiloten eingeleitet.

Ansonsten wird "pz" um ein exponentiell abnehmendes Delta modifiziert, d.h., das Raumschiff wird zuerst sehr stark, dann immer sanfter abgebremst. Wir wollen ja unser Mittagessen im Magen behalten.

Zusätzlichs Plus: Dank neuster Flugdämpungstechnik - wir haben an nichts gespart - wird diesmal das Raumschiff nicht mehr so stark durchgerüttelt, sondern gleitet vielmehr erschütterungsfrei auf sein Flugziel zu - wenn wir denn einigermassen haben Kurs halten können.

Und wenn wir aus dem Sattel geworfen werden?

Den Autopiloten kann man jederzeit per Knopfdruck abbrechen. Das hat keinerlei negativen Folgen. Es sei denn, man befindet sich im Hyperspace - dann haut's einen aus der Bahn: Wir trudeln wüst durch den Raum, alles wackelt und dreht sich, die Turbinen kreischen auf, bis endlich auch die letzten Reste an Bewegungsenergie verpufft sind. Keine angenehme Erfahrung.

Ds gleiche passiert, wenn der "Spot" den "kritischen Bereich" erreicht.

//autopilot-abbruch-------------------------------
procedure thauptf.ap_break;
var
  vz:integer;
begin
  if ap_steps=_ap_break_steps then begin
    ap_mark_z:=ap_dz/_ap_break_steps;

    if ap_hmmove_ok then begin
      //abbruch waehrend hypermove
      ap_dx:=random(20)-random(20);
      ap_dy:=random(20)-random(20);
      ap_dz:=random(20)-random(20);
      dosound(_snd_break,false);
    end
    else begin
      dosound(_snd_bremsen,false);
    end;
  end;

  //ende
  if ap_steps<=0 then begin
    taktt.tag:=_ap_ende;
    exit;
  end;

  //hypermove aktiv?
  if ap_hmmove_ok then begin
    //unkoordiniert schwingen
    rotx:=getangle(rotx+ap_dx);
    roty:=getangle(roty+ap_dy);
    rotz:=getangle(rotz+ap_dz);
  end;

  //abbremsen
  if(ap_z-pz)>0 then vz:=1 else vz:=-1;
  pz:=pz-vz*ap_mark_z;

  dec(ap_steps);ap_pb.Position:=ap_steps;
end;


Ende gut, alles gut?

Egal, ob wir den Flug mit Bravour gemeistert haben oder ob es uns aus dem Hyperspace katapultiert hat, der Autopilot bewahrt stets kühlen Kopf und leitet am Schluss des Fluges die "ap_ende"-Phase ein.

//autopilot ist fertig-----------------
procedure thauptf.ap_ende;
begin
  ap_gpb.Position:=0;
  taktt.tag:=_ap_stop;
  cockpitp.Color:=clblack;
  apb.caption:='AutoPilot';
  ap_hmmove_ok:=false;
  dosound(_snd_stop,false);
end;


Zu guter letzt gibt's noch was auf die Ohren

In "OGL_Planets" sind diverse Sounds eingebaut, die bei verschiedenen Situationen abgespielt werden. Leider habe ich keine Ahnung, ob OGL selbst irgendwelche Sound-Techniken anbietet. Und mit der Windows-API zu DirectSound habe ich mich bisher auch nie auseinandergesetzt.

Wir benutzten die ziemlich simpel gestrickte Funktion "sndPlaySound", gekapselt über "dosound". Die Synchronisation war hier das grösste Problem. Genauer: Es musste dafür gesorgt werden, dass das Geschehen auf dem Bildschirm sich einigermassen mit der Geräuchkulisse deckt.

So ganz ist mir das nicht gelungen. Manchmal schleicht sich ein "Loop" eines Soundes ein, wo eigentlich keiner (mehr) hingehört, etwa beim Abbremsen des Raumschiffs. Dennoch, dafür dass die ganze Soundmaschinerie mit nur einer einzigen Prozedur abgedeckt wurde, klingt's recht brauchbar.

//--------------------------------------------------------------
procedure thauptf.dosound(typ:byte;loopok:bool);
var
  fn:string;
  uflags:cardinal;
begin
  if not soundok then exit;

  //spielt bereits gleicher sound?
  if typ=snd_typ then exit;

  //neuen sound auswaehlen
  snd_typ:=typ;
  case typ of
    _snd_stop       :fn:='';
    _snd_start      :fn:='snd_start.wav';
    _snd_slow       :fn:='snd_slow.wav';
    _snd_fast       :fn:='snd_fast.wav';
    _snd_bremsen    :fn:='snd_bremsen.wav';
    _snd_fastbremsen:fn:='snd_fastbremsen.wav';
    _snd_break      :fn:='snd_break.wav';
    _snd_teleport   :fn:='snd_teleport.wav';
  end;

  //ewig widerholen?
  uflags:=SND_ASYNC;if loopok then uflags:=uflags or SND_LOOP;
  if fn='' then sndPlaySound(nil,0)
           else sndPlaySound(pchar(homedir+fn),uflags);
end;


Ein Fazit

Im Laufe meines Lebenes habe ich hunderte, wenn nicht tausende Programme geschrieben. Aber selten hat es mir soviel Spass gemacht wie bei "OGL_Planets". Die Doku hier ist zwar wesentlich umfangreicher ausgefallen, als geplant war. Doch wenn man sich den Source mal rein quantitativ anschaut, ist's eigentlich verblüffend wenig geworden.

Das Verhältnis "Ergebnis" zu "Anzahl Codezeilen" stimmt jedenfalls. Ehrlich, ich kann mich nicht erinnern, jemals ein besseres "EACV" erreicht zu haben. Rein subjektiv gesprochen jetzt, mein Chef wäre da sicher anderer Meinung ...

Das Programm hat zweifellos seine Macken. Zum Beispiel bewegt man sich beim Direktflug im Autopilot-Modus bisweilen rückwärts statt vorwärts. Woran das liegt, habe ich während der Doku herausgefunden. Geändert habe ich's aber nicht. Überhaupt bin ich über eine ganze Reihe von Unschönheiten gestolpert, die mir in der aktiven Programmierphase nicht aufgefallen waren. Auch die Soundeinbindung ist reichlich holprig geraten - vielleicht etwas zu minimalistisch. Thematisch ist zudem anzukreiden, dass bei den Himmelskörpern die Grössen oft nicht stimmen. Und sie lungern nur an einer Stelle herum, statt, wie sich das gehören würde, um die Sonne zu kreisen. Vom Kometenschweif gar nicht zu sprechen; der sieht aus so mancher Blickrichtungen echt ätzend aus.

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

Sonne, Mond und Sterne - alles verschnürt in einem Packet

"OGL_Planets" wurde mit Delphi 7 programmiert. Der komplette Source, die Texturen, die Sounds, die EXE und die nötigen OGL-DLLs sind alle in diesem ZIP-Archiv verpackt:

OGL-Planets.zip (ca. 1,1 MB)


Das Original-OGL-Packet 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)


Ach ja, "OGL_Planets" wäre nicht ganz komplett gewesen, wenn dort nicht auch noch einer der (ge)wichtigsten Protagonisten des Science Fictions untergebracht worden wäre: The one and only - Miss Piggy! Die hat jetzt nämlich den TV- mit dem PC-Screen getauscht und treibt sich irgendwo in unserem Solarsystem herum. Den Source dazu habe ich nicht kommentiert. Solche Schweinereien spielen sich besser im Verborgenem ab. Es stellt sich aber die Frage: Wer hat die kleine Wutz schon wo gefunden?

Have fun!

OpenGl Planets - Miss Piggy als verstecktes Oster-Ei in OpenGL Planets

Miss Piggy: Ein Easter-Pig in "OGL_Planets". Sucht sie hier etwa nach Kermit?



| Home | News | Software | HTML | DHTML | Javascript | CGI | VRML | Linux | Dirty-Progs | VidSplitt | OpenGL Henrys | PicOfPics | OpenGL Planets | OpenGL ISS | MediaPanelyzer | Pixel-Evolution | CPU-Eater | FLV-CCC | Sprite-Painter | PHP | Hilfsfunktionen | Volltext-Suche | Src2Textarea | Bilder | Texte | Alles fliesst | Comics | Musik | Leben | Links | Sitemap | Admin |

© by DanPHPEd - Letzte Änderung: 05. Mai 2009