Wetter und Wind

Author: Niklas Rothe

Das Wetter

Das Wetter für Plantex wird in einem Wetterobjekt gespeichert, dass zu Spielanfang erzeugt wird und im Laufe dessen verschiedene Funktionen übernimmt. Dabei wird jedoch nicht bei jedem Wetterwechsel ein neues Wetter erzeugt, sondern das aktuelle Wetter wird schlicht und einfach verändert. Folgende Informationen werden dabei im Wetter gespeichert:

pub struct Weather {
    particles: Vec<Particle>,
    program: Program,
    indices: glium::index::NoIndices,
    context: Rc<GameContext>,
    camera: Camera,
    actual_buf: VertexBuffer<Instance>,
    vertex_buffer: VertexBuffer<Vertex>,
    form: Form,
    strength: Strength,
    wind: Vector2f,
    wind_speed: f32,
    delta_time: f32,
    weather_time: f32,
    last_biome: biome::Biome,
    change: bool,
    sun_color: Vector3f,
    sky_light: Vector3f,
}

Viele der gespeicherten Daten dienen dabei vor allem dem berechnen der Wettereffekte, die immer wieder benötigt werden. Wichtig für das eigentliche Wetter ist vor allem der particles Vektor, der eine Liste aller aktiven Wetterteilchen enthält, die Form des Wetters, die strength, sowie der Wind, welcher durch einen Richtungsvektor wind und eine Geschwindigkeitsangabe wind_speed gespeichert wird.

Partikelsystemgrundlagen

Realisiert wird das Wettersystem durch ein verhältnismäßig simpel gehaltenes Partikelsystem, bei dem je nach Ausprägung des Wetters unterschiedlich viele Partikel erstellt und verarbeitet werden, wobei sich sowohl Fallgeschwindigkeit, wie auch Richtung, aber auch Beeinflussung durch den Wind und ähnliches je nach Ausprägung unterscheiden können.

Grundlage des Partikelsystems sind zwei Dreiecke, die durch Triangle Strips zu einem Viereck verbunden werden. Aus diesem wiederum werden im Shader Kreise erzeugt, indem nur die Punkte angezeigt werden, die einen gewissen Abstand (entspricht dem Radius des Kreises) von einem Mittelpunkt haben. Dieser Grundlegende Punkt wird dann vervielfältigt in der Welt gerendert um einen Wettereffekt zu erzeugen.

Die Partikel

Die Partikel werden durch einen eigenen Struct erstellt, der ebenso wie das Wetter relevante Informationen für den tatsächlichen Partikel, sowie einige zur Berechnung des Verhaltens genutzte Variablen enthält:

pub struct Particle {
    position: Point3<f32>,
    velocity: f32,
    trans_x: f32,
    trans_y: f32,
    trans_z: f32,
    lifetime: f32,
}

Gespeichert sind hier die aktuelle position des Partikels, die ständig abhängig von Form, Wind und einigen zufälligen Faktoren verändert wird, die velocity, die die Eigengeschwindigkeit des Partikels enthält, sowie seine lifetime, die vor allem für einen dynamischen Übergang der Wettereffekte relevant ist. Um beim Rendern der einzelnen Partikel Zeit zu sparen, wird ein weiterer Struct mit dem Namen Instance genutzt, der nur die Position des Partikels enthält. Durch Instancing ist es nun möglich alle Partikel auf einmal an die Shader zu übergeben und somit einige Aufrufe zu sparen.

Zudem ist für die Partikel eine Methode in_cave implementiert, die es ermöglicht zu überprüfen, ob sich ein bestimmter Partikel innerhalb einer Höhle befindet. Dies ermöglicht es die Wettereffekte nur dort darzustellen, wo sie auch in der Realität sichtbar wären. Gleichzeitig wird somit auch noch die Performance verbessert, da weniger Partikel gerendert werden müssen, was noch dadurch unterstützt wird, das auch Partikel, die sich hinter dem Spieler befinden nicht dargestellt werden.

if !particle.in_cave(&world_manager) &&
               ((particle.position - camera.position).dot(camera.get_look_at_vector())) > 0.0 {
                    tmp_instances.push(Instance {
                        position: [particle.position.x, particle.position.y, particle.position.z],
                    })
}

Billboarding

Um aus der 2-D Struktur der Partikel nun die Illusion entstehen zu lassen es handele sich tatsächlich um 3 Dimensionale Objekte, wird ein relativ simples Billboarding genutzt.

        view[0][0] = 1.0;
        view[0][1] = 0.0;
        view[0][2] = 0.0;

        view[1][0] = 0.0;
        view[1][1] = 0.0;
        view[1][2] = 1.0;

        view[2][0] = 0.0;
        view[2][1] = 1.0;
        view[2][2] = 0.0;

