OGL_HENRYs - Eine kleine Welt im Eigenbau
OGL_HENRYs-Tutorial von Daniel Schwamm (02.01.2008)
Inhalt
Verflixt, es ist exakt 03:15 Uhr, als ich diesen Bericht beginne. Kann nicht
schlafen. Blödes Hirn, will nicht aufhören zu arbeiten. Bin ich halt an den PC
gekrabbelt und tippe etwas runter ...
1997 hatte ich meine erste Berührung mit 3D-Welten, die man selbst basteln konnte.
Mit VRML, einer Script-Sprache, die man Netscape beibringen konnte. Damit liessen sich
Ebenen im Raum schaffen, über die man hinwegfliegen konnte. Mit Texturen versehen
sah das schon recht beeindruckend aus. Allerdings ging mein damaliger PC ziemlich
schnell in die Knie, so wie die Welten etwas komplexer wurden.
Seitdem hat sich einiges getan: Die Rechner wurden schneller, die Grafikkarten
leistungsfähiger, die Software ausgereifter. Nur, so scheint es, ist VRML tot.
Im Laufe der Zeit habe ich einige "3D-Engines" in Delphi programmiert, für Spiele
und Grafikdemonstrationen. War eine ziemlich mühselige Geschichte, mathematisch auch
nie so ganz ausgereift. Mein bestes Ergebnis war eine Art 3D-Klötzchen-Welt (aus dem
Spiel "WertherSpace"), in der man sich immerhin "rechtwinklig" bewegen konnte, d.h.,
nach oben, unten, links und rechts, aber eine Drehung um 5 Grad oder so war nicht
möglich.
3D-Spiel WertherSpace von Daniel Schwamm: Ein begehbare Klötzchenwelt, realisiert ohne OpenGL oder DirectX, sondern mittels eigener 3D-Engine, die in Delphi 7 programmiert wurde.
Dann stolperte ich im Web bei
http://www.delphigl.com über ein Bericht, bei dem es um OpenGL für Delphi ging.
DelphiGL: Ein Installer für eine OpenGL-Umgebung unter Delphi.
Mein Interesse an OpenGL erwachte aufs Neue; ich zog mir das Material und schaute mir die
mitgelieferten DelphiGL-Demos an. Diese beschränkten sich aber meist auf die Darstellung
einfacher Objekte wie Quader, die im Raum schwebten und sich um alle Achsen drehen liessen.
Nicht sehr aufregend, das konnte VRML auch schon.
Mehr Source fand ich eigentlich nicht zu OGL und Delphi. Ich fand aber fertige Programme,
die ziemlich ausgefeilte Welten zeigten. Und die waren angeblich auch mit OGL programmiert
worden (vermutlich aber eher in C++ als Delphi). Es ging also. Nur wie?
Ich beschloss, es herauszubekommen. Und so begann das kleine Projekt "OpenGL HENRYs"
oder kurz "OGL_HENRY", eine Art virtuelles Abbild meiner Arbeitsstätte, dem Auktionshaus
HENRY's (http://www.henrys.de).
Über die URL
http://www.delphigl.com kommt man an die Bibliotheken mit den OpenGL-DLLs
und Units, die für das Projekt benötigt werden. Die Units müssen ins Projekt eingebunden
werden, damit der Source kompiliert werden kann, und die DLLs müssen der EXE zur Verfügung
stehen, sonst sieht man nach dem Start ausser Fehlermeldungen nichts.
Unser Projekt beginnt - wie eigentlich jedes Delphi-Projekt - mit einer leeren Form.
Angesichts der zentralen Rolle, die ihr zu kommt, habe ich sie "hauptf" genannt.
Diese wird schnell mit ein paar Komponenten gefüllt, die alle als nicht sichtbar
definiert sind.
Delphi-Form 'hauptf': Hauptform mit FileListBoxen, die auf die Textur-Ordner zeigen, Memos, die die ASCII-Text-Maps speichern bzw. den Hilfe-Text, und einer ListBox für die Textur-IDs.
Die Units, die wir von DelphiGL verwenden, sind: "DGLOpenGL", "easySDL", "SDL"
und "SDL_Image". Die kommen in den "uses"-Part der Haupt-Unit:
00001
00002
00003
00004
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, FileCtrl, ImgList, Grids, SortGrid,
DGLOpenGL, easySDL, SDL, SDL_Image;
Kompiliert man das Ganze, gibt es übrigens eine Menge Warnings zu den OGL-Units,
die man aber getrost ignorieren kann.
Im OnCreate-Ereignis der Form werden ein paar Variablen gesetzt und die OGL-Geschichte
initialisiert:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
procedure Thauptf.FormCreate(Sender: TObject);
begin
homedir:=extractfilepath(application.exename);
pbmp:=tbitmap.Create;
ppbmp:=tbitmap.Create;
DC:=GetDC(Handle);
if not InitOpenGL then Application.Terminate;
RC:=CreateRenderingContext(
DC,
[opDoubleBuffered],
32, //Farbbit-Tiefe
24,
0,0,0,
0
);
ActivateRenderingContext(DC,RC);
lightok:=_lightok;
setupgl;
ccolor:=clred;
init;
Application.OnIdle:=IdleHandler;
width:=640;
height:=480;
end;
In "homedir" speichern wir den Arbeitsordner des Programms weg; diese Information
wird später noch benötigt, wenn die Texturen geladen werden sollen.
Die Bitmaps "pbmp" und "ppbmp" verwenden wir dazu, eine kleine 2D-Ansicht unserer
3D-Welt wiederzugeben (die "Minimap"). Ausserdem werden wir später sehen, wie man
darüber eine einfache Kollisionskontrolle realisieren kann.
"GetDC" ist eine API-Funktion, die uns den Device-Context "DC" der Form liefert.
Über die OGL-Funktionen "CreateRenderingContext" und "ActivateRenderingContext" sorgen
wir anschliessend dafür, dass alle OpenGL-Ausgaben auf eben diesem "DC" erfolgen.
Ach ja, mit den Parametern von "CreateRenderingContext" habe ich herumgespielt,
aber viel bewirkt hat das nicht. Ich dachte z.B., ich könnte durch Reduzierung der
Farbtiefe mehr Speed herauskitzeln. Das klappte aber nicht. Vielleicht habe ich da
aber auch etwas falsch gemacht. Die Bedeutung der einzelnen Parameter habe ich
inzwischen ohnehin wieder vergessen.
Es folgen noch zwei Initialisierungsroutinen, "SetupGL" und "Init", die wir gleich
näher betrachten werden.
Gut geklaut ist die Idee, das OnIdle-Ereignis der Applikation auf eine eigene Funktion
umzubiegen. Wann immer die Applikation Luft hat, wird diese Funktion aufgerufen. Klar,
dass wir hier unsere ganzen Grafikarbeiten stattfinden lassen. Raffiniert!
Zuletzt wird die Fensterdimension festgelegt. Das ist an dieser Stelle nicht ganz
unwichtig, denn das bewirkt ein OnResize-Ereignis der Form, was noch weitere
Initialisierungen zur Folge hat.
Bei dem lahmen Rechner, den ich mein eigen nenne, hat es sich übrigens als nützlich
erwiesen, in der Programmierphase mit einer kleinen Form zu arbeiten, denn die
Grafikausgaben wurden dadurch enorm beschleunigt. Dadurch kommt man schneller an
die Positionen in unserer 3D-Welt, die gerade entwickelt werden.
Einige Eigenschaften unserer Welt müssen nur einmal vorgegeben werden und ändern sich
danach nicht mehr (oder nur selten). Zum Beispiel, von wo aus das Licht einfällt und
mit welcher Intensität, ob Objekte, die weiter hinten im Raum liegen, von denen
überdeckt werden, die weiter vorne sind, die Materialeigenschaften der Objekte usw.
Das erledigen wir mit der Funktion "SetupGL", die von FormCreate aufgerufen wird:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
procedure Thauptf.SetupGL;
var
light0_ambient,
light0_diffuse,
light0_pos,
lmodel_ambient,
mat_shininess,
mat_specular:array[0..3] of single;
fogCol:array[0..3]of single;
begin
//glDepthrange(1,mapz*100);
glDepthFunc(gl_less);
//Tiefentest aktivieren
glenable(GL_ALPHA_TEST);
glShadeModel(GL_SMOOTH);
glEnable(GL_BLEND );
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
glDepthMask(TRUE);
glDepthFunc(GL_LESS );
glenable(GL_DEPTH_TEST);
//lichtwerte
filllighta(light0_ambient,0.5,0.5,0.5,1.0); //Hintergrundlicht
filllighta(light0_diffuse,1,1,1,1.0); //gerichtetes Licht
filllighta(light0_pos,0,0,2,1.0);
filllighta(lmodel_ambient,1.0,1.0,1.0,1.0);
filllighta(mat_specular,1,1,1,1); //Reflektionslicht
filllighta(mat_shininess,90.0,0,0,0); //0=harter Übergang bis 128
// Lichtquelle 0 einstellen: links Fenster
glLightfv(GL_LIGHT0,GL_AMBIENT,@light0_ambient);
glLightfv(GL_LIGHT0,GL_DIFFUSE,@light0_diffuse);
glLightfv(GL_LIGHT0,GL_POSITION,@light0_pos);
//glLightfv(GL_LIGHT0,GL_SPOT_DIRECTION,@light0_pos);
// Beleuchtungsmodell wählen ...
glLightModelfv(GL_LIGHT_MODEL_AMBIENT,@lmodel_ambient);
//glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_FALSE);
glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER,GL_FALSE);
// Materialeigenschaften definieren
glMaterialfv(GL_FRONT_AND_BACK,GL_SHININESS,@mat_shininess);
glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,@mat_specular);
// ...und Beleuchtung einschalten.
if lightok then begin
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
end;
//Nebel
//GL_EXP, GL_EXP2, GL_LINEAR
glFogi(GL_FOG_MODE,GL_LINEAR);
fogcol[0]:=0.5;fogcol[1]:=0.5;fogcol[2]:=0.5;
glFogfv(GL_FOG_COLOR,@fogCol); // Farbe
glFogf(GL_FOG_DENSITY,0.35); // je dichter je grösser
//gl_dont_care, gl_nicest, gl_fastest
glHint(GL_FOG_HINT,GL_DONT_CARE); // Fog Hint Value
glFogf(GL_FOG_START,5.0); // Start Nebel
glFogf(GL_FOG_END,10.0); // Ende Nebel
if fogok then glEnable(GL_FOG);
glenable(GL_TEXTURE_MATRIX);
end;
Es lohnt sich, mit den Parametern der Funktionen herumzuspielen. Die Bedeutung der
einzelnen Parameter kann man über Google ersurfen.
Schön finde ich bei OGL, dass man Umgebungsvariablen eines bestimmten "Themas" wie
etwa "Beleuchtung" oder "Nebel" erst setzen kann, und sich diese Themen
anschliessend über eine einzelne "enable"-Funktion en block an- und ausschalten lassen.
Mithilfe einiger Globals wie "lightok" oder "fogok", die über Tastaturcodes geändert
werden können, lassen sich so schnell grosse Änderungen am Erscheinungsbild unserer
3D-Welt vornehmen.
Ich habe dann doch noch Schlaf gefunden. Nun ist ein neuer Tag und ich bin ausgeruht,
es kann also weitergehen.
Die nächste Initialisierungsroutine "Init" baut uns nun unsere Welt zusammen, welche
in die eben definierte OpenGL-Umgebung platziert wird.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
procedure Thauptf.init;
procedure settx(fn:string;r:integer);
var
tex:PSDL_Surface;
begin
tex:=IMG_Load(pchar(fn));
if assigned(tex) then begin
//glGenTextures(1,@tx);
glGenTextures(1,@txa[r]);
glBindTexture(GL_TEXTURE_2D,txa[r]);
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);
// Achtung! Einige Bildformate erwarten statt GL_RGB, GL_BGR.
//Diese Konstante fehlt in den Standard-Headern
glTexImage2D(
GL_TEXTURE_2D,0,3,tex^.w,tex^.h,0,
GL_RGB,GL_UNSIGNED_BYTE,
tex^.pixels
);
SDL_FreeSurface(tex);
end;
end;
var
r:integer;
begin
for r:=0 to _txamax-1 do begin
txa[r]:=0;
end;
//Personen-Texturen als erstes
pflb.Directory:=homedir+'\personen';
for r:=0 to pflb.items.count-1 do begin
settx(pflb.Directory+'\'+pflb.Items[r],r);
end;
if _startmap='Info' then mapu.setmap_info
else if _startmap='Flur' then mapu.setmap_flur
else if _startmap='Mode' then mapu.setmap_mode
else if _startmap='EDV' then mapu.setmap_edv;
boxmode:=_box;
p.visible:=_panelok;
FormResize(Self);
sethome;
end;
Die Konstante "_txamax" gibt die maximale Anzahl von Objekten wieder,
die in unserer Welt verwaltet werden können. Bei uns sind das 200 Objekte.
Zunächst wird das Integer-Array "txa" komplett auf null gesetzt. Hier drin
werden die IDs der einzelnen Texturen gespeichert.
Anschliessend durchlaufen wir den Unterordner "personen", in dem die Texturbilder
der Menschen unserer Welt liegen. Wie alle anderen Texturen, die wir verwenden,
handelt es sich um JPG-Bilder mit der Dimension 256 x 256 Pixel.
Aus der möglichen Grösse der Texturen wurde ich nie so ganz schlau. OpenGL kann
durchaus mit grösseren Bildern als 256 x 256 Pixeln arbeiten. Auf meinem Geschäfts-PC
gab es damit jedenfalls keine Probleme. Aber bei meinem Home-PC werden solche Texturen
nicht angezeigt. Noch erstaunlicher ist, dass ein zweiter Geschäfts-PC sie auch
nicht anzeigte, obwohl er mehr Speicher und Power als der erste hat. Ich vermute
einmal, es ist die Grafikkarte, die entscheidet, wie gross die Texturen letztlich
werden dürfen.
Also, jedes gefundene Bild wird an die interne Funktion "settx" übergeben. Diese läd
das Bild mittels der OpenGL-Funktion "IMG_Load" ein und bindet es an die OGL-Umgebung.
Als Referenz erhalten wir eine ID zurück, die über die Befehle "glGenTextures"
und "glBindTexture" im "txa"-Array gesichert wird.
Das wir die Personenbilder an dieser Stelle und am Anfang unserer Textur-Arrays laden
hat einen einfachen Grund: Im Gegensatz zu den Raum-Objekten, die wir anschliessend
verarbeiten, können die Personen in allen Räumen auftauchen, nicht nur in einem. Für
ortsgebundene Objekte wie ein Tisch gilt das nicht (klar, in der realen Welt kann man
natürlich auch einen Tisch in einen anderen Raum tragen, aber in unserer virtuellen Welt
ist das nicht vorgesehen).
OGL_HENRYs ist eine Welt, die aus einem quasi unendlich grossem Raum besteht,
in dem vier Unterräume mit Objekten liegen. Es sind dies die Räume "Info",
"Flur", "Mode" und "EDV". Aus Platz- und Geschwindigkeitsgründen ist zu einer Zeit
immer nur einer der vier Räume "mit Leben gefüllt".
Grob sieht die HENRY's Welt in etwa so aus:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
leerer, unendlicher Raum
################################
# # #
# MODE # EDV #
# # #
# # #
# # #
# # #
################################
# FLUR #
################################
# #
# INFO #
# #
################################
leerer, unendlicher Raum
Befinden wir uns im Raum "Info", wird in "init" die Funktion "mapu.setmap_info"
aufgerufen, sind wir in den Raum "EDV" gegangen, dann wird "mapu.setmap_edv"
aufgerufen usw. Die Unit "mapu", in der diese Setup-Funktionen implementiert
wurden, sehen wir uns gleich noch an.
Zuletzt wird "FormResize" aufgerufen, wodurch unsere Welt neu auf die Form gezeichnet
wird. Die Funktion "sethome" bringt uns an eine definierten Position innerhalb des
gewählten Unterraums. Das ist wichtig, denn natürlich landet man z.B. an einer anderen
Position im Flur, je nachdem, ob man ihn von der Info oder der Mode aus betreten hat.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
procedure Thauptf.FormResize(Sender: TObject);
begin
//Hilfefenster an Formgrösse anpassen
helpm.left:=(width-helpm.width) div 2;
helpm.top:=(height-helpm.height) div 2;
//Viewport an Fensterdimension anpassen
glViewport(0,0,ClientWidth,ClientHeight);
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluPerspective(_brennweite,1.5,_NearClipping,_FarClipping);
glMatrixMode(GL_MODELVIEW);
//geänderte Ansicht neu malen
DrawScene;
end;
//springe im Raum an eine zuvor gewählte Position
procedure thauptf.sethome;
begin
//Raumkoordinaten
px:=map.startpos.x;
py:=map.startpos.y;
pz:=map.startpos.z;
//Richtung, in die man schaut
rotx:=map.startrot.x;
roty:=map.startrot.y;
rotz:=map.startrot.z;
end;
Die Funktionen, die den Aufbau unserer vier Unterräume übernehmen,
sind in der Unit "mapu" gekapselt.
Beim Betreten eines neuen Raumes wird - wie in "init" gesehen - eine
zugehörige "set_map"-Funktion aufgerufen. Die wollen wir uns nun einmal
näher am Beispiel "setmap_info" zur Initialisierung des "Info"-Raums ansehen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
procedure setmap_info;
begin
//Startwert
hauptf.map.startpos.x:=15.22;
hauptf.map.startpos.y:=1.77;
hauptf.map.startpos.z:=20.06;
hauptf.map.startrot.x:=0;
hauptf.map.startrot.y:=20;
hauptf.map.startrot.z:=0;
hauptf.map.h:=3;
//Durchgänge-ASCII-Zeichen
clrdurchgang;
hauptf.map.durchgang[0]:='i';
//lese Map, Objekte und Personen
rdmap('Info',hauptf.map);
//Deckenaufteilung bzw. Bodenaufteilung adaptieren
hauptf.map.txdeckewc:=hauptf.map.w/2;
hauptf.map.txdeckedc:=hauptf.map.d/2;
hauptf.map.txbodenwc:=hauptf.map.w/2;
hauptf.map.txbodendc:=hauptf.map.d/2;
//Ausgänge setzen
clrouta;
hauptf.map.outa[0].ch:='D';
hauptf.map.outa[0].map:='Flur';
hauptf.map.outa[0].mapstart:='90';
//springe startpos
hauptf.sethome;
end;
Zunächst wird die Startposition unseres "Avatars" im Raum festgelegt, wobei die
Zahlenwerte als Meter und Zentimeter interpretiert werden können. Die (0/0/0)-Koordinate
entspricht dem unteren linken Eck des "Info"-Raums. Wir stehen also nach den obigen
Startwerten etwa 15 m rechts davon und etwa 20 m tiefer im Raum drin. Die Augenhöhe
liegt bei 1,77 m, denn so gross bin ich selbst. Wer HENRY's aus Sicht eines Hundes
erleben will, kann hier ja einmal 50 cm oder so eintragen :-)
Für Programmierer aus der DirectX-Welt (Alternative zu OpenGL) bleibt übrigens festzuhalten,
dass sich der Nullpunkt bei OpenGL stets unten links befindet, nicht wie beim Microsoft
üblich oben links! Die OpenGL-Variante ist die natürlichere, wie ich finde; das
Umdenken tat teilweise aber trotzdem weh - insbesondere, weil wir später auch noch
eine 2D-Map für die Kollisionskontrolle machen, bei der wieder die MS-Regeln gelten.
Die nächsten drei Werte geben an, in welche Richtung wir schauen (und uns bewegen).
Die Angaben sind als Grad-Werte zu verstehen, reichen also von 0 bis 359. 0/0/0
bedeutet, wir schauen exakt geradeaus, also direkt in den Raum hinein. In unserem
Fall mache wir eine leichte Drehung von 20 Grad um die y-Achse - wie eine Tänzerin
an einer Stange.
Startet man das Programm, stellen wir vielleicht überrascht fest, dass wir
nach links schauen, nicht nach rechts, wie man vermuten könnte. Denn tatsächlich ist
es bei OpenGL so, das wir, der Betrachter, uns gar nicht bewegen oder rotieren,
sondern nur der Raum um uns herum! Und wenn der sich um 20 Grad nach rechts dreht,
schauen wir als Resultat eben gerade 20 Grad nach links in den Raum hinein.
Um die Sache noch verwickelter zu machen: Bei den Bewegungskoordinaten habe ich die
Werte offenbar doch so umgerechnet, als würden wir uns bewegen, nicht der Raum.
Beim Rotieren jedoch habe ich das - wie gerade beschrieben - nicht getan. Mh ...
nicht schön ... Wohl vergessen. Egal, leben wir nun damit.
Die Variable "hauptf.map.h" gibt die Höhe unseres Raumes an. Der "Info"-Raum hat
also eine Höhe von drei Metern (so wie dies auch der Realität entspricht). Andere
Räume, wie etwa der Flur, sind dagegen sehr viel höher. Was ebenfalls der
gegebenen Wirklichkeit von HENRY's Auktionshaus entnommen ist.
An dieser Stelle sei kurz angedeutet, wie wir unsere Welt eigentlich in die
OpenGL-Umgebung hineinbekommen wollen. Wenn man so will, war dies der für mich
kreativste Teil der ganzen Arbeit. Denn Anfangs hatte ich keinen blassen Schimmer,
wie ich das bewerkstelligen könnte.
Bei vielen OpenGL-Programmen, die man im Web findet, erkennt man im Source, wie die
Objekte in sehr langen Listen einzeln definiert werden. Also ihre genaue Position
im Raum, ihre Rotation, ihre exakten Ausmasse, die "aufgeklebten" Texturen dazu usw.
Ein Mörder-Job!
Nix für einen faulen Hund wie mich. Und wenn es auch eine drastische Einschränkung
der Wiedergabe der Realität darstellte, war mir schnell klar, dass ich einen anderen,
sehr viel einfacheren Weg beschreiten würde.
Ich füllte einfach eine Textdatei mit bestimmten Zeichen als quasi zweidimensionale
Ansicht meines Raumes, wobei jedes Zeichen für ein Objekt bestimmter Grösse und Art
steht. So verwende ich z.B. das Zeichen "W" als Wand, "T" als Theke, "t" als Tisch
usw. Gruppiert man gleiche Zeichen, dann wird das Objekt entsprechend vergrössert.
Dabei legte ich fest, dass (fast) jedes Zeichen-Objekt die Breite und Tiefe von
50 cm hat. Klar, das ergibt reichlich dicke Wände. Und eine Rotation der Objekte
liess sich so auch nicht ohne Weiteres realisieren; alles ist daher stur an den
Standard-Raumkoordinaten - Norden, Osten, Süden und Westen - ausgerichtet.
Aber hey, das genügt. Wir wollen hier ja keine Gebäude virtuell errichten, die
später Realität werden sollen (und in denen dann eventuell sogar irgendwelche
armen Menschen leben müssen). Wir bauen nur vereinfacht und virtuell nach,
was bereits lange Realität ist.
Okay, sehen wir uns mal an einem kleinen Beispiel an, wie man eine solche Textmap
gestalten kann:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
Ein leerer Raum, ringsherum umgeben von Wänden (W):
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
W W
W W
W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
Mit Tisch (t) ist es schöner:
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
W W
W W
W ttttt W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
Jetzt noch eine Theke (T) davor. Der Tisch wird etwas tiefer:
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
WTTTTTTTTTTTTTTTT W
W ttttt W
W ttttt W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
Und nun noch rasch einen Raum nebenan gebaut:
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
WTTTTTTTTTTTTTTTT W W
W ttttt W W
W ttttt W W
W W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Auf die Idee mit den ASCII-Text-Maps sind natürlich schon viele Programmierer
vor mir gekommen. Ich habe nur keinen OpenGL-Source gefunden, der mir gezeigt
hätte, wie ich diese Maps letztlich an den Grafik-Renderer verfüttern kann.
Denn das ist natürlich der nächste Schritt: Eine Art Parser muss die Text-Maps
durchlaufen, die gefundenen Zeichen interpretieren sowie ihre Position berücksichtigen
und daraus die entsprechende OpenGL-Objekte generieren, ganz so wie bei den direkt
programmierten Objekt-Listen.
Mh ... was ich hier Grafik-Renderer nennen, ist vielleicht gar kein Grafik-Renderer.
Ist mir aber Wurst, ich nenne das Teil jedenfalls so. Das ist jedenfalls der Part
im Programm, der meine Liste an Objekten durchgeht und sie an die definierten Positionen
im Raum malt, wobei stets dafür gesorgt wird, dass Objekte weiter hinten perspektivisch
verkleinert werden und keine Objekte verdecken, die weiter vorne liegen. Das praktische
bei der Verwendung von OpenGL hierbei ist, dass es die ganze Mathematik dahinter für
uns mehr oder weniger automatisch erledigt.
Zu diesem Grafik-Renderer kommen wir aber erst später.
Machen wir weiter mit der "setmap_info"-Prozedur.
Nachdem wir unsere Position im Raum definiert haben, füllen wir nun das Array
"hauptf.map.durchgang" mit bestimmten Zeichen, die der Grafik-Renderer später als
"Durchgänge" begreifen soll. Nun ja, eigentlich sind es eher Objekte, die keine
Kollision bewirken sollen, wenn man sie erreicht. Üblicherweise sollte man ja
durch einen Tisch nicht durchlaufen können. An einer Bodenkachel hängen zu bleiben
macht dagegen wenig Sinn.
Im Falle der "Info"-Map bezeichnet das Zeichen "i" eine solche Bodenkachel. Bei
anderen Maps, wie etwa dem Flur, entspricht "i" dagegen einem Teppich, der auf
dem Boden liegt, "F" eine Art Tor, unter dem man durchgehen kann usw.
Im nächsten Schritt wird die aktuelle MAP-Struktur geladen: "rdmap('Info',hauptf.map)".
Dazu gleich mehr.
Nun folgen ein paar "Grafik"-Definitionen, die speziell für diesen Raum gelten.
Boden und Decke unserer Räume haben eine einheitliche Textur verpasst bekommen.
Sie werden nämlich nicht, wie alle anderen Objekte im Raum, über die ASCII-Text-Map
platziert, sondern einfach - mathematisch berechnet - über die komplette Raumgrösse
gespannt. Um doch noch ein wenig Abwechslung hinein zu bringen, werden die Boden-
und Decken-Texturen über ein paar Struktur-Variablen in raumspezifischer Weise
"gekachelt".
Nun kann man Räume betreten und wieder verlassen. Dafür werden Ausgänge definiert.
Das machen wir über ein Array "hauptf.map.outa". Es ist Teil der Map-Struktur und
wird wie folgt deklariert:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
tout=record
ch:char;
map:string;
mapstart:string;
end;
tmap=record
...
outa:array[0..5]of tout;
durchgang:array[0..5]of char;
end;
Dabei gilt: Jedes Zeichen, dass im Ausgang-Array in "ch" aufgelistet wird, hat für
den Grafik-Renderer die spezielle Bedeutung eines Ausgangs (ähnlich wie beim Durchgang).
Kommen wir an eine solche Stelle, sagt die Kollisionskontrolle nicht "Autsch! Du bist
gegen eine Wand gerannt", sondern veranlasst vielmehr die Neuinitialisierung der sich
am Ausgang bzw. Eingang anschliessenden Text-Map (beschrieben in "map" mit Startkennung
"mapstart").
Konkret heisst das in unserem Fall: Komme ich im "Info"-Raum an eine Stelle, die in der
Textmap mit "D" markiert ist, dann wechselt das Programm in die Map "Flur" und sucht
dort nach den Zeichen "90", um mich genau an diese Stelle zu platzieren.
Das "outa"-Array erlaubt es, maximal fünf verschiedene Ausgänge je Map setzen zu können.
Die "Info"-Map hat aber nur einen.
Betrachten wir nun die "rdmap"-Prozedur, die unsere Textmap von der Platte liest,
zeilenweise und zeichenweise durchgeht, und daraus die zu den Zeichen gehörenden
OpenGL-Objekte generiert.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
procedure rdmap(fn:string;var map:tmap);
procedure settx(fn:string;r:integer);
var
tex:PSDL_Surface;
begin
tex:=IMG_Load(pchar(fn));
if assigned(tex) then begin
glGenTextures(1,@hauptf.txa[r]);
glBindTexture(GL_TEXTURE_2D,hauptf.txa[r]);
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);
// Achtung! Einige Bildformate erwarten statt GL_RGB, GL_BGR.
//Diese Konstante fehlt in den Standard-Headern
glTexImage2D(
GL_TEXTURE_2D,0,3,tex^.w,tex^.h,0,
GL_RGB,GL_UNSIGNED_BYTE,
tex^.pixels
);
SDL_FreeSurface(tex);
end;
end;
procedure getdim(ch:char;x,z:byte;var w,d:byte);
var
s:string;
r,c:integer;
fullok:bool;
begin
//Startpunkt immer, daher löschen
s:=hauptf.mapm.lines[z];
s[x]:='<';hauptf.mapm.lines[z]:=s;
//wie breit ist Objekt?
w:=1;
c:=x+1;
while(c<length(s))and(s[c]=ch) do begin
//markiere waagrechte Blöcke
s[c]:='_';hauptf.mapm.lines[z]:=s;
inc(w);
inc(c);
end;
//wie tief ist Objekt
d:=1;
fullok:=_spanok;
for r:=z+1 to hauptf.mapm.lines.count-1 do begin
s:=hauptf.mapm.lines[r];
if length(s)<x then break;
if s[x]<>ch then break;
//markiere senkrechte Blöcke
s[x]:='|';hauptf.mapm.lines[r]:=s;
if _spanok then begin
//über die Tiefe volle Breite mit Objekt-Blöcken?
if fullok then begin
c:=x+1;
while(c<x+w)and(s[c]=ch) do begin
//markiere waagrechte Blöcke
s[c]:='>';hauptf.mapm.lines[r]:=s;
inc(c);
end;
if c<x+w then fullok:=false;
end;
end;
inc(d);
end;
//Einzelblock?
if(w=1)and(d=1)then exit;
//Raum nicht voll aufgespannt?
if not fullok then begin
//setze Full-Blöcke zurück
s:=hauptf.mapm.text;
s:=stringreplace(s,'>',ch,[rfreplaceall]);
if w>=d then begin
d:=1;
s:=stringreplace(s,'_',' ',[rfreplaceall]);
s:=stringreplace(s,'|',ch,[rfreplaceall]);
end
else begin
w:=1;
s:=stringreplace(s,'_',ch,[rfreplaceall]);
s:=stringreplace(s,'|',' ',[rfreplaceall]);
end;
hauptf.mapm.text:=s;
end
else begin
s:=hauptf.mapm.text;
s:=stringreplace(s,'>',' ',[rfreplaceall]);
s:=stringreplace(s,'_',' ',[rfreplaceall]);
s:=stringreplace(s,'|',' ',[rfreplaceall]);
hauptf.mapm.text:=s;
end;
end;
var
r,c:word;
s,ss,bufs:string;
anz:word;
w,d:byte;
begin
hauptf.map.name:=fn;
//alte Texturen löschen (ausser Personen)
hauptf.texturesfree;
//Texturen laden
hauptf.flb.Directory:=hauptf.homedir+fn;
for r:=0 to hauptf.flb.items.count-1 do begin
settx(
hauptf.flb.Directory+'\'+hauptf.flb.Items[r],
r+hauptf.pflb.Items.count
);
end;
//Map-Standard-Texturen
hauptf.map.txdecke:=
hauptf.flb.Items.indexof('txdecke.jpg')+
hauptf.pflb.Items.count;
hauptf.map.txboden:=
hauptf.flb.Items.indexof('txboden.jpg')+
hauptf.pflb.Items.count;
//lade Map in Memo
hauptf.mapm.lines.loadfromfile(hauptf.homedir+fn+'/_map.txt');
//Map retten
bufs:=hauptf.mapm.Text;
//zähle Objekte-Blöcke
s:=trim(hauptf.mapm.Text);
anz:=0;
for c:=1 to length(s) do begin
if(s[c]=' ')or(s[c]=#10)or(s[c]=#13)then continue;
inc(anz);
end;
//Map löschen und erst einmal überdimensionieren
setlength(map.quads,0);
setlength(map.quads,anz);
map.anz:=anz;
hauptf.map.w:=0;
hauptf.map.d:=0;
//hole Objekte und Personen
anz:=0;
for r:=0 to hauptf.mapm.lines.count-1 do begin
s:=hauptf.mapm.lines[r];
if length(s)>hauptf.map.w then hauptf.map.w:=length(s);
for c:=1 to length(s) do begin
if s[c]=' ' then continue;
if s[c]='<' then continue;
//Person?
if s[c] in ['0'..'9'] then begin
ss:=s[c]+s[c+1];
s[c]:='<';s[c+1]:=' ';
hauptf.mapm.lines[r]:=s;
w:=1;
d:=1;
end
else begin
//neues Objekt: bestimme Dimension
getdim(s[c],c,r,w,d);
ss:=s[c];
end;
//binde Textur und Höhe an Objekt
if s2quad(ss,c-1,r,w,d,map.quads[anz]) then begin
//erhöhe Objektzähler
inc(anz);
end;
s:=hauptf.mapm.lines[r];
end;
hauptf.mapm.lines[r]:=s;
end;
//korrigiere Anzahl Objekte
map.anz:=anz;//map.anz-anzc;
hauptf.map.d:=hauptf.mapm.lines.count;
//Decken- und Boden-Textur-Aufteilung
map.txdeckewc:=hauptf.map.w;
map.txdeckedc:=hauptf.map.d;
map.txbodenwc:=hauptf.map.w;
map.txbodendc:=hauptf.map.d;
//hauptf.showtxa;
//map zurücksetzen
hauptf.mapm.Text:=bufs;
//panel füllen
panelu.initp;
end;
Zunächst werden alle alten Texturen von eventuell zuvor geladenen Maps
gelöscht, um den Speicher wieder frei zu bekommen.
Dann werden alle Texturen geladen, die sich im zugehörigen Unterordner
auf der Platte befinden. Das Verfahren ähnelt sehr dem, dass wir schon
weiter oben für die Personen-Texturen verwendet haben. Wichtig ist, dass
das Array nicht von vorne aufgefüllt wird, sondern ab Position
"hauptf.pflb.Items.count" - die Personen-FileListBox zeigt dabei auf
den Ordner mit den Personenbildern. Dadurch gehen die zuvor geladenen
Personentexturen nicht verloren. Und das Programm ist so adaptiv, dass
jederzeit neue Personenbilder hinzukommen können, ohne dass dadurch die
Indizes verrutschen.
Anschliessend werden die Boden- und Decken-Texturen gesondert geladen,
da deren Verarbeitung anders erfolgt als die der Textmap-Objekte (wie
weiter oben erwähnt).
Jetzt wird die Textmap in ein TMemo "hauptf.mapm" geladen.
In einem nächsten Schritt wird die Textmap durchlaufen und alle
Zeichen gezählt, die ungleich Carriage Return und Leerzeichen sind.
Diese Zahl "anz" gibt uns damit die Anzahl der Objekte wieder, die
in der Map Verwendung finden.
Es handelt sich um eine maximale Anzahl, die nur im Worst Case wirklich
gegeben ist. Denn wie wir gleich sehen werden, lassen sich Gruppen von
Zeichen zusammenfassen und bilden so statt z.B. 20 Einzelobjekten nur ein
grosses Objekt.
Mann, das war eine Schufterei bis diese Gruppierung von Objekten zu
Einzelobjekten korrekt funktioniert hat. Aber der Vorteil liegt auf
der Hand: Wenn der Grafik-Renderer statt 200 Objekten nur 30 verwalten
muss, wird die Ausgabe erheblich flüssiger. Wo ich vorher nur
ruckelnderweise durch die Räume kam, kann ich jetzt geradezu fliegen :-)
Zunächst allokieren wir aber erst einmal Speicher für den Worst Case.
Wir verwenden dazu die Funktion "setlength", um eine "anz"-grosse
Struktur von "map.quads" anzulegen, die wie folgt definiert sind:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
type
tpos=record
x,y,z:single;
end;
tdim=record
w,h,d:single;
end;
trot=record
x,y,z:smallint;
end;
ttex=record
wc,dc:byte;
o,u,l,r,v,h:byte;
end;
tquad=record
pos:tpos;
dim:tdim;
tex:ttex;
end;
tout=record
ch:char;
map:string;
mapstart:string;
end;
tqa=array of tquad;
tmap=record
name:string; //Name der Map, z.B. 'info'
anz:word; //Anzahl TQuads
quads:tqa; //Folge von TQuads
fn:string; //Dateiname
txdecke:byte; //ID der Decken-Textur
txdeckewc:single; //Decken-Kachelung Breite
txdeckedc:single; //Decken-Kachelung Tiefe
//keine Höhen-Kachelung,
//denn die Höhe der Decke ist Null
// 'Zwischendecke' (nur für 'Flur')
txdeckez:byte;
txdeckezwc:single;
txdeckezdc:single;
txboden:byte; //ID Boden-Textur
txbodenwc:single; //Boden-Kacheln Breite
txbodendc:single; //Boden-Kacheln Tiefe
startpos:tpos; //Home-Position des Betrachters
startrot:trot; //Home-Rotation des Betrachters
w,d,h:single; //Breite, Tiefe und Höhe der Map
outa:array[0..5]of tout; //definierte Ausgänge
durchgang:array[0..5]of char; //definierte Durchgänge
end;
"Quads" sind - wie der Name schon sagt - einfache Quader.
Sie besitzen die Attribute "pos" mit ihrem Raumkoordinaten,
"dim" mit ihren Raumausmassen, und "tex" mit der ID der zugehörigen
Textur.
Die HENRY's Welt besteht also nur aus einer Aneinanderreihung von
Quadern. Runde Formen sucht man darin vergeblich. Es wäre allerdings nicht
all zu schwer, auch Kugeln oder Zylinder in das Programm zu integrieren,
man müsste halt zusätzlich Strukturen wie "tkugel" oder "tzylinder" mit
spezifischen Attributen wie "Radius" definieren.
Die Struktur MAP wird jeweils nur von einem Raum "besetzt" - obwohl man natürlich
auch leicht ein Array von Maps realisieren könnte. Aber da wir uns ja immer nur
in einem Raum zu einer Zeit befinden können und die Neu-Initialisierung der Map
nur wenige Sekunden dauert, wäre das eher Speicherverschwendung.
Die Text-Map wird nun zeilenweise aufgerufen. Wir prüfen jeweils, an welcher Position das
äusserste rechte Element zu finden ist. Das Maximum dieser Werte ergibt die Breite
unserer Map und wird in "hauptf.map.w" gesichert.
Die Tiefe der Map ergibt sich aus der Anzahl der Zeilen.
Die Höhe der Map wurde in der "setmap"-Prozedur festgeschrieben.
In "s" steht jeweils eine Zeile unserer Textmap. Diese wird nun
zeichenweise durchgegangen. Leerzeichen werden ignoriert; sie stellen
objektfreien Raum dar.
Auch das "<"-Zeichen wird ignoriert: Damit kennzeichnen wir Objekte,
die zuvor in ein Gruppenobjekt zusammen gefasst wurden, d.h., kein
eigenständiges Objekt bilden. Leerzeichen dürfen hier nicht gesetzt
werden, denn sonst befände sich hier ja für den Grafik-Renderer später
nichts, wodurch man durch Wände laufen könnte.
Achtung! Dieses Verfahren der Gruppen-Kennzeichnung bedeutet natürlich,
das bei der Erstellung der Text-Maps tunlichst auf die Verwendung des
"<"-Zeichens verzichtet werden sollte! Das gilt auch für die Zeichen
"|" und "_", die hier ebenfalls intern Verwendung finden.
Finden wir dagegen ein Zeichen zwischen '0' und '8', dann wird an diese
Stelle kein Objekt platziert, sondern eine Person. Personen werden stets
durch zwei aufeinander folgende Zahlen gekennzeichnet, wobei die so
entstandene Nummern 00-89 gerade dem Filenamen der zugehörigen Textur
im Personen-Dateiordner entsprechen.
Die Nummernfolge "90" bis "99" ist übrigens reserviert für einen anderen
Zweck: Hiermit werden die Ziel definiert, die wir erreichen, wenn wir einen
neuen Raum betreten. Dazu kommen wir aber erst später
So heisst mein Textur-File z.B. "tx_00.jpg". Um meinen Personen-Avatar also
in einer Text-Map zu platzieren, müssen irgendwo die Zeichen "00" auftauchen.
Das heisst wiederum auch, dass nur maximal 90 verschiedene Personen
in den Maps untergebracht werden können (die aber beliebig oft, was aber
zugegebenermassen wenig Sinn macht).
Finden wir ein anderes Zeichen, dann handelt es sich um ein neues Objekt.
Genauer: Es handelt sich um die obere linke Ecke eines neuen Objekts.
Die interne Funktion "getdim" prüft, ob und inwieweit sich anschliessende
Objekte zu einer Gruppe zusammen fassen lassen.
Das will ich hier mal an einem Beispiel verdeutlichen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
Textmap zu Beginn, der Parser sitzt bei P, wo vorher ein W stand.
Breite und Tiefe des Objekts ist 1.
PWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
W tttttttttt W
W tttttttttt W
W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Der Parser hat 'W' gefunden, also ein neues Objekt. Nun läuft er
nach rechts und prüft, ob er dort weitere 'W' findet. Jedes andere
Zeichen unterbricht den Prozess. In unserem Fall kommt er bis
Breite 31:
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWP
W W
W W
W tttttttttt W
W tttttttttt W
W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Nun geht der Parser in die nächste Zeile (Tiefe=2) und prüft, ob er dort
genauso viele 'W' wie in der ersten Zeile findet. Das tut er nicht,
also verwirft er die neue Höhe von 2 und setzt sich auf 1 zurück.
Das gruppierte Objekt hat also die Dimension 31 x 1.
Damit die zur Gruppe gehörenden Objekte nicht noch einmal geparst
werden, werden sie mit dem '<'-Zeichen markiert:
WP<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
W W
W W
W tttttttttt W
W tttttttttt W
W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Der Parser rennt weiter und erkennt als Nächstes zwei neue Gruppenobjekte,
nämlich die seitlichen Wände, jeweils mit der Dimension 1 x 7:
WP<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
W W
P <
< tttttttttt <
< tttttttttt <
< <
< <
<WWWWWWWWWWWWWWWWWWWWWWWWWWWWW<
Leereichen und '<' werden ignoriert, er finden schliesslich das 't'-Zeichen
und macht daraus ein weiteres Objekt mit der Dimension 2 x 10:
WP<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
W W
P <
< t<<<<<<<<< <
< <<<<<<<<<< <
< <
< <
<WWWWWWWWWWWWWWWWWWWWWWWWWWWWW<
Also ehrlich gesagt weiss ich nicht mehr, ob der Algorithmus exakt so
wie beschrieben vorgeht. In Zwischenschritten wird auch noch mit den
Zeichen "|" und "_" gearbeitet. Zudem dürfte ich mich im Beispiel sicher
irgendwo bei den Dimensionsangaben verzählt haben. Aber das Prinzip sollte
klarer geworden sein.
Haben wir eine Person oder ein Objekt und dessen zugehörige Ausmasse in
Breite und Tiefe ermittelt, werden diese Informationen an die Funktion
"s2quad" geschickt. Die schauen wir uns gleich an. Ausserdem wird der
Objektzähler "anz" neu hochgezählt, sodass er am Ende auf der echten
Anzahl Objekte in der Map steht.
Schliesslich haben wir die Text-Map durchgeparst und alle Objekte
erstellt. Nun werden noch ein paar Map-Variablen gesetzt und ganz am
Schluss mit "panelu.initp" die 2D-Minimap initialisiert.
Grrr! Was eine Fummelei mit diesen albernen Text-Zeichen bisher. Wann kommen
endlich die OGL-Objekte dran?
Geduld, wir nähern uns!
Wir betrachten zunächst allerdings die Funktion "s2quads", die unsere Textmap-Objekte
mit zugehöriger Dimension übergeben bekommt:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
function s2quad(ss:string;x,z,w,d:byte;var q:tquad):bool;
function filter(s:string;ch:char;v:byte):byte;
var
c:byte;
begin
result:=1;
//tx_ww_h4_ww_d11.jpg
//Vorbau weg -> _ww_d11.jpg
s:=copy(s,9,length(s));
//suche Parameter
c:=pos('_'+ch,s);
if c=0 then exit;
//Parameter weg -> w_d11.jpg
s:=copy(s,c+2,length(s));
if length(s)=0 then begin
beep;
exit;
end;
//x-Dimensional?
if s[1]=ch then begin
result:=v;
exit;
end;
//Anzahl extrahieren
c:=1;while(c<length(s))and(s[c] in ['0'..'9'])do inc(c);
try
result:=strtoint(copy(s,1,c-1));
except
beep;
end;
end;
var
fn,s:string;
foundok:bool;
r:integer;
tx:gluint;
y,h:single;
ch:char;
begin
result:=true;
ch:=ss[1];
if(ch>='0')and(ch<='9')then begin
//Person: bestimme tex-Index
s:='tx_'+ss+'.jpg';
r:=hauptf.pflb.Items.indexof(s);
if r=-1 then begin
result:=false;
exit;
end;
y:=1;h:=1;
setpersonquad(r,x,y,z,w,h,d,q);
end
else begin
//Eintrag holen
foundok:=false;
tx:=0;
for r:=0 to hauptf.flb.Items.count-1 do begin
s:=lowercase(hauptf.flb.Items[r]);
//Grossbuchstabe?
if ch=uppercase(ch) then foundok:=copy(s,4,2)=lowercase(ch+ch)
else foundok:=copy(s,4,2)=lowercase(ch+'0');
if foundok then begin
tx:=r+hauptf.pflb.Items.count;
fn:=hauptf.flb.Items[r];
break;
end;
end;
if not foundok then begin
beep;
exit;
end;
//Höhe des Objekt (h) und Höhenposition des Objekt (y) bestimmen
//tx_ww_h4.jpg
s:=copy(s,7,2);
y:=0;h:=hauptf.map.h;
if s='h0' then begin h:=0;y:=0.01;end
else if s='h1' then h:=0.8
else if s='h2' then begin h:=0.6;y:=0.8;end
else if s='h3' then h:=2
else if s='h4' then begin h:=0.8;y:=1.8;end
else if s='h5' then h:=hauptf.map.h-4
else if s='h6' then begin h:=hauptf.map.h-4;y:=4;end
else if s='h7' then h:=hauptf.map.h-2
else if s='h8' then h:=hauptf.map.h-1
else if s='h9' then h:=hauptf.map.h
else begin
beep;
end;
setobjquad(tx,x,y,z,w,h,d,q);
//if fn='tx_rr_h9_w3_d3.jpg' then beep;
//Aufteilung Textur Breite
//tx_ww_h4_ww_d11.jpg
q.tex.wc:=filter(fn,'w',w);
q.tex.dc:=filter(fn,'d',d);
//Textur andere Seite?
fn:=copy(fn,1,length(fn)-4);
r:=hauptf.flb.items.indexof(fn+'_o.jpg');
if r>-1 then q.tex.o:=r++hauptf.pflb.Items.count;
r:=hauptf.flb.items.indexof(fn+'_r.jpg');
if r>-1 then q.tex.r:=r+hauptf.pflb.Items.count;
r:=hauptf.flb.items.indexof(fn+'_v.jpg');
if r>-1 then q.tex.v:=r+hauptf.pflb.Items.count;
r:=hauptf.flb.items.indexof(fn+'_h.jpg');
if r>-1 then q.tex.h:=r+hauptf.pflb.Items.count;
end;
end;
Wir schauen anhand des ermittelten Zeichencodes nach, ob es sich bei dem zu generierenden
Objekt um eine Person oder um ein Objekt handelt.
Ist es eine Personen, können wir aus dem - in diesem Fall stets zweistelligem - Zeichencode
"ss" den zugehörigen Dateinamen der Textur bestimmen: "tx_"+ss+".jpg". Über die
IndexOf-Funktion der Personen-FileListBox "pflb" finden wir den Index der Datei,
der gerade dem Index unseres Textur-Arrays "txa" entspricht. Dann rufen wir
"setpersonquad" auf.
Handelt es sich um ein Objekt-Zeichencode, dann muss etwas aufwendiger vorgegangen werden.
Personen in der HENRY's-Welt werden ziemlich primitiv und einheitlich dargestellt: Als
mit Texturen rundherum beklebte Quader mit immer den gleichen Ausmassen.
Objekte sind auch nur Quader, sollen aber teilweise z.B. von oben anders aussehen als von
der Seite (wie etwa bei einem Tisch). Das verlangt verschiedene Texturen je Objekt. Zudem
haben Objekte verschiedene Höhen. Und manchmal ist es darüber hinaus auch wünschenswert,
einen Seitenwand nicht mit einer durchgehenden Textur zu versehen, sondern diese zu
kacheln, wie es etwa bei einem Schrank oder einem Regal an der Wand sinnvoll sein könnte.
All diese Zusatzinformationen zu einem Objekt können nicht in unsere 2D-Text-Maps
eingearbeitet werden, denn dort steht uns ja nur ein Zeichen je Objekt zur Verfügung,
was die maximale Anzahl verschiedener Objekte je Raum ohnehin schon stark einschränkt.
Um unseren Objekt-Parser einfach und flexibel zu halten und ohne am Ende doch lange Listen
von Objekten direkt im Source codieren zu müssen, packen wir die Zusatzinformationen zu den
Objekten einfach in deren Textur-File-Namen! Das will ich am Zeichen "r" für ein Regal
verdeutlichen:
Die Funktion "s2quad" hat im Zeichencode "s" den Wert "r" übermittelt bekommen. Es handelt
sich um ein kleines "r", so merken wir uns "r0" (sonst "rr"). Wir suchen in der FileListBox
der Objekte "flb" nach mindestens einem Dateinamen, der an passender Stelle (4 und 5 Zeichen)
diese beiden Buchstaben enthält.
Im "Info"-Ordner werden wir fündig; der Dateinamen lautet "tx_r0_h9_w3_d3.jpg"
Textur 'tx-r0-h9-w3-d3.jpg': Eine Textur für ein Regal im Raum 'Info'. Im recht kryptisch wirkenden Dateinamen sind zusätzliche Angaben zur Anwendung der Textur in unserer virtuellen Welt codiert, etwa wie hoch das damit ausgestattete OpenGL-Objekt werden soll (hier Höhenklasse 'h9').
Wir scannen nun nach dem Höhencode, der mit "_h" beginnt, gefolgt von einer Zahl zwischen
"0" und "9". Diese Zahl ist der Code für eine von 10 verschiedenen Höhendefinitionen,
die einmal die Höhe des Objekts angeben ("h"), zum anderen seine Höhenposition im Raum ("y").
Ist die Zahl "0", hat das Objekt keine Höhe (wie etwa ein Teppich) und "schwebt" ganz knapp
über dem Boden, ist die Zahl "1", ist das Objekt 80 cm hoch und steht auf dem Boden, "2"
bedeutet 60 cm hoch, beginnend bei einer Höhe von 80 cm (nützlich z.B. für ein Objekt, welches
auf einem Tisch steht).
Die grösseren Zahlencodes gehen gewissermassen umgedreht vor: Von der raumspezifischen
Höhe aus subtrahieren sie einen bestimmten Wert. So bedeutet etwa der Code "8" eine Höhe
von "Raumhöhe minus einem Meter und bis zum Boden reichend". Das hat den Vorteil, dass
man nachträglich die Gebäudehöhe ändern kann und die Objekte quasi mitwachsen oder
mitschrumpfen.
In unserem Fall finden wir den Höhencode "9", was gleichbedeutend mit "Raumhöhe bis zum
Boden" ist, in dem sich das Objekt befindet. Der "Info"-Raum ist, wie weiter oben angegeben,
3 m hoch. Regale in der "Info", die mit einem kleinen "r" in der Textmap markiert sind, haben
also eine Höhe von 3 Metern.
Diese Information geben wir an die Prozedur "setobjquad" weiter - ganz so wie bei Personen
mit der Prozedur "setpersonquad". In beiden Fällen füllen wir damit eine Struktur, die der
Grafik-Renderer später "zu Papier" bringen kann.
Anders als bei Personen geht 's mit den Objekten danach aber weiter: Sie können mehr als nur
eine Textur besitzen und die Textur kann in spezifischer Weise über eine Seitenfläche
gekachelt werden.
In unserem Beispiel finden wir im Regal-Textur-Dateinamen "tx_r0_h9_w3_d3.jpg" zwar keinen
Code für mehrere Signaturen. Jedoch sagen uns die Codes "_w3" und "_d3", dass die Textur je
Seite in drei Teile gekachelt werden soll, jeweils in der Breite ("_w") und in der Tiefe ("_d").
Textur ohne Kachelung: Das Textur-Bild des Regals wird über das komplette OpenGL-Objekt gespannt. Dadurch wirkt der Regalinhalt hinten links jedoch unnatürlich gross.
Textur mit Kachelung: Das Textur-Bild des Regals wird diesmal gekachelt, d.h., es legt sich in wiederholter Weise über das OpenGL-Objekt des Schranks. Dadurch passt sich die scheinbare Grösse des Regalinhalts einigermassen an die Umgebung angespannt.
Sehen wir uns noch schnell das Zeichen "T" für Theke an: Hier finden wir in der Textur-FileListBox
gleich zwei Texturen mit den Zeichencode "_tt":
Textur 'tx-tt-h1.jpg': Textur #1 für die Seiten des Theken-OpenGL-Objekts.
Textur 'tx-tt-h1-o.jpg': Textur #2 für die obere Platte des Theken-OpenGL-Objekts. Erkennbar am '*-o' im Dateinamen.
Die Filenamen liefern uns den Höhencode "_h1", d.h., das Objekt soll 80 cm hoch sein und
bis zum Boden reichen. Es gibt keine Codes für die Textur-Kachelung, d.h., jede Textur
wird über die komplette Seite aufgespannt.
Anders als im vorherigen Fall finden wir aber im Dateinamen "tx_tt_h1_o.jpg" den Code "_o".
Der bedeutet, dass diese Textur nur auf die Oberseite des zugehörigen Objektes "T" geklebt
werden soll. Alle anderen Seiten bekommen die "Seiten-neutrale" Textur "tx_tt_h1.jpg".
Neben "_o" für "Oben" gibt 's noch die Codes "_v" und "_h" für "Vorne" und "Hinten"
Wollen wir also sämtlichen Theken in der "Info" von vorne ein anderes Erscheinungsbild
verpassen, muss nicht eine Zeile neu programmiert werden; es genügt, ein Textur-File mit
passendem Namen in den Datei-Ordner zu schmeissen:
Theke mit Standard-Textur
Textur 'tx-tt-h1-v.jpg': Textur für einen Grossrechner.
Theke mit alternativer Textur: Eine neue Front für die Theken im Raum 'Info'
In gleicher Weise kann man auch Objekte in ihrer Höhe verändern, in dem man
einfach den Höhencode im Dateinamen der zugehörigen Textur ändert:
Theke mit Standard-Höhenklasse
Theke mit alternativer Höhenklasse: Geheimnisvolles Wachstum der Theke im Raum 'Info'.
Wir haben unsere Textmap eingelesen und alle Objekte und Personen herausgeparst. Dann
haben wir über die zugehörigen Textur-Dateinamen zusätzliche Informationen zum
Erscheinungsbild gewonnen. Bleibt nur, diese Informationen an Strukturen zu übergebe,
die der Grafik-Renderer später "verstehen" kann.
Das ist eine ziemlich unspektakuläre Aktion, wie man hier sieht:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
//Personen in Quad-Struktur
procedure setpersonquad(tx:gluint;x,y,z,w,h,d:single;var q:tquad);
begin
q.pos.x:=x;q.pos.y:=y;q.pos.z:=z;
q.dim.w:=w;q.dim.h:=h;q.dim.d:=d;
//q.rot.x:=0;q.rot.y:=0;q.rot.z:=0;
q.tex.wc:=1;
q.tex.dc:=1;
q.tex.o:=tx;q.tex.u:=tx;
q.tex.l:=tx;q.tex.r:=tx;
q.tex.v:=tx;q.tex.h:=tx;
end;
//Objekte in Quad-Struktur
procedure setobjquad(tx:gluint;x,y,z,w,h,d:single;var q:tquad);
begin
q.pos.x:=x;q.pos.y:=y;q.pos.z:=z;
q.dim.w:=w;q.dim.h:=h;q.dim.d:=d;
//q.rot.x:=0;q.rot.y:=0;q.rot.z:=0;
q.tex.wc:=1;
q.tex.dc:=1;
q.tex.o:=tx;q.tex.u:=tx;
q.tex.l:=tx;q.tex.r:=tx;
q.tex.v:=tx;q.tex.h:=tx;
end;
Wir füllen je Person bzw. Objekt einfach die zugehörige Quad-Struktur
der "hauptf.map". Alle nötigen Variablen haben wir zuvor ermittelt.
Wie man sieht, ist hier etwas Source auskommentiert worden, der eine Rotation
der Objekte ermöglicht. Ich habe das nicht weiter verfolgt, in "OGL_HENRYs"
ist alles orthogonal an den Achsen ausgerichtet. Hier wäre aber die richtige
Stelle, das Modell zu erweitern. Aber natürlich müsste dann auch der Grafik-Renderer
entsprechend angepasst werden.
Unsere Anfangs beschriebene Prozedur "init" ist nun komplett abgearbeitet.
Das Programm hat nichts mehr zu tun, es ist "idle". Nun schlägt die Stunde unseres
Idle-Handlers, den wir in "FormCreate" auf die Funktion "IdleHandler" umgebogen haben.
Denn diese wird nun automatisch aufgerufen.
00001
00002
00003
00004
00005
00006
procedure Thauptf.IdleHandler(Sender: TObject; var Done: Boolean);
begin
drawscene;
if _bremse>0 then sleep(_bremse);
done:=false;
end;
Die Konstante "_bremse" kann man verwenden, um die Grafikausgabe zu verlangsamen.
Das kann sinnvoll sein, wenn man in der Programmierphase Mal-Aktionen im Detail
analysieren will. Üblicherweise hat "_bremse" aber natürlich den Wert "0".
Wir widmen uns nun der Funktion "drawscene", die über den Idle-Handler aufgerufen
wird, wann immer das Programm Zeit dazu findet.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
//binde Textur-ID ins Modell ein -----------------------
procedure thauptf.coltex(tx:byte);
begin
glBindTexture(GL_TEXTURE_2D,txa[tx]);
glEnable(GL_TEXTURE_2D);
end;
//male Decke, Boden und alle Quads des Modells ---------
procedure Thauptf.DrawScene;
var
x,y,z,r:integer;
dd,w,h,d:single;
scenerotz,sceneroty,xtrans,ytrans,ztrans:double;
collision:bool;
begin
//Kollisionscheck
panelu.paintp(collision);
if collision then begin
px:=lastpos.x;
py:=lastpos.y;
pz:=lastpos.z;
end;
//Hintergrundfarbe
if fogok then glClearColor(0.5,0.5,0.5,1)
else glClearColor(0.0,0.0,0.0,1);
//Bildpuffer komplett löschen
glClear(GL_COLOR_BUFFER_BIT OR GL_DEPTH_BUFFER_BIT);
glLoadIdentity;
xtrans:=-px;
ytrans:=-py-walkbias;
ztrans:=-pz;
lookupdown:=360.0-rotx;
sceneroty:=360.0-roty;
scenerotz:=360.0-rotz;
glRotatef(lookupdown,1.0,0,0);
glRotatef(sceneroty,0,1.0,0);
glRotatef(scenerotz,0,0,1.0);
glTranslatef(xtrans,ytrans,ztrans);
if _infocap then begin
caption:=
ftos(px)+'/'+ftos(py)+'/'+ftos(pz)+
' | '+
ftos(rotx)+'/'+ftos(roty)+'/'+ftos(rotz)+
' | '+
'Objekte: '+inttostr(map.anz);
end;
//room
x:=0;y:=0;z:=0;
w:=map.w/2;h:=hauptf.map.h;d:=map.d/2;
//Box-Modus: 0 nicht, 1=nur Boden, 2=nur Decke, 3=alles
if(boxmode=3)or(boxmode=2)then begin
//decke
coltex(map.txdecke);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(0,map.txdeckedc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(map.txdeckewc,map.txdeckedc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(map.txdeckewc,0);glVertex3d(x+w,y+h,z+0);
glEnd();
end;
if(boxmode=3)or(boxmode=1)then begin
//boden
h:=0;
coltex(map.txboden);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(0,map.txbodendc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(map.txbodenwc,map.txbodendc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(map.txbodenwc,0);glVertex3d(x+w,y+h,z+0);
glEnd();
end;
if map.name='Flur' then begin
//Zwischendecke
coltex(map.txdeckez);
//Längsbalken
dd:=1;
for z:=0 to 2 do begin
dd:=dd+2;
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,5.2,dd+0);
glTexCoord2f(0,1);glVertex3d(x+0,5.2,dd+0.5);
glTexCoord2f(w,1);glVertex3d(x+w,5.2,dd+0.5);
glTexCoord2f(w,0);glVertex3d(x+w,5.2,dd+0);
glEnd();
end;
//Querbalken
dd:=0;
for x:=0 to ((trunc(w)-1)div 5)-1 do begin
dd:=dd+5;
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(dd,5.3,0);
glTexCoord2f(0,1);glVertex3d(dd,5.3,d);
glTexCoord2f(1,1);glVertex3d(dd+0.5,5.3,d);
glTexCoord2f(1,0);glVertex3d(dd+0.5,5.3,0);
glEnd();
end;
end;
for r:=0 to map.anz-1 do begin
drawquad(map.quads[r]);
end;
SwapBuffers(DC);
end;
Die Funktion "DrawScene" ist im Prinzip dieser ominöse Grafik-Renderer, von dem
immer wieder die Rede war. Hier endlich also werden die zuvor gefüllten TQuads der
TMap-Struktur zu OGL-Objekten gewandelt und auf unserer Hauptform als OGL-Grafik
ausgegeben.
Zunächst wird geprüft, ob unsere aktuelle Position im Modell eine Kollision verursacht,
d.h., ob wir in eine Wand oder einen Schrank oder ähnliches gerannt sind.
Sollte dies der Fall sein, bleiben wir einfach auf der letzten zuvor möglichen Stelle
("lastpos") stehen.
Dann bekommt das Ausgabefenster eine Hintergrundfarbe verpasst. Die ändert
sich, je nachdem, ob der Nebelmodus ("fogok") aktiv ist oder nicht.
Die OGL-Befehle "glClear" und "glLoadIdentity" löschen die OGL-Puffer und
bringen uns im Modell an die Ursprungsposition.
Nun werden eine paar Variablen berechnet, die die Position und Sehrichtung
im Modell wiedergeben, die unser Avatar aktuell innehat. Diese Werte können
sich seit der letzten "DrawScene"-Aktion natürlich geändert haben.
Wie bereits erwähnt, bewegen nicht wir uns im Modell, sondern das Modell bewegt
sich um uns. Durch das vorherige "glLoadIdentity" befinden wir uns zur Zeit am
Ursprungsort. Um nun das Modell an die richtige, eben berechnete Stelle zu rücken
und zu drehen, verwenden wir die OGL-Befehle "glRotatef" und "glTranslatef".
"glRotatef" rotiert das Modell gemäss unserer Blickrichtung bzw. in genau
umgekehrter Weise, wie weiter oben beschrieben. Schauen wir 20 Grad nach unten,
kippt das Modell 20 Grad nach oben, blicken wir 180 Grad "aus dem Modell heraus",
wird das Modell um 180 Grad um die y-Achse gedreht, sodass wir wieder scheinbar
in das Modell hineinblicken.
Nach der Rotation sorgt "glTranslatef" dafür, dass sich das komplette Modell an
den Achsen entlang verschiebt, so das wir scheinbar an der zuvor berechneten Position
zu stehen kommen. Tatsächlich bleiben wir aber stets am Ursprungsort fixiert.
Ich erinnere mich noch, dass ich Anfangs die Befehle "glRotatef" und "glTranslatef"
in umgekehrter Reihenfolge ausgeführt habe. Ich wechselte also erst die Position,
dann begann ich die Rotation. Ich dachte, dass käme auf das Gleiche heraus. Dem ist
aber nicht so.
Wenn wir am Ursprungsort stehen und das Modell rotieren lassen, bildet unsere Position
den Schnittpunkt der x-, y- und z-Achse. Egal, wie wüst wir auch die Sehrichtung verändern,
wir bleiben doch stets im unteren, nein, besser: vorderen linken Ecke des Modells stehen.
Bewegen wir uns aber mittels "glTranslatef" vom Ursprungsort weg und rotiere
anschliessend das Modell, dann liegen die Drehachsen ein Stück entfernt von uns.
Das hat zur Folge, dass uns durch die Rotation quasi der Boden unter den Füssen
weggezogen wird und wir so erneut den Standort im Modell wechseln - der dann nichts
mehr mit dem zuvor berechneten zu tun hat!
Holla, die Waldfee! Das hat mich ein paar Stunden gekostet, bis ich 's endlich
geblickt hatte :-)
Weiter im Source: Wir haben unser Model um uns zurechtgerückt, nun kann es perspektivisch
korrekt gemalt werden. Da wir uns in einem Zimmer-Raum befinden und Zimmer üblicherweise
einen Boden und eine Decke haben, beginnen wir mit diesen.
Wie bereits erwähnt werden Decke und Boden unserer Räume anders als Objekte behandelt,
da wir diese im 2D-Textmap-Modell ja nicht eintragen können; Boden und Decke nehmen
schliesslich in der 2D-Ansicht den gleichen Raum ein. Und da sie überall vorhanden sind,
wäre auch kein Platz mehr gewesen, um sonstige Objekte eintragen zu können.
Wir berechnen daher einfach die Ausmasse von Decke und Boden. Das ist aber trivial.
Zunächst zur Decke: In der Map-Struktur haben wir die Breite "map.w" und Tiefe "map.d" des
aktuellen Raumes vermerkt - der Boden ist natürlich entsprechend breit und tief. Die y-Position,
an der die Decke "im Raum schwebt", ergibt sich logischerweise aus der Höhe des Raums "map.h".
Sieht man sich obigen Source an, stellt man vielleicht überrascht fest, dass als Breite
und Tiefe für die Decke nur jeweils die Hälfte der Map-Werte angegeben ist, die Höhe aber
1:1 übernommen wurde.
Das hat mit Faulheit zu tun. Meine ursprünglichen Text-Maps waren nämlich so ausgelegt, dass
jedes Zeichen darin einen Quader mit einem Meter Breite und einem Meter Tiefe beschrieb.
Das schuf aber schnell Platzprobleme; die Maps wurden zu grob, um alle gewünschten Objekte
darin sauber platzieren zu können. Ausserdem waren Wände mit einem Meter Dicke auch sehr weit
von der Realität bei Henry's entfernt. Ich arbeite ja nicht in Fort Knox :-)
Ich änderte also kurzerhand den Massstab: Jedes Zeichen in der Textmap bedeutete ab da nur
noch 50 cm Breite und 50 cm Tiefe. Weil die Höhe in den Text-Maps keine Rolle spielt, blieb
deren Massstab davon unberührt.
Aber natürlich mussten alle Text-Maps entsprechend aufgebohrt werden: Um einen Meter
Breite zu versinnbildlichen waren jetzt ja zwei statt einem Zeichen nötig. Die ganzen
Maps mussten also quasi Zeichen für Zeichen verdoppelt werden. War eine elende Schufterei,
denn zu dem Zeitpunkt waren meine Text-Maps mit den 1 x 1 m Ausmassen eigentlich schon
fertig gewesen.
Nun ja, nicht nur die Text-Maps waren fertig, auch der Grafik-Renderer arbeitete bereits.
Und da ich bisher mit den "natürlichen" Meter-Werten rechnete, und nun plötzlich 20 Zeichen
Breite in der Textmap nur noch 10 Meter Breite im realen Modell entsprechen, halbierte ich
einfach die Map-Werte bei der Übergabe an den Grafik-Renderer.
Okay, wir wissen die Dimension der Decke und wo sie im Modell "anzuheften" ist.
Aber einfach nur eine farblose Decke ist etwas öde. Daher bekommt die Decke wie
alle anderen Objekte im Modell eine Textur verpasst.
Die Textur der Decke haben wir zuvor in der "setmap"-Prozedur geladen. Ihre ID steht in
der Map-Variablen "map.txdecke". Mittels der "coltex"-Funktion machen wir nun die
Decken-Textur zur aktuellen OGL-Textur. Alle nachfolgenden Textur-Befehle,
eingekapselt in "glBegin" und "glEnd", beziehen sich dann automatisch auf sie.
Über die OGL-Befehle "glTexCoord2f" und "glVertex3d" wird die aktuelle Textur an ein
Objekt "geklebt", optional in gekachelter Weise. Das kann man ungefähr so lesen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
Nimm die Textur-Ecke links unten: glTexCoord2f(0,0);
Platziere dieses Eck links vorne im Modell: glVertex3d(0,h,0);
Nimm die Textur-Ecke links oben: glTexCoord2f(0,map.txdeckedc);
Platziere dieses Eck links hinten im Modell: glVertex3d(0,h,d);
Nimm die Textur-Ecke rechts oben: glTexCoord2f(map.txdeckewc,map.txdeckedc);
Platziere dieses Eck rechts hinten im Modell: glVertex3d(w,h,d);
Nimm die Textur-Ecke rechts unten: glTexCoord2f(map.txdeckewc,0);
Platziere dieses Eck rechts vorne im Modell: glVertex3d(w,h,0);
Der Boden unserer Welt wird in gleicher Weise realisiert. Wichtigster Unterschied
ist, dass eine andere Textur verwendet wird ("map.txboden"), und das bei der
Textur-Kleberei die Höhe "h" auf null gesetzt wird.
Eine Besonderheit bei HENRY's ist der "Flur" zwischen "Info" und "Mode". Er ist mit 7 Metern
sehr hoch und besitzt eine Art Zwischendecke. Dem wollte ich auch im Modell Rechnung tragen.
Erkennt das Programm am Map-Namen "map.name", dass wir uns im Flur befinden, wird nun auch
noch eine Zwischendecke generiert, die sich in etwa 5 Metern Höhe befindet.
Diese Zwischendecke ist eine Art Gitterstruktur, sodass man problemlos durchschauen kann,
und darüber die "echte" Decke sieht. Sie baut sich aus einer Reihe von Quer- und Längsbalken
auf, die im Prinzip genauso wie die vollständige Decke erzeugt wird, allerdings über eine
Schleife realisiert, wobei jeweils neue Koordinaten für die Textur-Geschichte berechnet
werden.
Jetzt endlich werden unsere Personen und Objekte ins OGL-Modell integriert. Eine einzige
kleine Schleife erledigt den Job, bei der alle Map-Objekte "maps.quads" an die Prozedur
"drawquad" übergeben werden.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
procedure Thauptf.drawquad(q:tquad);
var
x,y,z,w,h,d:single;
begin
x:=q.pos.x;y:=q.pos.y;z:=q.pos.z;
w:=q.dim.w;h:=q.dim.h;d:=q.dim.d;
x:=x/2;y:=y;z:=z/2;
w:=w/2;h:=h;d:=d/2;
if h<Hauptf.map.h then begin
//oben
coltex(q.tex.o);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+w,y+h,z+0);
glEnd();
end;
//links
coltex(q.tex.l);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+0,z+0);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+0,y+0,z+d);
glEnd();
//rechts
coltex(q.tex.r);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+w,y+0,z+0);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+w,y+h,z+0);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+w,y+0,z+d);
glEnd();
//vorne
coltex(q.tex.v);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+0,z+d);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+w,y+0,z+d);
glEnd();
//hinten
coltex(q.tex.h);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+0,z+0);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+w,y+h,z+0);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+w,y+0,z+0);
glEnd();
end;
Die "Fertigung" der Quads unterscheidet nicht mehr zwischen Personen
und Objekten; sie werden auf die genau gleiche Weise generiert.
Das Verfahren ähnelt auch sehr dem, was wir schon kennen, um Boden
und Decke zu erzeugen. Anders als Boden und Decke besitzen unsere
Quader aber mehrere Seiten, die "bemalt" werden müssen, sprich, mit
Texturen versehen.
Höhe, Breite, Tiefe sowie die Koordinaten sind alle in der übergebenen
Quads-Struktur vermerkt. Breite, Tiefe, x- und y-Position müssen wieder
aus den oben genannten Gründen halbiert werden. Höhe und y-Position
bleiben dagegen unverändert.
Jetzt können wir Seite für Seite die Quader mit Texturen versehen.
Ist ein Objekt genauso hoch wie die Decke - das gilt z.B. für
eine Wand - dann ersparen wir es uns, die "Kopf"-Textur anzukleben.
Ebenso bemalen wir die Unterseite der Objekte nicht, da diese i.d.R.
auf dem Boden aufliegen, also ohnehin nie zu sehen sind. Das bringt
etwas Speed-Gewinn.
Bei den "glTexCoord2f"-Kommandos habe ich's mir offensichtlich einfach gemacht:
Egal, welche Seite gerade bearbeitet wird, stets übergebe ich die gleichen
Parameter.
Nur bei den "glVertex3d"-Parametern muss man logisch vorgehen, damit an die
richtigen Stellen gemalt wird. Basierend auf die für ein Objekt fixen "x"-,
"y"- und "z"-Werten wird nur jeweils die Breite "w", die Höhe "h" und/oder
die Tiefe "d" dazu addiert, damit's passt. Keine grosse Sache also.
Gehen wir zurück in die "DrawScene"-Funktion. Hier fehlt uns nach
Abschluss des ... mh ... Renderings ... nur noch ein Kommando: SwapBuffers(DC)
Dadurch wird die eben in einem Puffer generierte OGL-Szenerie mit einem Schlag
auf unseren ganz am Anfang definierten Device Context "DC" kopiert, sprich, im
Hauptfenster zu Anzeige gebracht.
That's it! Unsere Welt existiert!
Startposition im Raum 'Info' vom Auktionshaus HENRY's: Wir befinden uns etwa in der Mitte des Raumes hinter einen Schreibtisch und blicken schräg links in das Modell hinein. Im Hintergrund befinden sich weitere Schreibtische. Und die stark stilisierten Avatare dreier Mitarbeiterinnen.
Wir können das Hauptfenster beliebig vergrössern und verkleinern, die Grafik passt
sich voll-automatisch an. Ist eine prima Sache. Das haben wir durch Abfangen des
OnResize-Ereignis der Form erreicht.
Aber egal, wie sehr wir nun auch am Fenster rütteln und schütteln mögen, wir erhalten
stets nur den gleichen Einblick in unsere Welt, 20 Grad nach links in die "Info" hinein.
Das ist öde. Da muss mehr Action rein!
Bewegung wird in OGL_HENRYs über verschiedene Tastaturcodes realisiert. Dazu basteln
wir uns eine Funktion zum OnKeyDown-Ereignis der Form:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
procedure Thauptf.FormKeyDown(
Sender: TObject; var Key: Word;Shift: TShiftState
);
begin
lastpos.x:=px;
lastpos.y:=py;
lastpos.z:=pz;
if ssctrl in shift then begin
if key=vk_up then rotx:=rotx-_rstep
else if key=vk_down then rotx:=rotx+_rstep;
if key=vk_left then rotz:=rotz-_rstep
else if key=vk_right then rotz:=rotz+_rstep;
exit;
end;
if key=vk_right then roty:=roty-_rstep
else if key=vk_left then roty:=roty+_rstep
else if key=vk_prior then py:=py+_pstep/2
else if key=vk_next then py:=py-_pstep/2
else if key=vk_up then begin
px:=px-sin(roty*_piover180)*_pstep;
pz:=pz-cos(roty*_piover180)*_pstep;
if walkbiasangle>=359.0 then walkbiasangle:=0.0
else walkbiasangle:=walkbiasangle+90;
walkbias:=sin(walkbiasangle*_piover180)/20.0;
end
else if key=vk_down then begin
px:=px+sin(roty*_piover180)*_pstep;
pz:=pz+cos(roty*_piover180)*_pstep;
if walkbiasangle<=1.0 then walkbiasangle:=359.0
else walkbiasangle:=walkbiasangle-90;
walkbias:=sin(walkbiasangle*_piover180)/20.0;
end
else if key=ord('L') then begin
if lightok then begin
gldisable(GL_LIGHT0);
gldisable(GL_LIGHTING);
lightok:=false;
end
else begin
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
lightok:=true;
end;
end
else if key=ord('B') then begin
inc(boxmode);if boxmode>3 then boxmode:=0;
end
else if key=vk_space then sethome
else if key=ord('H') then helpm.visible:=not helpm.visible
else if key=ord('P') then p.visible:=not p.visible
else if key=ord('N') then begin
if fogok then begin
gldisable(GL_FOG);
fogok:=false;
end
else begin
glEnable(GL_FOG);
fogok:=true;
end;
end
else if key=ord('1') then mapu.setmap_info
else if key=ord('2') then mapu.setmap_flur
else if key=ord('3') then mapu.setmap_mode
else if key=ord('4') then mapu.setmap_edv
else if key=vk_escape then close;
end;
Unsere Position im Modell wird durch die Form-Variablen "px", "py"
und "pz" definiert. Die Blickrichtung steckt in den Form-Variablen
"rotx", "roty" und "rotz".
Bevor wir eine Bewegung ausführen, retten wir erst einmal unsere aktuelle
Position in "lastpos". Denn es könnte ja sein, dass uns unser nächster "Schritt"
nach vorne direkt in eine Wand führt. Da dies aber in der Realität schlechterdings
möglich ist, muss uns das Programm anschliessend wieder auf die vorherige Position
zurücksetzen.
Die Koordinaten-Änderungen erfolgen über die Cursor-Tasten. "Cursor hoch" führt uns
tiefer ins Modell rein, "Cursor runter" weiter raus.
Beim Vorwärtsgehen und Rückwärtsgehen habe ich mir eine Idee geklaut, die ich
bei jemanden anderem im Source gefunden habe: Um das Auf und Ab beim Gehen
zu simulieren, ändert sich durch die Bewegung die Sichthöhe "py" um einen
Kosinus-Sinus-Wert "walkbias". Die Mathematik dahinter kapiere ich zwar nicht
ganz, aber es funzt ganz nett.
Mit den Cursor-Tasten "links" und "rechts" können wir uns um die y-Achse drehen. Drücken
wir gleichzeitig die "STRG"-Taste, dann können wir die Blickrichtung neigen und senken
bzw. uns in Schieflage begeben.
Ansicht des Raumes 'Flur' in gekippter Perspektive: Einige Meter vor uns ist die Treppe, die zum Raum 'Mode' führt. Auf der rechten Seite ist der Avatar des Wachmannes zu erkennen. Und noch weiter rechts befindet sich der Ausgang von HENRY's Auktionshaus.
Ach ja, die "Gewichtigkeit", mit der ein Tastendruck eine Koordinaten- und/oder
Rotations-Variable ändert, wird in Konstanten festgeschrieben. So bewirkt etwa ein
"Cursor hoch"-Tastaturdruck eine Bewegung in den Raum um "_pstep" Meter. In meinem Modell
hat "_pstep" den konstanten Wert "1", was ja auch in etwa der Länge eines Schrittes
entspricht.
Die "B"-Taste schaltet in abwechselnder Reihenfolge die Anzeige von Boden und Decke ab.
Das erlaubt ungewohnte Einsichten von unten und oben in das Modell hinein.
Ansicht des Raumes 'Mode', hier ohne Boden-Texturen: Per Optionen wurden die Boden-Texturen entfernt. Unter unseren Füssen befindet sich daher nur noch ein grosses schwarzes Nichts.
Um nach oben und unten "schweben" zu können, kann man die "Bild auf"- und Bild ab"-Tasten
verwenden. Das ermöglicht schöne Rundflüge über das komplette Modell. So muss sich
Supermann fühlen, wenn es wieder heisst "Ist es ein Vogel, ist es ein Flugzeug ...?"
Flug über den Raum 'Info': Wir haben die Decken-Texturen entfernt und uns nach oben bewegt. Nun erhalten wir einen Überblock über die gesamte Architektur des Raumes unter uns. Im vorderen Teil etwa ist mein Büro mit dem Grossrechner und nicht weniger als vier PCs zu erkennen.
Mit "Space" springt man an die Home-Position des Raums zurück, in dem man sich
gerade befindet.
Mit den Zahlen "1" bis "4" kann man schnell von einem Raum zum anderen springen:
(1) Raum 'Info': Das technisch reichhaltig ausgestattete Büro des ollen Schwamms.
(2) Raum 'Flur': Hier kommt sonst kein Kunde hin.
(3) Raum 'Mode': Was für ein Spanner macht denn da auf den Toiletten mit der Kamera herum?
(4) Raum 'EDV': Chaos für die Chaoten.
Mittels "L"-Taste kann ein anderes Beleuchtungsmodell gewählt werden.
Raum 'Flur' ohne Licht: Im Dunkeln lässt sich gut Munkeln. Der Wachmann vorne rechts sorgt aber (leider) dafür, das es stets gesittet zugeht.
"N" schaltet Nebel an und aus. Vielleicht nützlich für Feuerwehrübungssimulationen :-)
Raum 'Flur' im Nebel: Die Sicht reicht nun nur noch einige Meter weit. Hier könnte John Carpenter jetzt locker den zweiten Teil von 'The Fog' drehen.
Die "H"-Taste dient dazu, einen Hilfeschirm anzuzeigen oder abzustellen.
Sichtbar gemachter Hilfeschirm: A little help for my friends.
Und zu guter Letzt aktiviert bzw. deaktiviert "P" die Anzeige der Minimap. Ist die
Minimap zu sehen, findet übrigens ein Kollisionscheck statt, ist sie nicht zu sehen,
können wir wie die Geister durch Wände gehen.
Aktivierte Minimap: Die Welt in 2D mit Kollisionsgarantie (oben links).
Unser Modell liefert uns bereits eine schöne kleine Welt, in der wir frei umher
wandern können. Grenzenlos frei. Rein gar nichts hält uns auf. Bisher! Denn das wollen
wir ändern; geht im wahren Leben ja auch nicht.
So weit ich das überblickt habe, scheint es in OGL durchaus die Möglichkeit zu geben,
festzustellen, ob sich zwei Körper "berühren", ob also eine Kollision zwischen ihnen
stattgefunden hat. Das könnte man sicher irgendwie nutzen, um zu verhindern, dass
unser Avatar durch Wände oder sonstige Objekte gehen kann.
Die Geschichte habe ich aber nie richtig kapiert. Und auch zu wenig Informationen
darüber gefunden. So blieb ich lieber auf vertrautem Terrain, und programmierte mir
eine einfache Kollisionskontrolle selbst. Benutzt habe ich dazu die 2D-Minimap, um
die wir uns jetzt kümmern wollen.
Wir erinnern uns: In der "mapu"-Unit-Funktion "rdmap" haben wir das 2D-Textmap-Modell
eingelesen, alle Objekte darin ausgeparst und die zugehörigen TQuads generiert. Ganz
am Schluss wurde noch "panelu.initp" aufgerufen, bevor das Idle-Ereignis eintrat und
der Grafik-Renderer los legen konnte.
"panelu.initp" sorgt dafür, dass die eben generierten TQuads gleich wieder in ein
2D-Modell zurück konvertiert werden - nämlich in eine "Minimap", die man im Programm
aktivieren kann und die dann oben links - über der OGL-Grafik liegend - gemalt wird.
Die Minimap zeigt uns den Raum, in dem wir uns gerade befinden, senkrecht von oben,
als vereinfachtes 2D-Modell. Unsere eigene Position im Modell wird darin durch einen
blinkenden Punkt hervorgehoben.
Alle Funktionen zur Minimap und zur Kollisionskontrolle sind in der Unit "panelu"
gekapselt. "panelu" deshalb, weil die Minimap letztlich auf ein TPanel gemalt wird.
Okay, der Name ist nicht sehr clever gewählt, "minimapu" oder "collchku" wäre sicher
aussagekräftiger gewesen.
Die Initialisierung der Minimap erfolgt - wie eben gesehen - durch Aufruf von
"panelu.initp":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
const
_dw=1;
_dd=1;
_pcolor=clwhite;
type
tp=class(tpanel)
end;
procedure initp;
var
cv:tcanvas;
rr,x,z,r,c,d,w:integer;
s:string;
collision:bool;
ok:bool;
begin
w:=trunc(hauptf.map.w*_dw)+10;
d:=trunc(hauptf.map.d*_dd)+10;
hauptf.p.clientWidth:=w;
hauptf.p.clientheight:=d;
//tp(hauptf.p).canvas.brush.Color:=clblack;
hauptf.pbmp.Width:=w;
hauptf.pbmp.height:=d;
//cv:=tp(hauptf.p).canvas;
cv:=hauptf.pbmp.canvas;
cv.Brush.color:=_pcolor;
cv.Rectangle(0,0,w,d);
for r:=0 to hauptf.mapm.lines.count-1 do begin
z:=r*_dd+5;
s:=hauptf.mapm.Lines[r];
for c:=1 to length(s) do begin
if s[c]=' ' then continue;
if s[c] in ['0'..'9'] then continue;
// durch bestimmte Zeichen kann durchgegangen
// werden, z.B. Vorhang
ok:=false;
for rr:=0 to high(hauptf.map.durchgang)-1 do begin
if s[c]= hauptf.map.durchgang[rr] then begin
ok:=true;
break;
end;
end;
if ok then continue;
x:=(c-1)*_dw+5;
cv.Pixels[x,z]:=clblack;
end;
end;
hauptf.ppbmp.width:=w;
hauptf.ppbmp.height:=d;
paintp(collision);
end;
Wie man sieht, holen wir uns die Breite und Tiefe der aktuellen Map, multiplizieren
sie mit dem Konstanten "_dw" und "_dd", addieren noch jeweils 10 Pixel dazu, und
verpassen dann dem TPanel "hauptf.p" die entsprechenden Ausmasse.
Da "_dw" den Wert "1" hat, gilt: Ist der aktuelle Raum 100 Meter breit, hat die
Variable "hauptf.map.w" den Wert "200" (200 * 50 cm = 100 m), und das heisst, die Minimap
wird 210 Pixel breit. Die 10 überschüssigen Pixel werden benötigt, um um das 2D-Modell
noch etwas Rand zu lassen.
Entsprechendes gilt für die Tiefe der Map, die im 2D-Modell zur Höhe des Panels wird.
Wir holen uns den Canvas einer zuvor in der Hauptform initialisierten Bitmap
"hauptf.pbmp.canvas" und färben ihn einheitlich ein.
Anschliessend durchlaufen wir die Textmap zeilenweise und zeichenweise. Ähnlich wie bei
der TQuad-Generierung suchen wir nach Objekt-Zeichen. Anders als dort werden aber
Personen-Zeichen ignoriert; Personen wollen wir in der Minimap nicht anzeigen (was
auch zur Folge hat, dass wir später problemlos durch Personen durchlaufen können;
wen das stört, der kann hier ansetzen, um's zu ändern).
Mir fällt dabei gerade auf, dass ich weiter oben gelogen habe: Wir verwenden hier
gar nicht die generierten Quads des 3D-Modells, um daraus ein 2D-Modell zu extrahieren,
sondern wir setzten die 2D-Textmap direkt in eine 2D-Pixel-Map um. Ist ja auch viel
einfacher zu realisieren. Die paar Pixel werden so schnell gemalt, dass eine
Objekt-Gruppierung wahrlich nicht nötig ist. Hatte ich nur vergessen.
Wenn wir also in der Textmap ein Objekt-Zeichen finden, wird dessen Position in Pixel
umgerechnet und an die entsprechende Stelle im "hauptf.pbmp.canvas" ein schwarzer
Punkt gemalt. Zuvor wird aber noch geprüft, ob es sich bei dem gefundenen Objekt-Zeichen
nicht um einen Durchgang handelt. Durchgänge werden in der Minimap nämlich auch nicht
angezeigt, was ebensolche Folgen wie bei Personen hat, diesmal aber sogar beabsichtigt: Wir
können später durch ihre 3D-Repräsentanten hindurchgehen.
Am Ende der Schleife enthält die Bitmap "hauptf.pbmp" ein Abbild der Textmap, bei der
nur die "festen" Körper in Form schwarzer Punkte eingezeichnet sind. Bis zum Wechsel
in einen anderen Raum bleibt diese Bitmap nun unverändert.
Um unsere eigene Person in der Minimap darstellen zu können, die sich ja permanent
ändern kann, benötigen wir eine zweite Bitmap "hauptf.ppbmp". Diese wird nun noch
grössenmässig an die "hauptf.pbmp" angepasst.
Zuletzt wird "paintp" aufgerufen.
In der Bitmap "hauptf.pbmp" ist der sich nicht ändernde Teil der Minimap gespeichert.
Nun gilt es, eine zweite Bitmap "hauptf.ppbmp" zu füllen, die darüber hinaus auch unsere
aktuelle Position im Modell anzeigt.
Umgerechnet auf die Pixeldimension der Minimap kann nun geprüft werden, ob sich
an unserer aktuellen Position bereits ein schwarzer Punkt, sprich, ein festes Objekt,
befindet. Ist dies der Fall, wird der Var-Parameter "collision" auf "true" gesetzt,
ansonsten auf "false".
Im "false"-Fall wird ein roter Punkt gesetzt, der unsere aktuelle Position im Modell
wiedergibt. Oder ein gelber Punkt, je nachdem, welchen Wert die Globale "hauptf.ccolor"
trägt. Dadurch blinkt unsere Position in der Minimap abwechselnd rot und gelb, wodurch
sie leichter zu erkennen ist.
Darüber hinaus ist auch noch zu prüfen, ob wir auf einem der Map-Ausgänge gelandet
sind. Haben wir nämlich einen solchen Ausgang erreicht, bedeutet das für das Programm,
dass wir in den nächsten Raum "transferiert" werden müssen.
Zu guter Letzt wird die adaptive Bitmap "hauptf.ppbmp" mit unserer Cursor-Position auf
den Canvas des TPanel "hauptf.p" kopiert, wodurch die Minimap angezeigt wird.
All das erledigt die Prozedur "paintp":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
procedure paintp(var collision:bool);
var
r,x,z,w,d:integer;
s:string;
begin
collision:=false;
if not hauptf.p.Visible then exit;
//Dimensionen
w:=hauptf.pbmp.Width;
d:=hauptf.pbmp.height;
//Map-Background
bitblt(
hauptf.ppbmp.canvas.Handle,0,0,w,d,
hauptf.pbmp.canvas.handle,0,0,
srccopy
);
//aktuelle Position
x:=trunc(hauptf.px)*2*_dw+5;
z:=trunc(hauptf.pz)*2*_dd+5;
if(x>1)and(x<w-2)and(z>1)and(z<d-2)then begin
//Kollision?
collision:=(tcolor(hauptf.ppbmp.canvas.pixels[x,z])<>_pcolor);
if not collision then
collision:=(tcolor(hauptf.ppbmp.canvas.pixels[x,z+1])<>_pcolor);
if not collision then
collision:=(tcolor(hauptf.ppbmp.canvas.pixels[x+1,z])<>_pcolor);
if not collision then
collision:=(tcolor(hauptf.ppbmp.canvas.pixels[x+1,z+1])<>_pcolor);
if not collision then begin
hauptf.ppbmp.canvas.pixels[x,z]:=hauptf.ccolor;
end
else begin
//Ausgang erwischt?
x:=trunc(hauptf.px)*2;
z:=trunc(hauptf.pz)*2;
if
(x>-1)and(x<trunc(hauptf.map.w))and
(z>-1)and(z<trunc(hauptf.map.d))
then begin
s:=hauptf.mapm.Lines[z];
s:=s[x+1];
for r:=0 to high(hauptf.map.outa)-1 do begin
if s=hauptf.map.outa[r].ch then begin
collision:=false;
chgmap(hauptf.map.outa[r]);
exit;
end;
end;
end;
end;
end;
if hauptf.ccolor=clred then hauptf.ccolor:=clyellow
else hauptf.ccolor:=clred;
try
bitblt(
tp(hauptf.p).canvas.Handle,0,0,w,d,
hauptf.ppbmp.canvas.handle,0,0,
srccopy
);
except
end;
end;
Wir prüfen hier zunächst, ob die Minimap überhaupt angezeigt werden soll.
Ist dies nicht der Fall, dann springen wir direkt aus der Prozedur raus,
wodurch der Var-Parameter "collision" stets "false" bleibt. Was wiederum
zur Folge hat, dass wir im Modell von festen Objekten generell nicht behindert
werden und sie problemlos durchqueren können.
Ansonsten kopieren wir die fixe Bitmap "hauptf.pbmp" auf die adaptive Bitmap
"hauptf.ppbmp".
Anhand unserer Modell-Positions-Variablen "hauptf.px" und "hauptf.pz" bestimmen
wir die Pixel-Position in der Minimap, an der wir uns gerade befinden. Wir prüfen
dann die Farbe der Pixel "hauptf.ppbmp" rund um uns herum. Taucht dort ein Pixel
mit schwarzer Farbe auf, dann haben wir uns zu dicht an einen "festen Körper"
herangewagt; es wird das Kollisionsflag "collision" gesetzt.
Liegt keine Kollision vor, wird unsere Position in der Bitmap markiert.
Liegt eine Kollision vor, bleibt zu prüfen, ob diese Kollision durch einen
Ausgang verursacht wurde. Dazu klopfen wir das "hauptf.map.outa"-Array ab.
Sollten wir tatsächlich einen Ausgang getroffen haben, wird die Funktion
"chgmap" aufgerufen und die Prozedur verlassen. Ansonsten wird fortgefahren und
die Minimap zur Anzeige gebracht.
Ach ja, letztlich wird der Wert von "collision" übrigens im Grafik-Renderer
geprüft, denn der wird ja permanent aufgerufen, eine Kollision kann also nicht
"verloren" gehen. Wir wir dort gesehen haben, bewirkt eine Kollision, dass wir
nur einfach an unsere vorherige Position "lastpos" zurück versetzt werden.
Einige Kollisionen mit festen Körpern bedeuten, dass wir einen Ausgang erreicht
haben. In der Prozedur "paintp" wurde auch schon festgestellt, um welchen Ausgang
der aktuellen Map es sich genau handelt. In den "setmap"-Prozeduren wurden zudem
bereits die Eigenschaften der einzelnen Ausgänge definiert. Dies kommt uns nun zu
gute, wenn die Funktion "chgmap" aufgerufen wird:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
procedure chgmap(o:tout);
var
r,c:integer;
s,mapstart:string;
rx,ry,rz:single;
begin
//Positionswerte merken
mapstart:=o.mapstart;
rx:=hauptf.rotx;
ry:=hauptf.roty;
rz:=hauptf.rotz;
//Map laden
if o.map='Info' then setmap_info
else if o.map='Flur' then setmap_flur
else if o.map='Mode' then setmap_mode
else if o.map='EDV' then setmap_edv;
//finde Startposition
for r:=0 to hauptf.mapm.Lines.count-1 do begin
s:=hauptf.mapm.Lines[r];
c:=pos(mapstart,s);
if c>0 then begin
hauptf.px:=(c+1)/2;
hauptf.pz:=r/2;
//Rotation anpassen
hauptf.rotx:=rx;
hauptf.roty:=ry;
hauptf.rotz:=rz;
exit;
end;
end;
end;
In "o" bekommt die Funktion den "getroffenen" Ausgang übergeben.
In dessen Struktur-Attribut "o.map" ist vermerkt, wo die Reise
hinzugehen hat.
Treffen wir z.B. auf den Nord-Ausgang des "Flur", dann steht hier,
dass der nächste Raum die "Mode" zu sein hat. Treffen wir dagegen
auf den Süd-Ausgang, dann muss die "Info" folgen.
Die entsprechende "setmap"-Prozedur wird also aufgerufen. Nun gilt es noch,
im neuen Raum auch an der richtigen Stelle "herauszukommen".
Die Information steckt im Attribut "o.mapstart", und zwar in Form einer
zweistelligen Zahl, die mit "9" beginnt. Folgendes Beispiel soll dies kurz
illustrieren:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
Ausgang 'D' führt in 'RAUM II' an Position '92'
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W RAUM I W
W W
W 90 W
WWWWWWWWWWWWWWWWWDDDWWWWWWWWWWWWWWWWWWW
--------------------------------------------
Ausgang 'd' führt in 'RAUM I' an Position '90'
Ausgang 'E' führt in 'RAUM III' an Position '90'
Ausgang 'D' führt in 'RAUM III' an Position '91'
WWWWWWWWWWWWWWWWWdddWWWWWWWWWWWWWWWWWWW
W 92 W
W RAUM II W W
W W W
W 90 W 91 W
WWWWWEEEWWWWWWWWWDDDWWWWWWWWWWWWWWWWWWW
--------------------------------------------
Ausgang 'E' führt in 'RAUM II' an Position '90'
Ausgang 'd' führt in 'RAUM II' an Position '91'
WWWWWEEEWWWWWWWWWdddWWWWWWWWWWWWWWWWWWW
W 90 W 91 W
W RAUM III W W
W W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Wir suchen also in der Textmap mittels der "pos"-Funktion nach der
Zahlen-Zeichenfolge "o.mapstart" und berechnen daraus dann unsere neuen
"hauptf.px"- und "hauptf.pz"-Werte, die uns im Modell an die richtige
Stelle bringen.
Zuletzt wird noch die Rotation unseres Avatars angepasst (denn die wurde
durch die "setmap"-Funktion rückgesetzt) sodass wir den neuen Raum
mit der gleichen "Orientierung" betreten, wie wir den letzten verlassen
haben. Wir könnten ja auf die komische Idee kommen, neue Räume nur noch
rückwärtsgewandt zu betreten - und solche Spleens unterstützen wir doch
gerne :-).
Damit hätten wir's nun wirklich geschafft: Die "OGL_HENRYs"-Welt ist fertig!
So ganz fertig war die OGL-Welt dann natürlich doch noch nicht. Denn einen
wesentlichen Aspekt habe ich noch gar nicht genannt, der aber entscheidend
zur Optik beitrug: Die Anfertigung der Texturen.
Das hat Spass gemacht. Mit einer Digital-Kamera bewaffnet bin ich durch's
ganze Haus gelaufen und habe an den unmöglichsten Stellen Fotos gemacht.
Wie zum Beispiel in den Damen-Toiletten :-)
Als Vollblut-Programmierer, der ich bin, verlasse ich ja nur relativ selten
meinen "Bunker". Was Sonnenlicht und frische Luft ist, lass ich mir von
kleinen Kindern erzählen. Und so erntete ich doch einige erstaunte Blicke
von Kollegen und Kunden, als sie mich plötzlich überall auf Händen und Knien
durch's Gebäude robben sahen.
Einige dachten sogar, ich sei Reporter, der einen Bericht über HENRY's
verfassen würde. Andere wollten unbedingt mit auf's Bild. Wiederum andere
begegneten mir eher misstrauisch und entfernten noch rasch den letzten
Steuerbescheid von ihrem Schreibtisch, bevor ich dessen Oberfläche knipste,
um daraus eine Textur zu machen.
Wie gesagt, es war ein grosser Spass :-)
|
Demo-Movie #1: Im Flur von HENRY's Auktionshaus |
|
Demo-Movie #2: Flug über HENRY's Auktionshaus |
|
Demo-Movie #3: Auf dem Weg in Daniels Büro |
"OGL_HENRYs" lässt sich relativ leicht und schnell an eigene Bedürfnisse
anpassen. Man muss sich ja nur die Text-Maps vornehmen und die diversen
Zeichen so platzieren, dass sie den eignen Räumlichkeiten entsprechen. Dann
noch ein paar Fotos gemacht, und diese - passend benamt - in die Textur-Ordner
der Räume schmeissen, schon hat man seine eigene Welt erschaffen.
Gleichzeitig birgt die "Engine" aber auch zahlreiche Mängel. So kann man
z.B. nicht ohne weiteres Objekte übereinander stellen; die Text-Maps erlauben
ja immer nur ein Objekt an einem Ort. Um also etwa PCs auf Schreibtischen
zu platzieren, wie wir es in "OGL_HENRYs" sehen, musste ich tricksen: Ich lasse
die PCs quasi über einem Loch im Schreibtisch schweben, wobei der Boden des PCs
genau dort anfängt, wo der Tisch nebenan aufhört, sodass das Loch unter dem PC
nicht zu sehen ist. Betrachtet man die Szenerie von unten, sieht man jedoch sehr
schnell solche Ungereimtheiten.
Blöd ist ebenso, dass die Anzahl möglicher Objekte in einem Raum durch die
Anzahl der Zeichen des ASCII-Zeichensatzes grundsätzlich beschränkt ist. Ja,
viele ASCII-Zeichen (wie etwa BEEP, RETURN, BACKSPACE usw.) können erst gar
nicht eingesetzt werden. Und übersichtlich oder gar aussagekräftig sind all
die Zeichen aber einer gewissen Menge auch nicht sonderlich.
Schwerer noch vielleicht wiegt das stupide Aussehen der Objekte, die ja letztlich
alle nur auf simplen Quadern basieren, auch wenn dies durch die Texturen schon
recht deutlich verdeckt wird. Fussballspielen kann man bei "OGL_HENRYs" jedenfalls
bestenfalls mit einem rechteckigen Ball.
Personen sehen sogar völlig lächerlich aus: Fliegende Quader, bei denen von allen
Seiten das gleiche Gesicht grinst. Und dazu schweben sie nur starr und dumm im Raum
herum. Es wäre allerdings kein grosses Problem, sie auch zu bewegen. Mann könnte
z.B. einen Timer auf die Form werfen, der regelmässig alle Personen in zufälliger
Weise die Position ändern lässt, inklusive einer Kollisionskontrolle, die
verhindert, dass die Jungs früher oder später ins Nirvana, d.h. in den
"unendlichen schwarzen Raum", abwandern.
Auch die Kollisionskontrolle hakt ab und an; so manches mal habe ich mich schon
durch Raumecken zwängen können, indem ich es nur hartnäckig genug versuchte.
Tja, so dünn bin selbst ich nicht, dass das auch im realen Leben klappen würde ...
So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy. Ist halt
nichts 100%iges. But who cares?
"OGL_HENRYs" wurde mit Delphi 7 programmiert. Der komplette Source, die Texturen, die
Text.Maps, die EXE und die nötigen OGL-DLLs sind alle in diesem ZIP-Archiv verpackt:
OGL-Henrys.zip (ca. 2,2 MB)
Das Original-OpenGL-Paket für Delphi habe ich von
http://www.delphigl.com
gesaugt. Das ZIP-File des Installers der Version "DGLSDK 2006.1", die ich
verwendet habe, findet ihr hier:
dglsdk-2006-1.zip (ca. 8 MB)
Have fun!
HENRY's Auktionshaus als OpenGL-Modell: Eine kleine virtuelle Welt im Eigenbau.