Wissenswertes bei
|
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
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!
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.
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.
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.
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.
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!
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 ...
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.
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.
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.
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.
HyperMove: Überlichtschnell durch die Röhre
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.
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.
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.
"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.
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.
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.
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.
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.
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.
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.
Lichtdauer bis Sonne: Mit Lichtgeschwindigkeit (300.000 km/s) ist's in 8 Min zu schaffen
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 ...
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!
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.
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.
"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.
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.
"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?"):
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".
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.
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 ...
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.
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.
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.
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.
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.
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".
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.
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.
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.
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.
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.
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
|
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)
|
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) )
|
|
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?
"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!
Miss Piggy: Ein Easter-Pig in "OGL_Planets". Sucht sie hier etwa nach Kermit?
|