OGL_HENRYs - Eine kleine Welt im Eigenbau

OGL_HENRYs-Tutorial von Daniel Schwamm (02.01.2008)

Inhalt

1. Mit OpenGL neue Welten entdecken

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 ...

1.1. Das gutes, alte VRML

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.

1.2. Delphi kann auch OGL

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.

Delphi-Tutorials - OpenGL HENRY's - WertherSpace, eine Klötzchenwelt mit eigener 3D-Engine
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.

Delphi-Tutorials - OpenGL HENRY's - DelphiGL, ein Installer für eine OpenGL-Umgebung unter Delphi
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?

1.3. Mach 's dir selbst, Programmiere!

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.

2. Delphi-Projekt OpenGL-HENRYs

2.1. Initialisierungsarbeiten

2.1.1. Erste Initialisierungen - FormCreate

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-Tutorials - OpenGL HENRY's - Die Delphi-Form 'hauptf' für die Oberfläche von OpenGL HENRY's
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.

2.1.2. Mehr Initialisierungen - OpenGL-Setup

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.

2.1.3. Und noch mehr Initialisierungen - Texturen & Co.

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).

2.2. Modellierung

2.2.1. Die Welt von HENRY's

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;

2.2.2. Map-Kontrolle durch die Unit "mapu.pas"

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.

2.2.3. ASCII-Text-Maps und Grafik-Renderer

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.

2.2.4. Map-Zeichen ohne Kollisionseigenschaft

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.

2.2.5. Map-Loading & Boden-Decken-Kachelung

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".

2.2.6. Von Map zu Map - lass mich rein, lass mich raus, oh Anna!

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.

2.3. Konvertierung der ASCII-Text-Map zu OpenGL-Objekten

2.3.1. ASCII-Text-Map einladen

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.

2.3.2. Map-Dimensionen berechnen

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.

2.3.3. Objekt- und Personen-Parsing in der Text-Map

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.

2.3.3.1. Von Nummern zur Person

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).

2.3.3.2. Objekt-Gruppierung

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.

2.3.4. Jetzt aber: Map-Objekt zu OGL-Objekt

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.

2.3.4.1. Infos im Objekt-Textur-Dateinamen

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"

Delphi-Tutorials - OpenGL HENRY's - Eine Textur für ein Regal im Raum 'Info'
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").

Delphi-Tutorials - OpenGL HENRY's - Textur ohne Kachelung
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.
Delphi-Tutorials - OpenGL HENRY's - Textur mit Kachelung
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":

Delphi-Tutorials - OpenGL HENRY's - Textur #1 für die Seiten des Theken-OpenGL-Objekts
Textur 'tx-tt-h1.jpg': Textur #1 für die Seiten des Theken-OpenGL-Objekts.
Delphi-Tutorials - OpenGL HENRY's - Textur #2 für die Theken-Platte
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:

Delphi-Tutorials - OpenGL HENRY's - Theke mit Standard-Textur
Theke mit Standard-Textur
Delphi-Tutorials - OpenGL HENRY's - Textur für Grossrechner
Textur 'tx-tt-h1-v.jpg': Textur für einen Grossrechner.
Delphi-Tutorials - OpenGL HENRY's - Theke mit alternativer Textur
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:

Delphi-Tutorials - OpenGL HENRY's - Theke mit Standard-Höhenklasse
Theke mit Standard-Höhenklasse
Delphi-Tutorials - OpenGL HENRY's - Theke mit alternativer Höhenklasse
Theke mit alternativer Höhenklasse: Geheimnisvolles Wachstum der Theke im Raum 'Info'.

2.3.5. Endlich: Die Textmap-Objekte füllen die Strukturen für den Grafik-Renderer

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.

2.4. Idle-Handler & Grafik-Ausgabe - kein bisschen Zeit wird verschwendet

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.

2.4.1. Positionsbestimmung - wir sind das Zentrum der Welt

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.

2.4.1.1. Die Reihenfolge der Bewegung des Modells ist nicht einerlei

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 :-)

2.4.2. Ein Deckel für den Topf, ein Boden für's Fass

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".

2.4.2.1. Faulheit und die Folgen

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.

2.4.3. Texturierung - ist alles so schön bunt hier

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.

2.4.4. Zwischenwelten

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.

2.4.5. Objekte der Begierde positionieren

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.

2.4.6. End of Rendering

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!

Delphi-Tutorials - OpenGL HENRY's - Home-Position im Raum 'Info' vom Auktionshaus HENRY's
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.

2.5. Action - und sie bewegt sich doch

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!

2.5.1. Auf Tastatur-Ereignisse reagieren

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.

2.5.1.1. Bewegung vorwärts und rückwärts

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.

Delphi-Tutorials - OpenGL HENRY's - Ansicht des Raumes 'Flur' in gekippter Perspektive
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.

2.5.1.2. Boden und Decke

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.

Delphi-Tutorials - OpenGL HENRY's - Ansicht des Raumes 'Mode', hier ohne Boden-Texturen
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.
2.5.1.3. Bewegung hoch und runter

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 ...?"

Delphi-Tutorials - OpenGL HENRY's - Flug über den Raum 'Info'
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.
2.5.1.4. Home-Position

Mit "Space" springt man an die Home-Position des Raums zurück, in dem man sich gerade befindet.

2.5.1.5. Raum wechseln

Mit den Zahlen "1" bis "4" kann man schnell von einem Raum zum anderen springen:

Delphi-Tutorials - OpenGL HENRY's - Mein von Technik dominiertes Büro im Raum 'Info'
(1) Raum 'Info': Das technisch reichhaltig ausgestattete Büro des ollen Schwamms.
Delphi-Tutorials - OpenGL HENRY's - An der Decke von Raum 'Flur'
(2) Raum 'Flur': Hier kommt sonst kein Kunde hin.
Delphi-Tutorials - OpenGL HENRY's - Selbstportrait in der Damentoilette im Raum 'Mode'
(3) Raum 'Mode': Was für ein Spanner macht denn da auf den Toiletten mit der Kamera herum?
Delphi-Tutorials - OpenGL HENRY's - Ansicht des Raumes 'EDV'
(4) Raum 'EDV': Chaos für die Chaoten.
2.5.1.6. Beleuchtung ändern

Mittels "L"-Taste kann ein anderes Beleuchtungsmodell gewählt werden.

Delphi-Tutorials - OpenGL HENRY's - Nach Geschäftsschluss ist im Raum 'Flur' das Licht ausgeschaltet worden
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 :-)

Delphi-Tutorials - OpenGL HENRY's - Nebel im 'Flur'? Zeit für einen Feueralarm!
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.
2.5.1.7. Ohne Hilfe geht es nicht

Die "H"-Taste dient dazu, einen Hilfeschirm anzuzeigen oder abzustellen.

Delphi-Tutorials - OpenGL HENRY's - Hilfe-Schirm in OGL_HENRYs
Sichtbar gemachter Hilfeschirm: A little help for my friends.
2.5.1.8. Wo sind wir? Minimap

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.

Delphi-Tutorials - OpenGL HENRY's - Kollisionskontrolle über 2D-Map
Aktivierte Minimap: Die Welt in 2D mit Kollisionsgarantie (oben links).

2.5.2. Kollisionskontrolle

2.5.2.1. Feste Körper

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.

2.5.2.2. Von 2D nach 3D nach 2D

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.

2.5.2.3. Minimap und Unit "panelu"

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.

2.5.2.4. Hat's geknallt? Und überhaupt, wo bin ich hier?

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.

2.6. Raum-Wechsel

2.6.1. Wo soll's denn hingehen, Schätzchen?

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".

2.6.2. Wo lande ich im nächsten Raum?

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!

3. Von einem, der auszog, die Welt zu erforschen

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 :-)

4. Flash-Movie-Demos

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

5. Ein Fazit

"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?

6. Download - eine ganze Welt in ein paar MB

"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!

Delphi-Tutorials - OpenGL HENRY's - Die virtuelle Welt von HENRY's Auktionshaus im Eigenbau
HENRY's Auktionshaus als OpenGL-Modell: Eine kleine virtuelle Welt im Eigenbau.