Home

News

Software
  - HTML
  - DHTML
  - Javascript
  - CGI
  - VRML
  - Linux
  - Dirty-Progs
    - CSS-DIV-Slicer
    - Sprite-Painter
    - FLV-CCC
    - CPU-Eater
    - Pixel-Evolution
    - MediaPanelyzer
    - OpenGL ISS
    - OpenGL Planets
    - PicOfPics
    - OpenGL Henrys
    - VidSplitt

  - PHP
    - Src2Textarea
    - Volltext-Suche
    - Hilfsfunktionen

Bilder

Texte

Alles fliesst

Comics

Musik

Leben

Links

Sitemap

Admin


Börsen-Infos bei
comdirect

OGL_Henrys - Eine kleine Welt im Eigenbau

OGL_Henrys-Tutorial von Daniel Schwamm (02.01.2008)

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

Mit Open GL neue Welten entdecken

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

Verlixt, es ist exakt 3: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 ...

Das gutes, alte VRML

1997 hatte ich meine erste Berührung mit 3D-Welten, die man selbst basteln konnte. Mit VRML, einer Scriptsprache, 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. Allerding 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. Allerding, so scheint es, ist VRML tot.

Delphi kann auch OGL

Im Laufe der Zeit habe ich einige "3D-Engines" in Delphi programmiert, für Spiele und Grafikdemos. War eine ziemlich mühseelige 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.

OpenGl Henrys - WertherSpace, eine Kötzchenwelt mit eigener 3D-Engine

"WertherSpace": Ein Klötzchenwelt vom Schwamm mittels eigener 3D-Engine

Dann stolperte ich im Web über ein Bericht, bei dem es um Open GL für Delphi ging.

OpenGl Henrys - DelphiGL, ein Installer für OGL mit Delphi

"delphigl.exe": Ein Installer für eine OGL-Entwicklungsumgebung für Delphi

Mein Interesse erwachte auf's neue; ich zog mir das Material und schaute mir die mitgelieferten 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?

Mach 's dir selbst, Programmiere!

Ich beschloss, es herauszubekommen. Und so begann das kleine Projekt "OGL_HENRY", eine Art virtuelles Abbild meiner Arbeitsstätte, dem Auktionshaus HENRY's.

Über die URL "http://www.delphigl.com/" kommt man an die Bibliotheken mit den OGL-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.

Initialisierungsarbeiten

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.

OpenGl Henrys - Hauptf, die Delphi-Hauptform von OGL_HENRYs

"Hauptf": Hauptform mit Filelistboxen, die auf Textur-Ordner zeigen, Memos, die die Textmaps speichern, 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:

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's übrigens 'ne 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:

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,     //farbbits
    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 Programmes weg; diese Information wird später noch benötig, 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 DeviceContext "DC" der Form liefert. Über die OGL-Funktionen "CreateRenderingContext" und "ActivateRenderingContext" sorgen wir anschliessend dafür, dass alle OGL-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 rauskitzeln. 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.

Noch mehr Initialisierungen

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:

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);        //gerichtetet 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 waehlen ...
  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); // 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 auschalten lassen. Mit Hilfe 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.

Eine Welt wird gebaut

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 nun unsere Welt zusammen, die in die eben definierte OGL-Umgebung plaziert wird.

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" komlett 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 JPGs mit der Dimension 256x256 Pixel.

Aus der möglichen Grösse der Texturen wurde ich nie so ganz schlau. OGL kann durchaus mit grösseren Bildern als 256x256 Pixeln arbeiten. Auf meinem Geschäfts-PC gab's damit jedenfalls keine Probleme. Aber bei meinem HomePC 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 hatte. Ich vermute mal, es ist die Grafikkarte, die entscheidet, wie gross die Texturen werden dürfen.

Also, jedes gefundene Bild wird an die inerne Funktion "settx" übergeben. Die läd das Bild mittels der OGL-Funktion "IMG_Load" ein und bindet es in die OGL-Umgebung ein. 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 viruellen Welt ist das nicht vorgesehen).

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:

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" sehen wir uns gleich 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 Unter-Raums. 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.

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
  //raum-koordinaten
  px:=map.startpos.x;
  py:=map.startpos.y;
  pz:=map.startpos.z;

  //richtung, in die man schaut
  rotx:=map.startrot.x;
  roty:=map.startrot.y;
  rotz:=map.startrot.z;
end;


Die Unit "mapu"

