AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Tutorials Tutorial: (Perlin) Noise
Tutorial durchsuchen
Ansicht
Themen-Optionen

Tutorial: (Perlin) Noise

Ein Tutorial von Aphton · begonnen am 6. Aug 2013
Antwort Antwort
Benutzerbild von Aphton
Aphton
Registriert seit: 31. Mai 2009
Guten Abend

Vorwort
Da ich mich vor kurzem nach Perlin Noise umgesehen habe und kaum fündig wurde (auch nichts auf der dp ) habe ich mich entschlossen,
ein kleines Tutorial zu schreiben. Mehr oder weniger als Dankeschön an die DP-Community!

Dank diversen Quellen bin ich mir nicht sicher, ob das, was ich unter Perlin Noise verstehe nicht zufällig Value-Noise ist, da es bei sehr vielen Quellen, wo ich mein Wissen her habe, verwechselt wurde. Bitte um Korrektur, sofern notwendig.

Was ist Perlin Noise (Abk. P.N.)
Meines Wissens nach ist P.N. kurz und bündig eine pseudozufällige Rauschfunktion. Zum Einsatz kommt sie größtenteils in der Computergrafik.
Sie eignet sich u.A. für "die Bildsynthese, um natürliche Phänomene wie Wolken, Landschaftstopologien oder Wasser zu simulieren" (laut Wikipedia).

Unser Algorithmus wird grundsätzlich ein zwei dimensionales Array vom Typ Byte zurücklieferen. Hierbei wird es sich dann um solch ein Resultat handeln! Was man damit nun anstellt, ist einem selbst überlassen.

