Generierung der Pflanzen

Autor: Karoline Plum

Interne Repräsentation einer Pflanze

Intern wird eine Pflanze bei uns einfach als eine Liste der ihr zugehörigen Äste gespeichert:

pub struct Tree {
    pub branches: Vec<Branch>,
    pub trunk_color: Vector3f,
    pub leaf_color: Vector3f,
}

Zusätzlich werden Stammfarbe und Blattfarbe gemerkt, wobei es je nach Pflanzenart unterschiedlich ist, welche Äste zum Stamm gezählt werden und welche zu den Blättern gezählt werden. Diese Information wird in den Ästen selbst abgespeichert:

pub struct Branch {
    pub points: Vec<ControlPoints>,
    pub is_trunk: bool,
}

Ein boolsches Flag kennzeichet, ob die Stamm- oder die Astfarbe verwendet werden soll. Desweiteren besteht ein Ast aus einer Liste von Kontrollpunkten, entlang derer er verläuft, wobei jeder Kontrollpunkt nicht nur aus seinen Modellkoordinaten besteht, sondern außerdem den Durchmesser des Astes an diesem Punkt speichert.

pub struct ControlPoint {
    pub point: Point3f,
    pub diameter: f32,
}

Der Baumgenerator

Der Baumgenerator kann Ausprägungen acht verschiedener Pflanzentypen generieren:

pub enum PlantType {
  WitheredTree,
  Shrub,
  Cactus,
  JungleTree,
  ClumpOfGrass,
  Conifer,
  OakTree,
  Flower,
}

Je nach Pflanzentyp bekommt der Baumgenerator (das struct Treegen) im Konstruktor unterschiedliche Standardparameter übergeben.

Die meisten Standardparameter sind Wertebereiche, aus denen mit Hilfe eines RNG beim Generieren einer Pflanze immer wieder gesamplet wird, sodass effektiv jede Pflanze und oft auch jeder einzelne Ast ausgehend von unterschiedlichen Parametern aufgebaut wird. Solche Wertebereiche-Parameter sind z.B.

trunk_diameter: Range<f32> unterer Stammdurchmesser
trunk_diameter_top: Range<f32> oberer Stammdurchmesser
trunk_height: Range<f32> Stammhöhe
min_branch_height: Range<f32> Höhe, ab der Äste wachsen können
branch_diameter_factor: Range<f32> das Verhältnis, in dem der Durchmesser eines Astes zum Durchmesser seines Vater-Astes steht
branch_diam_reduction: Range<f32> das Verhältnis, in dem der Durchmesser eines Kontrollpunktes zum Durchmesser des vorhergehenden Kontrollpunkts des Astes steht
branch_segment_length: Range<f32> und branch_segment_length2: Range<f32> das Verhältnis, in dem die Abstände der Kontrollpunkte entlang des Astes zum Astdurchmesser stehen (zwei verschiedene Paramter je nach Rekursionstiefe, da bei höherer Rekursionstiefe die Astsegmente bei gleichbleibendem Durchmesser länger sein müssen)
branch_segment_count: Range<u32> die Anzahl an Kontrollpunkten pro Ast
branch_angle_deg: Range<f32> der Winkel, in dem die Äste vom Vater-Ast abgehen
branch_segment_angle: Range<f32> der Winkel, in dem sich ein einzelner Ast entlang seiner Kontrollpunkte biegen kann
trunk_color: (Range<f32>,Range<f32>,<Rangef32>) Stammfarbe
leaf_color: (Range<f32>,Range<f32>,<Rangef32>) Blattfarbe

Dann gibt es noch drei Paramter, die keine Wertebereiche darstellen:

branch_chance: f32 wie viele neue Äste an einem Kontrollpunkt durchschnittlich entstehen
leaf_depth: u16 die Rekursionstiefe, ab der nicht mehr die Stammfarbe, sondern nun die Blattfarbe übergeben werden soll
height_branchlength_dependence: fn(f32) → f32 das Verhältnis zwischen Asthöhe und Astlänge

So zeichnet zum Beispiel Kakteen aus, dass die Stacheln entlang des kompletten „Stammes“ wachsen können (min_branch_height: 0.05..0.1), die Äste sehr dünn sind (branch_diameter_factor: 0.3..0.4), in einem 90°-Winkel gerade vom Stamm abgehen (branch_angle_deg: 90.0..90.00001) und sich entlang der Kontrollpunkte nicht biegen (branch_segment_angle: 0.0..0.00001).

Typisch für das Grasbüschel sind hingegen eine extrem große Anzahl an abgehenden Grashalmen (branch_chance: 12.0) und extremere Winkel, in denen die Halme wuchern (branch_angle_deg: 60.0..100.0 und branch_segment_angle: 25.0..30.0).

Tannenbaum und Laubbaum zeichnen sich durch ein besonderes Verhältnis zwischen Asthöhe und Astlänge aus.

Beim Laubbaum ist die übergebene Funktion quadratisch, sodass der Baum in der Mitte die längsten Äste hat und am oberen und unteren Ende der Baumkrone etwas kürzere:

height_branchlength_dependence: { 
  fn f(height: f32) -> f32 { 
    let mut result = 25.0 - 7.6 * (height - 4.5) * (height - 4.5); 
    if result <= 0.0 { 
      result = 0.1; 
    } 
    result 
  } 
  f 
  },

Der Tannenbaum bekommt als Parameter andererseits eine lineare Funktion übergeben, sodass die Äste nach oben hin immer kürzer werden:

height_branchlength_dependence: {
    fn f(heigt: f32) → f32 {
      1.00.125 * height
    }
    f
}

Bei allen anderen Pflanzentypen besteht keine Relation zwischen Asthöhe und Astlänge und die Funktion liefert konstant 1.

Wenn eine neue Pflanze mit den übergebenen Parametern erstellt werden soll, wird in create_trunk der Stamm zunächst in Segmente aufgeteilt und an jedem Segmentübergang wird beliebig oft (abhängig vom Zufallsgenerator und der branch_chance) die rekursive Funkion create_branch aufgerufen, die sich entlang der Kontrollpunkte des Astes selber wieder mit einer gewissen Wahrscheinlichkeit aufruft. create_branch bekommt unter anderem die Rekursionstiefe als Parameter übergeben, damit bei einer Tiefe von 4 ein Rekursionsabbruch erfolgen kann, da ab dann leider die generierten Pflanzen so viele Vertices haben, dass das ganze Spiel nicht mehr startet. Abschliessend wird noch der Stamm in die Liste der Äste eingefügt. Damit das obere Ende des Stammes kein glatter Zylinder bleibt, werden zwei weitere Kontrollpunkte mit kleinerem Durchmesser hinzugefüt, sodass der Stamm nun oben mehr oder weniger spitz zuläuft.

Erzeugung verschiedener Pflanzeninstanzen

Zuerst sahen alle Pflanzen in unserer Welt unterschiedlich aus und wurden beim Laden jedes Chunks immer neu generiert. Beim Rendern musste für jede Pflanze ein komplett neuer Vertex-Buffer erzeugt werden. Um die Performance des Spiels zu verbessern, entschieden wir uns dann dazu, das Konzept des Geometry Instancing auch auf die Pflanzen anzuwenden. Deshalb gibt es nun in jeder Welt je Pflanzentyp nur 5 verschiedene Ausprägungen, also insgesamt 40 verschiedene Pflanzen. Die Ausprägungen variieren jedoch weiterhin je nach Seed der Welt.

Der WorldGenerator erzeugt nun einmalig die Liste der 40 Pflanzen, die auf unserer Welt wachsen können, indem er für jeden Pflanzentyp einen Treegen erstellt und diesen fünf mal aufruft. Wenn durch den WorldGenerator während des Spiels ein neuer Chunk geladen wird, wird auf den betroffenen HexPillars lediglich der Index der dort wachsenden Pflanze in der globalen Pflanzenliste abgespeichert.

Dieses System ermöglicht es, nur eine plant_view pro Pflanzenausprägung erzeugen zu müssen, da die Modellkoordinaten für alle Instanzen dieselben sind, und nur die Position im Raum für jede Pflanze abweicht.

results matching ""

    No results matching ""