Ich möchte fraktale Landschaften generieren und die Möglichkeiten nutzen, die mit LaTeX und Co. verfügbar sind.

Ansatz:

  • Berechnung der Landschaft mit Lua nach dem Diamond-square Algorithmus (siehe auch englischsprachige Beschreibung)
  • Ausgabe mit pgfplots, denn hier habe ich vielfältige Darstellungsmöglichkeiten
  • Surface plot mit mesh
  • Colormap zum Einfärben verwenden: blau für unterhalb des Meeresspiegels, grün für Berge, weiß für Schnee, Farbverlauf je nach Höhe

Ich werde ein komplettes Dokument als Antwort posten, das obiges umsetzt. Hat jemand dazu Verbesserungsvorschläge?

Weitere Herausforderungen:

  • Abgeschnitten auf Meeresspiegelhöhe durch blaue Ebene, vllt. durchsichtig mit opacity damit man die Tiefe dennoch wahrnimmt
  • Der Insel-Ansatz könnte gut aussehen: initialisieren auf Meeresspiegelhöhe des ganzen Randes, so müsste sich eine Insel erheben können, weniger Probleme mit dem Rand
  • Größere Matrix, hier habe ich 128, bei 256 erhalte ich Fehler. Es sollten Zweierpotenzen sein, algorithmusbedingt.

Wer experimentiert, z.B. nur den 3D Viewpoint ändert, kann den seed-Wert zum Initialisieren des Zufallsgenerators beibehalten, ansonsten variieren, bis etwas Schönes herauskommt.

gefragt 29 Jun '14, 18:13

stefan's gravatar image

stefan ♦♦
18.3k163148
Akzeptiert-Rate: 50%


Diese Lösung verwendet den Diamond-square-Algorithmus in einer Implementierung von Marc Lepage. Den Lua-Code kann man natürlich auch auslagern, das ist sogar empfehlenswert. Hier belasse ich ihn der Handhabbarkeit halber im Dokument.

  • Berechnung mit Lua
  • Ausgabe als pgfplots Surface-Plot mit Färbung (Meer, Berge, Schnee) in colormap, view für Ansicht eingestellt

Zum Variieren den seed-Wert ändern, das ist der erste Parameter beim Aufruf der Terrain-Funktion. Festhalten, wenn man nur view ändern möchte. shader=interp würde die Farben interpolieren, es sieht aber auch nicht perfekt aus.

Die Berechnung dauert lange, zum Testen besser kleinere Werte für Matrix-Dimension und mesh rows (Dimension+1) wählen.

Open in writeLaTeX
\documentclass[border=10pt]{standalone}
\usepackage{pgfplots}
\usepackage{luacode}
\begin{luacode*}
  function terrain(seed,dimension,options)
    -- inner functions come from the Heightmap module
    -- Module Copyright (C) 2011 Marc Lepage

    local max, random = math.max, math.random

    -- Find power of two sufficient for size
    local function pot(size)
      local pot = 2
      while true do
        if size <= pot then return pot end
        pot = 2*pot
      end
    end

    -- Create a table with 0 to n zero values
    local function tcreate(n)
      local t = {}
      for i = 0, n do t[i] = 0 end
      return t
    end

    -- Square step
    -- Sets map[x][y] from square of radius d using height function f
    local function square(map, x, y, d, f)
      local sum, num = 0, 0
      if 0 <= x-d then
        if   0 <= y-d   then sum, num = sum + map[x-d][y-d], num + 1 end
        if y+d <= map.h then sum, num = sum + map[x-d][y+d], num + 1 end
      end
      if x+d <= map.w then
        if   0 <= y-d   then sum, num = sum + map[x+d][y-d], num + 1 end
        if y+d <= map.h then sum, num = sum + map[x+d][y+d], num + 1 end
      end
      map[x][y] = f(map, x, y, d, sum/num)
    end

    -- Diamond step
    -- Sets map[x][y] from diamond of radius d using height function f
    local function diamond(map, x, y, d, f)
      local sum, num = 0, 0
      if   0 <= x-d   then sum, num = sum + map[x-d][y], num + 1 end
      if x+d <= map.w then sum, num = sum + map[x+d][y], num + 1 end
      if   0 <= y-d   then sum, num = sum + map[x][y-d], num + 1 end
      if y+d <= map.h then sum, num = sum + map[x][y+d], num + 1 end
      map[x][y] = f(map, x, y, d, sum/num)
    end

    -- Diamond square algorithm generates cloud/plasma fractal heightmap
    -- http://en.wikipedia.org/wiki/Diamond-square_algorithm
    -- Size must be power of two
    -- Height function f must look like f(map, x, y, d, h) and return h'
    local function diamondsquare(size, f)
      -- create map
      local map = { w = size, h = size }
      for c = 0, size do map[c] = tcreate(size) end
      -- seed four corners
      local d = size
      map[0][0] = f(map, 0, 0, d, 0)
      map[0][d] = f(map, 0, d, d, 0)
      map[d][0] = f(map, d, 0, d, 0)
      map[d][d] = f(map, d, d, d, 0)
      d = d/2
      -- perform square and diamond steps
      while 1 <= d do
        for x = d, map.w-1, 2*d do
          for y = d, map.h-1, 2*d do
            square(map, x, y, d, f)
          end
        end
        for x = d, map.w-1, 2*d do
          for y = 0, map.h, 2*d do
            diamond(map, x, y, d, f)
          end
        end
        for x = 0, map.w, 2*d do
          for y = d, map.h-1, 2*d do
            diamond(map, x, y, d, f)
          end
        end
        d = d/2
      end
      return map
    end

    -- Default height function
    -- d is depth (from size to 1 by powers of two)
    -- h is mean height at map[x][y] (from square/diamond of radius d)
    -- returns h' which is used to set map[x][y]
    function defaultf(map, x, y, d, h)
      return h + (random()-0.5)*d
    end

    -- Create a heightmap using the specified height function (or default)
    -- map[x][y] where x from 0 to map.w and y from 0 to map.h
    function create(width, height, f)
      f = f and f or defaultf
      -- make heightmap
      local map = diamondsquare(pot(max(width, height)), f)
      -- clip heightmap to desired size
      for x = 0, map.w do for y = height+1, map.h do map[x][y] = nil end end
      for x = width+1, map.w do map[x] = nil end
      map.w, map.h = width, height
      return map
    end

    -- Initialize pseudo random number generator with seed, to be able to reproduce
    math.randomseed(seed)
    map = create(dimension, dimension)
    if options ~= [[]] then
       tex.sprint("\\addplot3["
         .. options .. "] coordinates{")
    else
      tex.sprint("\\addplot3 coordinates{")
    end
    for x = 0, map.w do
      for y = 0, map.h do
         tex.sprint("("..x..","..y..","..map[x][y]..")")
      end
    end
    tex.sprint("};")
  end
\end{luacode*}
\begin{document}
\begin{tikzpicture}
  \begin{axis}[colormap={terrain}{color(0cm)=(blue!40!black);
    color(1cm)=(blue); color(2cm)=(green!40!black);
    color(4cm)=(green!60!white);color(4cm)=(white!95!black);
    color(8cm)=(white); color(8cm)=(white)},
    hide axis, view = {90}{10}]
    \directlua{terrain(14,128,[[surf,mesh/rows=129,mesh/check=false]])}
  \end{axis}
\end{tikzpicture}
\end{document}

Fraktale Landschaft

Mit shader=interp:

Fraktale Landschaft

seed=10, view={10}{55}

Fraktale Landschaft

Weitere Ideen und Schwierigkeiten stehen ganz oben am Ende der Frage. Verbesserungen und Idee sind sehr willkommen!

Permanenter link

beantwortet 29 Jun '14, 18:28

stefan's gravatar image

stefan ♦♦
18.3k163148
Akzeptiert-Rate: 50%

bearbeitet 29 Jun '14, 18:46

1

Die Farbeinstellungen (color(4cm)=(green!60!white);, color(4cm)=(white!95!black); usw.) sollen ansteigenden Werten sein!

(29 Jun '17, 06:34) Pathe

@Pathe: Bitte auf keinen Fall Blockcode-Markdown in Kommentaren verwenden! Das zerstört leider noch immer die Kommentarfunktion und manchmal die ganze Seite!

(29 Jun '17, 12:48) saputello

@Pathe: In LuaTeX 0.85 wurde pdfpagewidth und diverse andere Primitive, die aus pdfTeX stammen umbenannt. Offenbar sind nicht alle bei Dir installierten Pakete auf einem entsprechend aktuellen Stand. Verwende notfalls das Paket luatex85.

(29 Jun '17, 12:58) saputello

@saputello Ich habe die Kommentar-Frage von @Pathe in eine separate Frage umgewandelt. Dass es schon eine ähnliche Frage gab, sah ich nicht. Es ist auch nicht ganz ein Duplikat, denn statt KOMA-Klasse ist es hier standalone, und es ist eine anders lautende Fehlermeldung. Nur die Lösung ist gleich, wie Du schriebst, luatex85 zu laden.

(29 Jun '17, 13:18) stefan ♦♦

@Pathe Ja, man könnte z.B. mit color(5cm)=... fortsetzen. Mein Augenmerk lag vor allem auf der Kombination pgfplots und Lua zur Darstellung von errechneten Grafiken, nicht ganz auf Farben. :-)

(29 Jun '17, 13:21) stefan ♦♦

Dies hier ist ein Zusatz zu der exzellenten Antwort von Stefan: es verwendet neue Moeglichkeiten von colormaps, was bei Landschaften nicht uninteressant ist:

Öffne in Overleaf
\documentclass[border=10pt]{standalone}

\usepackage{pgfplots}
\pgfplotsset{compat=1.15}
\usepackage{luacode}
\begin{luacode*}
  function terrain(seed,dimension,options)
    -- inner functions come from the Heightmap module
    -- Module Copyright (C) 2011 Marc Lepage

    local max, random = math.max, math.random

    -- Find power of two sufficient for size
    local function pot(size)
      local pot = 2
      while true do
        if size <= pot then return pot end
        pot = 2*pot
      end
    end

    -- Create a table with 0 to n zero values
    local function tcreate(n)
      local t = {}
      for i = 0, n do t[i] = 0 end
      return t
    end

    -- Square step
    -- Sets map[x][y] from square of radius d using height function f
    local function square(map, x, y, d, f)
      local sum, num = 0, 0
      if 0 <= x-d then
        if   0 <= y-d   then sum, num = sum + map[x-d][y-d], num + 1 end
        if y+d <= map.h then sum, num = sum + map[x-d][y+d], num + 1 end
      end
      if x+d <= map.w then
        if   0 <= y-d   then sum, num = sum + map[x+d][y-d], num + 1 end
        if y+d <= map.h then sum, num = sum + map[x+d][y+d], num + 1 end
      end
      map[x][y] = f(map, x, y, d, sum/num)
    end

    -- Diamond step
    -- Sets map[x][y] from diamond of radius d using height function f
    local function diamond(map, x, y, d, f)
      local sum, num = 0, 0
      if   0 <= x-d   then sum, num = sum + map[x-d][y], num + 1 end
      if x+d <= map.w then sum, num = sum + map[x+d][y], num + 1 end
      if   0 <= y-d   then sum, num = sum + map[x][y-d], num + 1 end
      if y+d <= map.h then sum, num = sum + map[x][y+d], num + 1 end
      map[x][y] = f(map, x, y, d, sum/num)
    end

    -- Diamond square algorithm generates cloud/plasma fractal heightmap
    -- http://en.wikipedia.org/wiki/Diamond-square_algorithm
    -- Size must be power of two
    -- Height function f must look like f(map, x, y, d, h) and return h'
    local function diamondsquare(size, f)
      -- create map
      local map = { w = size, h = size }
      for c = 0, size do map[c] = tcreate(size) end
      -- seed four corners
      local d = size
      map[0][0] = f(map, 0, 0, d, 0)
      map[0][d] = f(map, 0, d, d, 0)
      map[d][0] = f(map, d, 0, d, 0)
      map[d][d] = f(map, d, d, d, 0)
      d = d/2
      -- perform square and diamond steps
      while 1 <= d do
        for x = d, map.w-1, 2*d do
          for y = d, map.h-1, 2*d do
            square(map, x, y, d, f)
          end
        end
        for x = d, map.w-1, 2*d do
          for y = 0, map.h, 2*d do
            diamond(map, x, y, d, f)
          end
        end
        for x = 0, map.w, 2*d do
          for y = d, map.h-1, 2*d do
            diamond(map, x, y, d, f)
          end
        end
        d = d/2
      end
      return map
    end

    -- Default height function
    -- d is depth (from size to 1 by powers of two)
    -- h is mean height at map[x][y] (from square/diamond of radius d)
    -- returns h' which is used to set map[x][y]
    function defaultf(map, x, y, d, h)
      return h + (random()-0.5)*d
    end

    -- Create a heightmap using the specified height function (or default)
    -- map[x][y] where x from 0 to map.w and y from 0 to map.h
    function create(width, height, f)
      f = f and f or defaultf
      -- make heightmap
      local map = diamondsquare(pot(max(width, height)), f)
      -- clip heightmap to desired size
      for x = 0, map.w do for y = height+1, map.h do map[x][y] = nil end end
      for x = width+1, map.w do map[x] = nil end
      map.w, map.h = width, height
      return map
    end

    -- Initialize pseudo random number generator with seed, to be able to reproduce
    math.randomseed(seed)
    map = create(dimension, dimension)
    if options ~= [[]] then
       tex.sprint("\\addplot3["
         .. options .. "] coordinates{")
    else
      tex.sprint("\\addplot3 coordinates{")
    end
    for x = 0, map.w do
      for y = 0, map.h do
         tex.sprint("("..x..","..y..","..map[x][y]..")")
      end
    end
    tex.sprint("};")
  end
\end{luacode*}
\begin{document}
\begin{tikzpicture}
  \begin{axis}[
    colormap={whiteblue}{color=(blue) color=(white)},
    colormap={gb}{color=(green) color=(yellow)
        color=(brown)},
    colormap={CM}{
        of colormap={
            whiteblue,
            target pos={-12000,-10000,-6000,-5000,-3000,
                -1000,-750,-500,-250,-100,-50,0},
            sample for=const,
        },
        of colormap={
            gb,
            target pos={10,100,
                200,500,1000,1100,1200,1500,2000,4000,
                6000,8000},
            sample for=const,
        },
    },
    %
    colorbar horizontal,
    colorbar style={minor x tick num=1, colormap access=const},
    hide axis, 
    view={10}{55},
    ]
    \directlua{terrain(14,128,[[
        surf,shader=interp, colormap access=const,
        mesh/rows=129,mesh/check=false]])}
  \end{axis}
\end{tikzpicture}
\end{document}

alt text

Die LUA implementierung ist 100% identisch; ich habe lediglich eine besondere colormap aus dem pgfplots manual (aus der Dokumentation des keys of colormap/target pos max ) eingefuegt und eingebunden. Das besondere hier ist, dass ich die colormap nicht im context eines normalen contour plots verwende (bei dem eine Zuordnung zwischen der Z koordinate und der Farbe wichtig ist), sondern einfach dem shader sage, dass er die resultierende colormap nehmen soll. Die colormap wiederum ist schon ziemlich komplex und auch nicht leicht zu durchschauen:

  • ich definiere zwei colormaps whiteblue fuer das wasser sowie gb fuer das land.
  • Ich baue die final colormap CM als konkatenation zweier of colormap klauseln.
  • Die erste of colormap klausel sampled eine Reihe von Datenpunkten aus whiteblue, naemlich genau an den spezifierten positionen. Damit bekommt man eine asymmetrische verteilung der verschiedenen blautoene hin. Die zahlen sind aus dem manual abgekupfert, weil es dort schick aussieht. Die absoluten Werte spielen eigentlich keine Rolle; wichtig ist nur der Abstand zwischen den Zahlen (bei einem contour plot waeren auch die absoluten Zahlen wichtig -- aber nicht hier).
  • Die zweite of colormap klausel sampled eine Reihe von Datenpunkten aus gb, nur ein kleines bisschen anders verteilt.
Permanenter link

beantwortet 24 Dez '17, 12:20

cfeuersaenger's gravatar image

cfeuersaenger
3.7k23
Akzeptiert-Rate: 34%

Deine Antwort
Vorschau umschalten

Folgen dieser Frage

Per E-Mail:

Wenn sie sich anmelden, kommen Sie für alle Updates hier in Frage

Per RSS:

Antworten

Antworten und Kommentare

Markdown-Grundlagen

  • *kursiv* oder _kursiv_
  • **Fett** oder __Fett__
  • Link:[Text](http://url.com/ "Titel")
  • Bild?![alt Text](/path/img.jpg "Titel")
  • nummerierte Liste: 1. Foo 2. Bar
  • zum Hinzufügen ein Zeilenumbruchs fügen Sie einfach zwei Leerzeichen an die Stelle an der die neue Linie sein soll.
  • grundlegende HTML-Tags werden ebenfalls unterstützt

Frage-Themen:

×298
×28
×5

gestellte Frage: 29 Jun '14, 18:13

Frage wurde gesehen: 15,730 Mal

zuletzt geändert: 24 Dez '17, 12:20