Die Funktionen, die den Aufbau unserer vier Unter-Rä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 mal näher am Beispiel des "Info"-Raums ansehen.

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-chars
  clrdurchgang;
  hauptf.map.durchgang[0]:='i';

  //lese map, objekte und personen
  rdmap('Info',hauptf.map);

  //decken/boden-aufteilung 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 unserers "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 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 mal 50 cm oder so eintragen :-)

Für grafikerfahrene Programmierer bleibt übrigens festzuhalten, dass sich der Nullpunkt bei OGL stets unten links befindet, nicht wie beim Microsoft üblich oben links! Die OGL-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 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 Gradwerte zu verstehen, reichen also von 0 bis 359. 0/0/0 bedeutet, wir schauen geradeaus, 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 OGL so, das wir, der Betrachter, uns gar nicht bewegen oder rotieren, sondern nur der Raum um uns herum! Und wenn der sich 20 Grad nach rechts dreht, schauen wir eben 20 Grad nach links.

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 ... 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's auch der Realität entspricht). Andere Räume, wie etwa der Flur, sind sehr viel höher.

Textmaps und GrafikRenderer

An dieser Stelle sei kurz angedeutet, wie wir unsere Welt eigentlich in die OGL-Umgebung bekommen. Wenn man so will, war dies der für mich kreativtse Teil der ganzen Arbeit. Denn Anfangs hatte ich keinen blassen Schimmer, wie ich das anfangen soll.

Bei vielen OGL-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 Zwei-Dimensionale 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 stur an den Raumkoordinaten 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 Menschen leben sollen). Wir bauen nur virtuell nach, was bereits Realität ist.

Okay, sehen wir uns mal an einem kleinen Beispiel an, wie man eine solche Textmap getalten kann:

Ein leerer Raum:

WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W                          W
W                          W
W                          W
W                          W
W                          W
W                          W
WWWWWWWWWWWWWWWWWWWWWWWWWWWW

Mit Tisch ist's schöner:

WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W                          W
W                          W
W                          W
W                          W
W   ttttt                  W
W                          W
WWWWWWWWWWWWWWWWWWWWWWWWWWWW

Jetzt noch eine Theke 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 Textmaps sind natürlich schon viele vor mir gekommen. Ich habe nur keinen OGL-Source gefunden, der mir gezeigt hätte, wie ich diese Maps letztlich an den GrafikRenderer verfüttern kann.

Denn das ist natürlich der nächste Schritt: Eine Art Parser muss die Textmaps durchlaufen, die gefundenen Zeichen interpretieren sowie ihre Position berücksichtigen und daraus die entsprechende OGL-Objekte generieren, ganz so wie bei den direkt programmierten Objekt-Listen.

Mh ... was ich hier GrafikRenderer nennen, ist vielleicht gar kein GrafikRenderer. Ist mir aber Wurst, ich nenne das Teil jedenfalls so. Das ist der Part im Programm, der meine Liste an Objekten durchgeht und sie an die definierten Positionen im Raum malt, wobei dafür gesorgt wird, das Objekte weiter hinten perspektivisch verkleinert werden und keine Objekte verdecken, die weiter vorne liegen. Praktisch bei OGL ist, dass es die ganze Mathematik dahinter für uns erledigt.

Zu diesem GrafikRenderer kommen wir aber erst später.

Kollisionsfreie Durchgänge

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 GrafikRenderer später als "Durchgänge" begreifen soll. Naja, 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" einem Teppich, der auf dem Boden liegt, "F" eine Art Tor, unter dem man durchgehen kann usw.

MapLoading & 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 MAP-Struktur plaziert, sondern einfach - mathematisch berechnet - über die komplette Raumgrösse gespannt. Um doch noch ein wenig Abwechslung rein zu bringen, werden die Boden- und Decken-Texturen über ein paar Struktur-Variablen in raumspezifischer Weise "gekachelt".

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 wie folgt deklariert:

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 GrafikRenderer die spezielle Bedeutung eines Ausgangs (ähnlich wie beim Durchgang). Kommen wir an eine solche Stelle, sagt die Kollisionskontrolle nicht "Autsch! Du bis gegen eine Wand gerannt", sondern veranlasst vielmehr die Neu-Initialisierung der sich am Aus-/Eingang anschliessenden 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 plazieren.

Das "outa"-Array erlaubt es, maximal fünf verschiedene Ausgänge je Map setzen zu können. Die "Info"-Map hat aber nur einen.

Konvertierung der TextMap zu OGL-Objekten