Damit man sich darunter evt. mehr vorstellen kann, hier eine Landschaftsgenerierung Anhand dieser Heightmap.
(Jeder Pixelwert (im Heightmap gibt die Höhe der Landschaft relativ zu der Position an, wobei 0 (schwarz) der niedrigste und 255 (weiß) der höchste Wert ist)

Notwendiges Vorwissen
  • Umgang mit Vektoren (Skalarprodukt, Normalisierung)
  • Interpolation (nicht linear!)

Der Algorithmus
  1. Einen Seed für den Zufallszahlen-Generator setzen. Dieser kann beliebig gewählt werden. Weiters ist dieser Schritt hauptsächlich zuständig für das Aussehen des Resultates!
  2. Ein beliebig großes, zwei dimensionales Array von Vektoren (x, y Tupel) erstellen und zufällig initialisieren und direkt normalisieren. Dieses Array nennen wir von nun an dispMap (displacement Map).
  3. Nun für jeden Punkt des Resultates:
    1. nach dispMap Dimensionen mappen -> diesen Punkt nennen wir nun pos.
    2. pos ist nun von vier Punkten im dispMap umzingelt. Man berechnet nun jeweils den Richtungsvektor von den vier Punkten zu pos. Diese vier Richtungsvektoren speichern wir
      in einem Array ab und nennen es gradients[].
    3. Nun bildet man jeweils das Skalaprodukt zwischen den vier dispMap und gradient Vektoren. Diese speichern wir und nenn sie von nun an dotprods[] (dot products array).
  4. Letzendlich interpoliert man die dotprodts zweimal horizontal (um die beiden Punkte bei pos.x zu bestimmen) und einmal vertikal, um letzendlich den Wert für den Punkt pos zu bestimmen.
  5. Das Resultat (eine Fließkommazahl) anschließend normalisieren: durch die diagonale eines der Quadrate in der gridMap dividieren.

Der Algorithmus mag auf dem ersten Blick schlecht verdaubar aussehen, aber durch die folgenden Codesnippets sollte es deutlicher werden.

Implementierung
Bevors damit losgeht - die Snippets sind zum größten Teil aus der Demo (Anhang) und zum Teil schnell aus dem Gedächtnis hingeschrieben.
Könnte kleine Tippfehler enthalten.
  1. Delphi-Quellcode:
    // einfaches Setzen über eine Zahl
    procedure setRandomSeed(const Seed: Integer); overload;
    begin
      RandSeed := Seed;
    end;

    // einfache Seed Berechnung durch (evt.) künstlich hergeführtes Integer-Overflow
    procedure setRandomSeed(const Seed: AnsiString); overload;
    var
      idx, sd: Integer;
    begin
      sd := 0;
      for idx := 1 to Length(Seed) do
        sd := sd*256 + Byte(Seed[idx]);
      RandSeed := sd;
    end;
  2. Delphi-Quellcode:
    // Datenstruktur für ein Vektor:
    type
      PVector2 = ^TVector2;
      TVector2 = record
        X, Y: Single;
        procedure assign(const AX, AY: Single);
        procedure normalize();
        class operator multiply(const A, B: TVector2): Single;
      end;

    // ...

    procedure TVector2.assign(const AX, AY: Single);
    begin
      X := AX;
      Y := AY;
    end;

    procedure TVector2.normalize();
    var
      size: Single;
    begin
      size := SQRT(X*X + Y*Y);
      if size = 0 then Exit; // divison-by-zero vermeiden
      X := X/size;
      Y := Y/size;
    end;

    class operator TVector2.multiply(const A, B: TVector2): Single;
    begin
      Result := A.X*B.X + A.Y*B.Y;
    end;

    // ...

    var
      dispMap: Array of Array of TVector2;

    // Punkt 2.
    const
      size = 8; // 8x8 dispMap!
    var
      i, j: Integer;
    begin
      SetLength(gridMap, size, size);
      for i := 0 to size-1 do
        for j := 0 to size-1 do
        begin
          dispMap[i][j].Assign(2*Random() - 1, 2*Random() - 1);
          dispMap[i][j].normalize();
        end;
    end;
  3. Nun der Kern:
    Delphi-Quellcode:
    type
      TByteArrayArray = Array of Array of Byte;
      TInterpolationFunc = function(const x, y, t: Single): Single;
      TVector2Map = Array of Array of TVector2;

    // ...

    function interpolator(const x, y, t: Single): Single;
    var
      f: Single;
    begin
      f := 3*t*t - 2*t*t*t;
      Result := x * (1-f) + y * f;
    end;

    {
      w und h geben die Breite und Höhe des Resultates an
      interpolator ist eine Funktion (vorzugsweise nicht-linear)
    }

    function generateNoiseBitmap(const w, h: Integer; const dispGrid: TVector2Map;
      const interpolator: TInterpolationFunc): TByteArrayArray;
    var
      vertices: Array[0..3] of PVector2;
      gradients: Array[0..3] of TVector2;
      dotprods: Array[0..3] of Single;
      gridDelta, pos: TVector2;
      leftBottomIdx: TPoint;
      fac: TVector2;
      y, x, i: Integer;
      pValue: PByte;
      diag, interpolation: Single;
    begin
      SetLength(Result, h, w);
      if not Assigned(interpolator) then Exit;
      // gridDelta gibt die breite und höhe eines Quadrates des dispMaps an (sie ist abhängig von der Breite und Höhe des Resultates):
      gridDelta.Assign(w / High(dispGrid), h / High(dispGrid[0]));
      // die Diagonale wird für die Normalisierung der Werte berechnet
      diag := gridDelta.Size;
      for y := 0 to h - 1 do
      begin
        pos.Y := y;
        pValue := @Result[y,0];
        for x := 0 to w - 1 do
        begin
          pos.X := x;
          // leftBottomIdx ist die nach dispMap gemappte Position des Punktes "pos"
          leftBottomIdx := Point(Floor(x/gridDelta.X), Floor(y/gridDelta.Y));
          // hier werden die 4 umzingelnde Punkte bestimmt
          vertices[0] := @dispGrid[leftBottomIdx.X+0, leftBottomIdx.Y+0];
          vertices[1] := @dispGrid[leftBottomIdx.X+0, leftBottomIdx.Y+1];
          vertices[2] := @dispGrid[leftBottomIdx.X+1, leftBottomIdx.Y+1];
          vertices[3] := @dispGrid[leftBottomIdx.X+1, leftBottomIdx.Y+0];
          // Gradienten Berechnung
          gradients[0] := vector2(gridDelta.X*(leftBottomIdx.X+0), gridDelta.Y*(leftBottomIdx.Y+0)) - pos;
          gradients[1] := vector2(gridDelta.X*(leftBottomIdx.X+0), gridDelta.Y*(leftBottomIdx.Y+1)) - pos;
          gradients[2] := vector2(gridDelta.X*(leftBottomIdx.X+1), gridDelta.Y*(leftBottomIdx.Y+1)) - pos;
          gradients[3] := vector2(gridDelta.X*(leftBottomIdx.X+1), gridDelta.Y*(leftBottomIdx.Y+0)) - pos;
          // fac gibt an, wie weit die gemappte Position horizontal und vertikal in den umzingelnden 4 Vertices liegt (in %, also [0,1])
          fac.Assign(
            x/gridDelta.X - leftBottomIdx.X,
            y/gridDelta.Y - leftBottomIdx.Y);
          // Skalarproduktberechnung
          for i := 0 to 3 do
            dotprods[i] := gradients[i] * vertices[i]^;
          interpolation :=
            interpolator( // vertikale interpolation der..
              interpolator(dotprods[0], dotprods[3], fac.X), // beiden horizontal interpolierten..
              interpolator(dotprods[1], dotprods[2], fac.X), // Werte!
              fac.Y) / diag; // die direkt normalisiert wird
          // interpolation = -1..1
          interpolation := (interpolation + 1)/2;
          // interpolation = 0 .. 1
          pValue^ := Max(0,Min(255, Floor(255*interpolation)));
          inc(pValue);
        end;
      end;
    end;

Überlagerung
Der Algorithmus generiert nun Graustufenbilder. Diese sehen je nach dispMap Dimensionen verschieden aus.
Das Rauschen ist umso stärker, je größer die Dimension des dispMaps ist.
In der Praxis berechnet berechnet man mehrere solcher NoiseMaps (mit 8x8, 16x16, 32x32) und überlagert (einfaches Blenden der Resultate!) diese!

Resultate
Im Anhang befindet sich eine kleine Demo Anwendung.

In der Memo befinden sich zwei Zeilen:
Code:
8x8,0.5
16x16,0.5
Bedeutung (Zeile 1):
- 8x8 gibt die Dimension des dispMaps an
- 0.5 gibt die Gewichtung für die Überlagerung später an

Es können beliebig viele Zeilen hinzugefügt werden! Achtet aber darauf, dass die Syntax stimmt und die Gewichtungen in Summe 1 ergeben!

Ich weiß nicht mehr genau warum, aber drüber befindet sich noch eine SpinEdit Komponente, bei der man einstellen muss, wie viele
dispMaps generiert werden sollen.
Exception-Handling ist nicht wirklich drinnen, hierbei handelt es sich nur um eine Demo!

Resultate sind übrigens im Anhang auch zu finden!

PS: Ich bin nicht allzu sehr ins Detail gegangen, das wird mir gerade bewusst. Sofern es Unklarheiten gibt, einfach Fragen, ich werde, so gut es geht, versuchen, diese zu beantworten.
Miniaturansicht angehängter Grafiken
3t%5E2-2t%5E3.png   schritt-2-dispmap.png   res1.png   res2.png  
Angehängte Dateien
Dateityp: rar MakeSomeNoise (Perlin).rar (329,5 KB, 64x aufgerufen)
das Erkennen beginnt, wenn der Erkennende vom zu Erkennenden Abstand nimmt
MfG

Geändert von Aphton ( 6. Aug 2013 um 14:46 Uhr)
 
Antwort Antwort


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 18:00 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz