Tutorial Terrain3

Aus DGL Wiki
Version vom 29. Januar 2008, 20:12 Uhr von Lord Horazont (Diskussion | Beiträge) (Farbraum-Link dem neuen Artikelnamen angepasst)

Wechseln zu: Navigation, Suche

Continuous Level of Detail fur Heightmaps

Einführung

Hi,

Ich habe in meinem ersten Heightmap Tutorial im Nachwort erzahlt, dass ich vielleicht ein Tutorial uber Level of Detail (LOD) fur Heightmaps schreibe. Ich will hiermit mein Versprechen einlosen, will euch aber auch gleich warnen, es ist nicht so leicht, wie man eigentlich denken konnte, denn viele Probleme stellen sich einem in den Weg, z.B. Cracks im Mesh.

Erkundigt man sich unter Spieleentwicklern nach LOD fur Heightmaps, wird man in den meisten Fallen wohl auf den Begriff ROAMing stosen. Dieser wird z.B. in einem meiner Lieblingsgames Tribes 2 verwendet. Mich hat dieser Algorithmus nicht wirklich angesprochen und ich habe weitergesucht. Nach einiger Zeit bin ich auf den Algorithmus aufmerksam geworden, der im Spiel Aquanox sein Konnen zeigt. Genauer gesagt bin ich uber das Virtual Terrain Project darauf gestosen. Das Original Script von Stefan Rottger findet sich direkt auf seiner Homepage unter Terrain Rendering: Real-Time Generation of Continuous Levels of Detail for Height Fields Ich kann euch wirklich warmstens empfehlen, dieses Script sich einmal genauer anzuschauen - sehr interessant. In diesem Tutorial werde ich das Schriftstuck etwas genauer beleuchten und ein paar Hilfestellungen dazu geben, in der Hoffnung, dass ihr es mir gleich tut und diesen Algorithmus selbst einmal nachbaut.

Quadtree

Der Ganze Algorithmus basiert auf einem Quadtree. Dieser wird durch eine Boolean Matrix(fur die Delpher, die immer noch nicht wissen, was eine Matrix ist: Wir reden uber ein 2 Dimensionales Array[0..n,0..n] of Boolean) reprasentiert, so kann man sich eine Menge Pointer(und damit Speicherplatz) und viel Rechenarbeit sparen. Wir gehen davon aus, dass die Matrix eine Grose von (2n+1)x(2n+1) hat. Idealerweise hat die verwendete Heightmap die gleichen Ausmase. Die Hauptnode sitzt in der Mitte der Matrix, die 4 Kinder... Bevor ich in Erklarungsnote komme, hier einfach ein Bild, das den Sachverhalt verdeutlichen soll:

Tutorial Terrain3 quadmatrix.gif

Ok, wir wissen nun, wie die Matrix aufgebaut ist, wie die Mutter Kind Verhaltnisse des Quadtrees sind, etc., nur ist noch immer nicht klar, wozu diese Matrix. Wie bereits gesagt, handelt es sich um eine Boolean Matrix. Die Werte, die die Matrix annehmen kann, sind True oder False :-). Diese sagen aus, ob eine Node gesetzt ist und damit gezeichnet wird, oder eben nicht. Unsere Ziele sind also zum einen die Matrix rendern und zum andern die Matrix berechnen

Rendern der Heightmap

Unsere Landschaft wird gerendert, indem wir den Quadtree rekursiv entlanggehen und immer wenn ein Endpunkt des Baums erreicht wird, wird ein kompletter oder teile eines Dreieck Fans gezeichnet. Diese Fans sind besonders gut dazu geeignet, ohne Locher und Cracks im Mesh ein Heighfield darzustellen:

Tutorial Terrain3 setmatrix.gif Tutorial Terrain3 triangulation.gif

Das linke Bild stellt eine Beispiel Matrix dar, das rechte die dazu passende Triangulation. Ihr konnt gerne nach Stellen suchen, an denen die Moglichkeit besteht, dass dort ein Crack ist. Ihr werdet keine finden. Zuerst wird einmal davon ausgegangen, dass der Levelunterschied im Quadtree zweier benachbarter Blocke niemals mehr als 1 betragt. Diese Bedingung wird beim Berechnen der Matrix abgesichert, dazu spater mehr. Das Zentrum eines Fans, ist immer auch das Zentrum eines Quadtree Blattes. Vier Vertieces bilden die Ecken des Blocks und weitere 4 bilden jeweils die Mittelpunkte der Seiten des Blattes:

Tutorial Terrain3 fan.gif

Es werden also hochstens 8 Dreiecke gezeichnet. Hat eine Seite keinen benachbarten Block gleichen Levels, so wird das Vertex in der Mitte der Seite einfach ubersprungen, was zur obigen Beispiel Triangulation fuhrt.


Erzeugen der Matrix

LOD besagt, dass Gegenstande in groser Entfernung mit einer niedrigeren Auflosung angezeigt werden, als nahe Objekte. Daneben werden auch bei groserer Entfernung bestimmte Teile eines Meshes mit hoherer Auflosung angezeigt, deren Oberflache besonders uneben ist, wodurch die Bildqualitat stark erhoht wird.


Oberflachenfehler

Fehler? Fehler?!? Will ich euch jetzt verarschen, ware doch dumm Fehler zu machen... Nein, die gemeinten Fehler besagen ausschlieslich, wie verfalscht eine Stelle in der Landschaft aussehen wurde, wenn man beim Hinuntergehen des Baumes an der aktuellen Stelle aufhort und zu rendern beginnt. Je hoher dieser Wert, desto wichtiger ist es, den Baum noch tiefer hinabzuschreiten.

Tutorial Terrain3 dhvalues.jpg

Um diese d2 genannten Werte fur ein Quadree Blatt zu erhalten, errechnet man zuerst die Erhebungsunterschiede an den Mitten der 4 Seiten und an den 2 Diagonalen(Achtung: das sind unterschiedliche Werte). Der d2 Wert errechnet sich daraus so:

d2=(1/d)*maxi=1..6|dhi|

In Delphi konnte das ganze dann so aussehen:

  procedure CalcD2Value(X, Z, Size : Longint);
  var
    HSize : LongInt; //Hälfte der eigenen Seitenlänge
    I : LongInt;
    EdgeHeights : Array[1..4] of Single; //Höhe der Ecken
    ExpectedHeight : Single;             //Erwartete höhe zw 2 Punkten
    RealHeight : Single;                 //Tatsächliche Höhe

    DHValues : Array[1..6] of Single;    //DHWerte

  const
    //Ort der Ecken des Quads
    Edges : Array[1..4] of Array[0..1] of ShortInt = ((+1, +1),(+1, -1),(-1,-1),(-1,+1));
    //Jeweils die 2 Ecken, zwischen denen ein DHWert berechnet wird
    DHLines : Array[1..6] of Array[0..1] of Byte =
      ((1,2),(2,3),(3,4),(4,1),(1,3),(2,4));

    //Positionen der DHWerte selber
    DHPositions : Array[1..6] of Array[0..1] of ShortInt =
      ((1,0),(0,-1),(-1,0),(0,1),(0,0),(0,0));

  begin
    //Höhen der 4 Ecken
    for I := 1 to 4 do
      EdgeHeights[I] := GetHeightMapHeight(X + Size * Edges[i,0],
                          Z + Size * Edges[i,1]);

    //DHWerte berechen
    for i := 1 to 6 do
    begin
      //Erwartete Höhe zwischen 2 Punkten
      ExpectedHeight := (EdgeHeights[DHLines[i,0]] + EdgeHeights[DHLines[i,1]])/2;
      //Tatsächlich vorliegende Höhe
      RealHeight := GetHeightMapHeight(
        X + DHPositions[i,0] * Size,
        Z + DHPositions[i,1] * Size);
      //DH Wert ist |absoluter Fehler|
      DHValues[I] := Abs(ExpectedHeight - RealHeight)
    end;

    //D2Value finden und setzen
    QuadMatrix[X,Z].D2Value := Trunc(1/(2*Size) * Max(DHValues));

    //D2Values für die 4 Kinder
    if Size > 1 then
    begin
      HSize := Size div 2;
      CalcD2Value(X + HSize, Z + HSize, HSize);
      CalcD2Value(X + HSize, Z - HSize, HSize);
      CalcD2Value(X - HSize, Z + HSize, HSize);
      CalcD2Value(X - HSize, Z - HSize, HSize)
    end
  end; (*CalcD2Values*)
...
  CalcD2Value((QuadTreeLength-1) div 2, (QuadTreeLength-1) div 2,
    (QuadTreeLength-1) div 2);
...

In diesem Beispiel steht Size immer fur die Halfte der Seitenlange einer Quadtreenode. Man denke hier am Besten an den Radius eines Kreises. Die d2 Werte werden auf Bytegrose komprimiert, um wertvollen Speicher einzusparen.


Matrix erzeugen

Jetzt wollen wir uns daran machen, die furs Zeichnen unbedingt notige Matrix zu erzeugen. Gehen wir einmal davon aus, dass die Matrix bereits sauber entleert ist und damit kein Node als gesetzt gilt. Wir laufen den Quadtree wieder rekursiv hinab und entscheiden an jeder Node nach folgender Formel, ob weiter unterteilt werden muss oder nicht:

f=l/(d*C*max(c*d2, 1)

Ist der Wert von f kleiner 1, dann wird weiter unterteilt. Es gilt: C ist eine feste Konstante. Je hoher sie ist, desto hoher ist die Landschaft insgesamt aufgelost. l ist die Entfernung des Node Mittelpunkts zur Kamera und klein c kontrolliert die Auflosung in Bereichen, die einen hohen Oberflachenfehler aufweisen.


Schweizer Kase

Tutorial Terrain3 kaese.jpg

Das bisherige Ergebnis ist doch mehr ernuchternd als erfreulich. Es sieht alles mehr nach Schweizer Kase aus. Viele Locher, aber sonst nicht viel zu bieten. In einem Spiel kann man so ganz schnell dem Spieler den Spass verderben, wir sollten also etwas dagegen unternehmen.

Um dieses Ziel zu erreichen, mussen wir ganz einfach die d2 Werte so verandern, dass Nebeneinanderliegende Blocke keinen Levelunterschied groser als 1 aufweisen (Bedingung für das Rendern der Matrix, siehe oben). Zuerst mussen also die d2 Werte einmal berechnet worden sein. Nun gehen wir den Quadtree wieder soweit herunter, bis wir genau 1 level vor dem niedrigsten sind und setzen den d2 Wert der Node neu:

QuadMatrix[X,Z].D2Value := Max(QuadMatrix[X,Z].D2Value, 
                               (d2Werte der benachbarten Nodes ein Level tiefer) * K)
K = C / (2 * (C - 1)); C groser 2
  procedure DoCrackAvoid(X, Z, Size, SizeToCheck : Longint);
  const
    Neighbours : Array[0..8] of Array[0..1] of ShortInt =
      ((+0,+0), (+3,+1),(+3,-1),(+1,-3),(-1,-3),(-3,-1),(-3,+1),(-1,+3),(+1,+3));
  var
    HSize : LongInt; //1/4 der Seitenlänge
    I : LongInt;
    D2Values : Array[0..8] of Byte;

  begin
    Assert(Size > 1);
    //Der Baum wird rekursiv abgehandlet(nicht unbedingt die
    //beste Idee. Wer Lust hat, darf sich gerne daran versuchen
    //es einmal ohne Rekursivität zu probieren!)

    HSize := Size div 2;
    //Wir haben noch nicht das gewünschte Level erreicht
    if Size > SizeToCheck then
    begin
      DoCrackAvoid(X + HSize, Z + HSize, HSize,SizeToCheck);
      DoCrackAvoid(X + HSize, Z - HSize, HSize,SizeToCheck);
      DoCrackAvoid(X - HSize, Z + HSize, HSize,SizeToCheck);
      DoCrackAvoid(X - HSize, Z - HSize, HSize,SizeToCheck)
    end
    else
    begin
      Assert(SizeToCheck = Size);
      D2Values[0] := QuadMatrix[X,Z].D2Value;
      for i := 1 to 8 do
        //Nachbarnodes holen....
        D2Values[i] := GetNode(X + HSize * Neighbours[i][0],
                               Z + HSize * Neighbours[i][1]) * K;

      QuadMatrix[X,Z].D2Value := MaxByte(D2Values)
    end
  end; (*DoCrackAvoid*)

Diesen Vorgang wiederholen wir, wobei wir immer ein Level niedriger gehen, bis wir schlieslich das Level 1 erreichen. Wenn wir nun unsere Matrix berechnen, ist unsere Bedingung erfullt und wir erhalten ein Mesh ohne Locher:

Tutorial Terrain3 result.jpg
Tutorial Terrain3 wireframe.jpg

Wer glaubt hier ein paar Locher finden zu konnen liegt falsch. Die Stellen die schwarz erscheinen, werden durch das OpenGl Licht nur so unglucklich getroffen, dass sie fast schwarz erscheinen. Ist euer Monitor richtig eingestellt, sollte dagegen alles passen. Siehe dazu auch: Farbraum. Die "Kamera" befindet sich übrigens links unten im Bild, dort wo ein rotes Quad sichtbar ist.

Anmerkung: Nach Aussage von Stefan Röttger ist diese Methode um Löcher zu entfernen nicht optimal. Es genügt bei jedem Vater Knoten alle Kinder in die Berechnung einzubeziehen. Die Nachbarn müssen nicht einbezogen werden. Genaueres findet sich in diesem Forumsbeitrag.

Nachwort

Wie gehts weiter? Um die ganze Sache nun noch ein bischen weiter zu verbessern, sollte man uberlegen, ob man die Landschaft nicht vielleicht texturiert. Hier bietet sich sicher das 2. Highmap Tutorial als guter Anfang an. Auch konnte man das "Popping" unterbinden, das auftritt, wenn man sich einer Stelle nahert und plotzlich ein paar neue Dreiecke angezeigt werden. Das ganze ist bekannt unter dem Begriff Geomorphing.

Nico Michaelis aka DelphiC



Vorhergehendes Tutorial:
Tutorial_Terrain2
Nächstes Tutorial:
Tutorial_Skyboxen

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.