Betrachten wir nun die "rdmap"-Prozedure, die unsere Textmap von der Platte liest, zeilen- und zeichenweise durchgeht, und daraus die zu den Zeichen gehörenden OGL-Objekte generiert.

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
      //markier breiteblöcke
      s[c]:='_';hauptf.mapm.lines[z]:=s;
      inc(w);
      inc(c);
    end;

    //wie tief ist objekt
    d:=1;
    fullok:=_spanok;
    //fullok:=false;
    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 tiefe-blöcke
      s[x]:='|';hauptf.mapm.lines[r]:=s;

      if _spanok then begin
        //volle breite mit objekt-blöcken?
        if fullok then begin
          c:=x+1;
          while(c<x+w)and(s[c]=ch) do begin
            //markiere breiteblö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 erstmal über-dimenesionieren
  setlength(map.quads,0);
  setlength(map.quads,anz);
  map.anz:=anz;
  hauptf.map.w:=0;
  hauptf.map.d:=0;

   //hole objekte+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 objektcounter
        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;

  //decke 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 Leezeichen 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 GrafikRenderer statt 200 Objekten nur 30 verwalten muss, wird die Ausgabe erheblich flüssiger. Wo ich vorher nur ruckelnd 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:

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;     //homeposition des betrachters
    startrot:trot;     //homerotation 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-Initialiserung der Map nur wenige Sekunden dauert, wäre das eher Speicherverschwendung.

Map-Dimensionen

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

Objekt- und Personen-Parsing

In "s" steht jeweil 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 GrafikRenderer 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 Textmaps tunlichst auf die Verwendung des "<"-Zeichens verzichtet werden sollte! Das gilt auch für die Zeichen "|" und "_", die hier ebenfalls intern Verwendung finden.

Von Nummern zur Person

Finden wir dagegen ein Zeichen zwischen '0' und '8', dann wird an diese Stelle kein Objekt plaziert, 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 reaserviert 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 Texmap zu plazieren, 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).

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 in wie weit sich anschliessende Objekte zu einer Gruppe zusammen fassen lassen.

Das will ich hier mal an einem Beispiel verdeutlichen:

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
genausoviele '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 nocheinmal 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 Dimensionswerten 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, so dass er am Ende auf der echten Anzahl Objekte in der Map steht.

Schliesslich haben wir die TextMap durchgeparst und alle Objekte erstellt. Nun werden noch ein paar Map-Variablen gesetzt und ganz am Schluss mit "panelu.initp" die 2D-Minimap initialisiert.

Jetzt aber: MapObjekt 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:

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 Textturdatei bestimmen: "tx_"+ss+".jpg". Über die IndexOf-Funktion der Personenfilelistbox "pflb" finden wir den Index der Datei, der gerade dem Index unserers Textur-Arrays "txa" entspricht. Dann rufen wir "setpersonquad" auf.

Handelt es sich um ein Objekt-Zeichencode, dann muss etwas aufwendiger vorgegangen werden.

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überhinaus 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-Textmaps 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-Filennamen! 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"

OpenGl Henrys - Die Textur für ein Regal im Raum INFO

"tx_r0_h9_w3_d3.jpg": Die Textur für ein Regal in der "Info"

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

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

OpenGl Henrys - Textur-Kachelung aktiv OpenGl Henrys - Textur-Kachelung inaktiv

Textur-Kachelung: Ohne Textur-Kachelung (rechts) wirkt der Regalinhalt unnatürlich gross

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

OpenGl Henrys - Textur #1 für die Theken-Seiten OpenGl Henrys - Textur #2 für die Theken-Platte

"tx_tt_h1.jpg" und "tx_tt_h1_o.jpg": Zwei Texturen für 'ne Theke

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-neutralen" 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 Texturfile mit passendem Namen in den Datei-Ordner zu schmeissen:

OpenGl Henrys - Textur für Grossrechner

OpenGl Henrys - Normale Textur für Theke OpenGl Henrys - Alternative Textur für Theke

"tx_tt_h1_v.jpg": Eine alternative Forderfront für Theken in der "Info"

In gleicher Weise kann man auch Objekte höhenverändern, in dem man einfach deren Textur-Dateinamen-Höhencodes ändert:

OpenGl Henrys - Höhe der Theke korrekt OpenGl Henrys - Höhe der Theke künstlich vergrössert

"tx_tt_h1.jpg" wird zu "tx_tt_h3.jpg": Geheimnisvolles Thekenwachstum in der "Info"

Endlich: Die Textmap-Objekte füllen die Strukturen für den GrafikRenderer

Wir haben unsere Textmap eingelesen und alle Objekte und Personen herausgeparst. Dann haben wir über die zugehörigen Texttur-Dateinamen zusätzliche Informationen zum Erscheinungsbild gewonnen. Bleibt nur, diese Informationen an Strukturen zu übergebe, die der GrafikRenderer später "verstehen" kann.

Das ist eine ziemlich unspektakuläre Aktion, wie man hier sieht:

//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 GrafikRenderer entsprechend angepasst werden.

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 IdleHandlers, den wir in "FormCreate" auf die Funktion "IdleHandler" umgebogen haben. Denn diese wird nun automatisch aufgerufen.

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 Malaktionen im Detail analysieren will. Üblicherweise hat "_bremse" aber natürlich den Wert "0".

Wir widmen uns nun der Funktion "drawscene", die über den IdleHandler aufgerufen wird, wann immer das Programm Zeit dazu findet.

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

  //collisionscheck
  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;

  //boxmode: 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 GrafikRenderer, 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 inne hat. Diese Werte können sich seit der letzten "DrawScene"-Aktion natürlich geändert haben.

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, so dass 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.

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

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

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 Textmaps 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 plazieren 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 Textmaps keine Rolle spielt, blieb deren Massstab davon unberührt.

Aber natürlich mussten alle Textmaps 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 'ne elende Schufterei, denn zu dem Zeitpunkt waren meine Textmaps mit den 1 x 1 m Ausmassen eigentlich schon fertig gewesen.

Naja, nicht nur die Textmaps waren fertig, auch der GrafikRenderer 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 GrafikRenderer.

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:

Nimm die Textur-Ecke links unten:            glTexCoord2f(0,0);
Plaziere dieses Eck links vorne im Modell:   glVertex3d(0,h,0);
      
Nimm die Textur-Ecke links oben:             glTexCoord2f(0,map.txdeckedc);
Plaziere dieses Eck links hinten im Modell:  glVertex3d(0,h,d);

Nimm die Textur-Ecke rechts oben:            glTexCoord2f(map.txdeckewc,map.txdeckedc);
Plaziere dieses Eck rechts hinten im Modell: glVertex3d(w,h,d);

Nimm die Textur-Ecke rechts unten:           glTexCoord2f(map.txdeckewc,0);
Plaziere dieses Eck rechts vorne im Modell:  glVertex3d(w,h,0);


Der Boden unserer Welt wird in gleicher Weise realisert. 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.

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, so dass 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.

Objekte der Begierde

Jetzt endlich werden unsere Personen und Objekte in's OGL-Modell integriert. Eine einzige kleine Schleife erledigt den Job, bei der alle Map-Objekte "maps.quads" an die Prozedur "drawquad" übergeben werden.

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.

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!

OpenGl Henrys - Home-Position im Raum INFO

OGL_HENRYs: Home-Position in der "Info"

Und sie bewegt sich doch

Wir können das Hauptfenster beliebig vergrössern und verkleinern, die Grafik passt sich voll-automatisch an. Ist 'ne prima Sache. Das haben wir durch Abfangen des OnResize-Ereignis der Form erreicht.

Aber egal, wie sehr wir nun auch am Fenster rütteln und schütteln mögen, wir erhalten stets nur den gleichen Einblick in unsere Welt, 20 Grad nach links in die "Info" hinein. Das ist öde. Da muss mehr Action rein!

Bewegung wird in OGL_Henrys über verschiedene Tastaturcodes realisiert. Dazu basteln wir uns eine Funktion zum OnKeyDown-Ereignis der Form:

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 Koodinaten-Änderungen erfolgen über die Cursortasten. "Cursor hoch" führt uns tiefer ins Modell rein, "Curser runter" weiter raus.

Beim Vorwärts- 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 Cosinus-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.

OpenGl Henrys - Gekippt im Flur

Schräge Perspektive: Gekippt im "Flur"

Ach ja, die "Gewichtigkeit", mit der ein Tastendruck eine Koordinaten- und/oder Rotations-Variable ändert, wird in Konstanten festgeschrieben. So bewirkt etwa ein "Cursor hoch"-Tastaturdruck eine Bewegung in den Raum um "_pstep" Meter. In meinem Modell hat "_pstep" den konstanten Wert "1", was ja auch in etwa der Länge eines Schrittes entspricht.

Die "B"-Taste schaltet in abwechselnder Reihenfolge die Anzeige von Boden und Decke ab. Das erlaubt ungewohnte Einsichten von unten und oben in das Modell hinein.

OpenGl Henrys - Moden ohne Boden

Moden ohne Boden: Unter uns nur ein schwarzes Loch

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

OpenGl Henrys - Auf und ab in der INFO

Bild Hoch/Runter in der "Info": Nur echtes Fliegen ist schöner

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

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

OpenGl Henrys - Mein von Technik dominiertes Büro

1) "Info"-Raum: Das Büro des ollen Schwamms

OpenGl Henrys - An der Decke von Raum FLUR

2) "Flur"-Raum: Hier kommt sonst kein Kunde hin

OpenGl Henrys - Selbstportrait in der Damentoilette im Raum MODE

3) "Mode"-Raum: Was für'n Spanner macht 'n da auf der Toilette mit der Kamera rum?

OpenGl Henrys - Vollgestopfter EDV-Raum

4) "EDV"-Raum: Chaos für die Chaoten

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

OpenGl Henrys - Nach Geschäftsschluss im Raum FLUR

4) "Flur"-Raum: Im Dunkeln lässt sich gut Munkeln

"N" schaltet Nebel an und aus. Vielleicht nützlich für Feuerwehr-Übungs-Simulationen :-)

OpenGl Henrys - Zeit für einen Feueralarm!

The Fog: Nebel des Grauens im "Flur"

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

OpenGl Henrys - Hilfe-Schirm in OGL_HENRYs

Hilfe-Schirm: A little help for my friends

Und zu guter letzt aktiviert bzw. deaktiviert "P" die Anzeige der Minimap. Ist die Minimap zu sehen, findet übrigens ein Kollisions-Check statt, ist sie nicht zu sehen, können wir wie die Geister durch Wände gehen.

OpenGl Henrys - Kollisions-Kontrolle über 2D-Map

Minimap: Die Welt in 2D mit Kollisionsgarantie

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.

Soweit 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 Kollisions-Kontrolle selbst. Benutzt habe ich dazu die 2D-Minimap, um die wir uns jetzt kümmern wollen.

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

Die 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":

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*50cm=100m), 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 zeilen- 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-Pixelmap 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 entä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.

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überhinaus 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überhinaus 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":

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;

  //mapbackground
  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 GrafikRenderer 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.

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:

procedure chgmap(o:tout);
var
  r,c:integer;
  s,mapstart:string;
  rx,ry,rz:single;
begin
  //positionswerte merken
  mapstart:=o.mapstart;
  rx:=hauptf.rotx;
  ry:=hauptf.roty;
  rz:=hauptf.rotz;


  //map laden
  if      o.map='Info' then setmap_info
  else if o.map='Flur' then setmap_flur
  else if o.map='Mode' then setmap_mode
  else if o.map='EDV' then setmap_edv;

  //finde startposition
  for r:=0 to hauptf.mapm.Lines.count-1 do begin
    s:=hauptf.mapm.Lines[r];
    c:=pos(mapstart,s);
    if c>0 then begin
      hauptf.px:=(c+1)/2;
      hauptf.pz:=r/2;

      //rotation anpassen
      hauptf.rotx:=rx;
      hauptf.roty:=ry;
      hauptf.rotz:=rz;

      exit;
    end;
  end;
end;


In "o" bekommt die Funktion den "getroffenen" Ausgang übergeben. In dessen Struktur-Attribut "o.map" ist vermerkt, wo die Reise hinzugehen hat.

Treffen wir z.B. auf den Nord-Ausgang des "Flur", dann steht hier, dass der nächste Raum die "Mode" zu sein hat. Treffen wir dagegen auf den Süd-Ausgang, dann muss die "Info" folgen.

Die entsprechende "setmap"-Prozedur wird also aufgerufen. Nun gilt es noch, im neuen Raum auch an der richtigen Stelle "herauszukommen".

Die Information steckt im Attribut "o.mapstart", und zwar in Form einer zweistelligen Zahl, die mit "9" beginnt. Folgendes Beispiel soll dies kurz illustrieren:

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 unserers Avatars angepasst (denn die wurde durch die "setmap"-Funktion rückgesetzt) so dass 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ärtsgewand zu betreten - und solche Spleens unterstützen wir doch gerne :-).

Damit hätten wir's nun wirklich geschafft: Die "OGL_HENRYs"-Welt ist fertig!

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

Ein Fazit

"OGL_HENRYs" lässt sich relativ leicht und schnell an eigene Bedürfnisse anpassen. Man muss sich ja nur die Textmaps vornehmen und die diversen Zeichen so umplazieren, 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 Textmaps erlauben ja immer nur ein Objekt an einem Ort. Um also etwa PCs auf Schreibtischen zu plazieren, 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, so dass 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 quaderförmigen 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 ...

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

Eine ganze Welt in ein paar MB

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

   OGL-Henrys.zip (ca. 2,2 MB)

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

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

Have fun!

OpenGl Henrys - Virtuelle Welt im Eigenbau

"OGL_HENRYs": Eine kleine virtuelle Welt im Eigenbau


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

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