Wie der Code zeigt wird schlicht und einfach ein Teil der Model-View-Matrix, genauer gesagt die 9 inneren Elemente durch eine Einheitsmatrix ersetzt. Dies führt dazu, dass die Rotation der Kamera beim Rendering nicht beachtet wird, wodurch die Fläche der einzelnen Partikel immer in Richtung der Kamera ausgerichtet ist. Zu erwähnen ist hierbei noch, dass es hier je nach Ausprägung leichte Unterschiede darin gibt, welcher Teil durch eine Einheitsmatrix ersetzt werden muss, was dem leicht unterschiedlichen Aussehen der Partikel geschuldet ist.

Realitätsnahe Simulation

Da eine vollständige Simulation des Wetters im Sinne der Performance vollkommen unmöglich ist, muss eine Möglichkeit gefunden werden mit möglichst wenig Partikeln eine möglichst hohe Realitätsdichte zu erzeugen. Der erste Schritt dazu ist das Umfeld zu begrenzen, in dem überhaupt Partikel erstellt werden sollen. Dazu wird um den Spieler ein imaginärer Zylinder erzeugt. In diesem können sich die Partikel bewegen und werden, wenn sie den Zylinder verlassen an der gegenüberliegenden Stelle wieder eingesetzt, wodurch um den Spieler herum immer ausreichend Partikel zur Verfügung stehen um die Illusion zu erzeugen dieses Wetter sei im kompletten Sichtfeld vorhanden.

            if len > BOX_SIZE - size || len < -BOX_SIZE - size {
                particle.position.x = particle.position.x - (1.95 * tmp.x);
                particle.position.y = particle.position.y - (1.95 * tmp.y);
            }

            if particle.position.z < self.camera.position.z - BOX_SIZE / size {
                particle.position.z += 1.9 * (BOX_SIZE / size);
            }

            if particle.position.z > self.camera.position.z + BOX_SIZE / size {
                particle.position.z -= 1.9 * (BOX_SIZE / size);
            }

Weiterhin ist es notwendig zu gewährleisten, dass die Partikel in einer scheinbar zufälligen Anordnung erscheinen und sich dabei bereits im Zylinder befinden.

while !spawn {
            radius = rand::random::<f32>();

            let dice = rand::random::<f32>();

            spawn = dice < radius;
        }

radius = radius * BOX_SIZE;

let angle = Rad::new(rand::random::<f32>() * PI * 2.0);
position: Point3::new(cam_pos.x + radius * Rad::sin(angle),
                      cam_pos.y + radius * Rad::cos(angle),
                      cam_pos.z + (BOX_SIZE * rand::random::<f32>() + 0.5))

Dazu werden, wie im Code gut zu sehen der x und y Wert durch einen zufälligen Radius auf einen bestimmten Abstand zum Spieler und dann auf einen zufälligen Punkt des Kreises gesetzt, der genau diesen Abstand hat. Dies führt jedoch dazu, dass sich im inneren mehr Partikel befinden als weiter außen, was der Tatsache geschuldet ist, dass weiter innen liegende Radien zwar genau so viele Partikel abbekommen wie äußere, jedoch einen viel kleineren Kreis bilden auf dem diese tatsächlich erscheinen können.

Dieses Problem wird durch die while-Schleife behoben, in der ein weiterer zufälliger Wert generiert wird, mit dem die Wahrscheinlichkeit, dass ein Partikel tatsächlich entsteht verringert wird, je näher er sich am Mittelpunkt befindet. Dadurch entsteht eine fast natürlich Verteilung, in der weder Muster, noch größere Ballungspunkte zu erkennen sind.

Author: Marten Mildt

Ausprägungen

Im Wettersystem können drei verschiedene Formen (in Abhängigkeit des aktuellen Bioms) von Wetter auftreten:

  • Schnee
  • Regen
  • Pollen

Diese Unterscheidung wird in Form eines Enums repräsentiert

pub enum Form {
    Rain = 1,
    Snow = 2,
    Pollen = 3,
}

und im struct "Weather" gespeichert.

pub struct Weather {
    ...
    form: Form,
    ...
}

In der Update-Methode von Weather wird zunächst mittels eines "match" die Unterscheidung des aktuellen Bioms vorgenommen (welches an jedem Pillar hängt und daher einfach über die aktuelle Kameraposition abgefragt werden kann).

match *biome {
                    biome::Biome::GrassLand => ...
                    biome::Biome::Desert => ...
                    biome::Biome::Snow => ...
                    biome::Biome::Forest => ...
                    biome::Biome::RainForest => ...
                    biome::Biome::Savanna => ...
                    biome::Biome::Stone => ...
                    biome::Biome::Debug => (),
}

In diesem match wird, je nach Biom, die Änderung der Form (des Wetters) zusammen mit einer gewissen Warscheinlichkeit für die Stärke gesetzt. D.h. im Regenwald gibt es Regen, im Schneegebiet Schnee, in der Savanna Pollen usw. Die Wetter-Stärke wird dabei auch wieder als Enum kodiert.

pub enum Strength {
    None = 0,
    Weak = 1,
    Medium = 2,
    Heavy = 3,
}

Für eine Änderung des Wetters von z.B. Regen auf Schnee wird die change-Flag (change: bool) auf true gesetzt und eine Verzögerung von bis zu 5 Sekunden eingebaut, um einen weichen, nicht abrupten Wetterwechsel zu gewährleisten. Ist dieses Flag gesetzt, werden zunächst alle Partikel nach und nach aus dem Buffer entfernt, bevor neue Partikel des aktuellen Wetters entstehen.

Beispielcode für das Schnee-Biom:

biome::Biome::Snow => {
    if (self.form == Form::Pollen || self.form == Form::Rain) &&
       self.particles.len() > 0 {
        self.change = true;
        return;
    }
    self.form = Form::Snow;
    self.last_biome = biome::Biome::Snow;
    self.strength = match chance {
        0.0...20.0 => Strength::Weak,
        20.0...50.0 => Strength::Medium,
        50.0...75.0 => Strength::Heavy,
        _ => Strength::None,
    };
}

Hier wird zunächst das change-Flag auf true gesetzt, sollten noch Pollen oder Regen aktiv sein. Dann wird die Form des Wetters auf Snow gesetzt, sowie die Stärke mit den gegebenen Warscheinlichkeiten:

  • 20% schwacher Schneefall
  • 30% normalerstarker Schneefall
  • 25% starker Schneefall
  • 25% kein Schneefall

Diese Wetteränderung wird, abgesehen von einem Biomwechsel, alle 120 Sekunden (zwei Minuten) neu ausgewürfelt.

Ist dieser Schritt vollzogen, wird die Bewegung der Partikel je nach Form aktualisiert. Dabei fällt Regen einfach nur stumpf nach unten (jedes Partikel mit einer leicht variierenden Geschwindigkeit), Schneepartikel hingegen bewegen sich zusätzlich auf ihrer X- und Y-Achse auf einer Sinus- bzw. Cosinuskurve (mit Noise) um einem realistischen Schneefall deutlich näher zu kommen. Pollen bewegen sich wie Schnee, allerdings nicht einfach nur nach unten, sondern ihre Z-Koordinate wird ebenfalls von einer Sinuskurve bestimmt.

match self.form {
    Form::Snow => ...
    Form::Rain => ...
    Form::Pollen => ...
}

In der Draw-Methode wird ebenfalls wieder zwischen den drei verschiedenen Wettertypen unterschieden, um dem Shader jeweils eigene Skalierungsmatrizen und Farbwerte zur verfügung zu stellen. Dabei werden Schnee und Pollen auf allen Achsen auf 5% runterskaliert, während Regen in der Z-Achse nur auf 50% runterskaliert. Dies ergibt bei Regen einen langgezogenen Kreis und die Illusion eines Striches (schnell fallender Regentropfen). Diese Matrizen werden im Vertexshader auf jeden Vertex aufskaliert.

Wind

Die Windrichtung (wind: Vector2f) ist ein für alle Partikel geltender (in diesem Sinne globaler) 2-Dimensionaler Vektor, der in Zusammenhang mit der Windstärke (wind_speed: f32) den Wind repräsentiert. Die Windrichtung und Windstärke werden alle 90 Sekunden angepasst (ausgewürfelt). Dabei wird die Änderung (positiv oder negativ) aufaddiert, sodass ein plötzlicher (in Nullzeit stattfindender) Windwechsel um z.B. 180° nicht möglich ist. Der mit der Windstärke skalierte Windvektor wird bei der Kalkulation der Partikelposition einfach aufaddiert.

Beispiel Regen:

Form::Rain => {
    particle.position.x += ((self.wind.x * self.wind_speed) * delta) * 2.0;
    particle.position.y += ((self.wind.y * self.wind_speed) * delta) * 2.0;
    particle.position.z = particle.position.z -
                          ((particle.velocity + 0.5) * 50.0 * delta)
}

results matching ""

    No results matching ""