Kamera und Ghost

Autor: Leonard Pieper

Die Kamera

Damit alles korrekt auf dem Bildschirm angezeigt wird, musste zu Anfang des Projekts eine Kamera implementiert werden. Diese speichert sich ihre eigene Position, die Kugelkoordinaten Theta und Phi (die angeben, in welche Richtung geschaut wird), das Seitenverhältnis (aspect_ratio) und die Projektions-Matrix. Die Kamera besitzt verschiedene Funktionen, die essentiell für viele Berechnungen anderer Gruppen sind. So wird zum Beispiel durch Anwendung von sin und cos auf die Kugelkoordinaten Theta und Phi der look_at_vector berechnet, und wenn dieser nun auf die aktuelle Position addiert wird, ist das Ergebnis der look_at_point.

 pub fn get_look_at_vector(&self) -> Vector3f {
        Vector3f::new(self.theta.sin() * self.phi.cos(),
                      self.theta.sin() * self.phi.sin(),
                      self.theta.cos())
}

  pub fn get_look_at_point(&self) -> Point3f {
        self.position + self.get_look_at_vector()
}

Außerdem können die Projektions- und die View-Matrix per getter-Methoden ausgegeben, und die Projektions-Matrix per setter-Methode gesetzt werden.

Nun soll die Kamera auch bewegt werden können. Dafür gibt es eine übergeordnete Methode move_by, die dann wiederum von mehreren untergeordneten Methoden aufgerufen wird. Bei dieser wird eine pos_diff auf die aktuelle position addiert und somit die Kamera bewegt. Essentiell soll die Kamera neben allen Himmelsrichtungen auch nach oben und nach unten bewegt werden können. Zudem soll sie auch in jede beliebige Richtung gedreht werden können, um sich umzuschauen.

 pub fn move_by(&mut self, pos_diff: Vector3f) {
        self.position += pos_diff;
}

Da wir uns, egal in welchem Winkel wir nach oben beziehungsweise unten schauen, gleich schnell in alle Richtungen bewegen wollen, wird bei den Methoden move_forward und move_backward zunächst die z-Koordinate des look_at_vector auf 0 gesetzt und dieser anschließend normalisiert. Nun haben wir einen 2-D-Vektor der Länge 1, den wir mit einem factor, der jeder Methode übergeben wird (dieser gibt die Geschwindigkeit an, mit der wir uns bewegen), multiplizieren und danach der Methode move_by übergeben.

 pub fn move_forward(&mut self, factor: f32) {
        let mut lookatvector = self.get_look_at_vector();
        lookatvector.z = 0.0;
        lookatvector = lookatvector.normalize();
        lookatvector *= factor;
        self.move_by(lookatvector);
}

Um die Kamera nach links beziehungsweise rechts zu bewegen wird sehr ähnlich vorgegangen, nur dass nun aus den normalisierten x-y-Koordinaten des look-at-vector ein neuer Vektor erstellt wird, der 90 Grad nach links beziehungsweise rechts von der Blickrichtung zeigt. Dann wird wieder move_by mit dem Vektor, der zuvor noch mit dem Geschwindigkeits-Faktor multipliziert wurde, aufgerufen.

  pub fn move_left(&mut self, factor: f32) {
        let mut lookatvector = self.get_look_at_vector();
        lookatvector.z = 0.0;
        lookatvector = lookatvector.normalize();
        // Get the orthogonal 2d-vector, which is 90 degrees to the left
        let mut move_dir = Vector3f::new(-lookatvector.y, lookatvector.x, 0.0);
        move_dir *= factor;
        self.move_by(move_dir);
}

Bei move_up beziehungsweise move_down wird move_by mit einem Vektor dessen x- und y-Koordinaten 0, und dessen z-Koordinate der factor ist, aufgerufen.

pub fn move_up(&mut self, factor: f32) {
        self.move_by(Vector3f::new(0.0, 0.0, factor));
}

Die Methode change_dir wird, wie der Name vermuten lässt, genutzt, um die Richtung, in die die Kamera schaut, zu ändern. Dies geschieht durch Übergabe von theta_diff und phi_diff, die die Änderung von Theta und Phi angeben und auf die aktuellen Werte von Theta und Phi addiert werden. Da wir vorher das Problem hatten, dass sich, wenn wir nach ganz oben beziehungsweise unten geschaut haben, die Kamera nach hinten gedreht hat, haben wir eine Fallunterscheidung eingebaut. Da Theta zwischen 0 und Pi liegt, wobei 0 oben und Pi unten ist, addieren wir, wenn Theta schon sehr klein ist ( < 0.1 ), nur noch positive Theta-Werte, damit wir sozusagen nicht höher schauen können als ein bestimmter Winkel. Analog wird dies für unten gemacht.

  pub fn change_dir(&mut self, theta_diff: f32, phi_diff: f32) {
        if self.theta < 0.1 {
            if theta_diff > 0.0 {
                // all the way UP, theta will not go any lower
                self.theta += theta_diff;
            }
        } else if self.theta > consts::PI - 0.1 {
            if theta_diff < 0.0 {
                // all the way DOWN, theta will not go any higher
                self.theta += theta_diff;
            }
        } else {
            self.theta += theta_diff;
        }
        self.phi += phi_diff;
}

Der Ghost

Zum Debuggen und zur schnellen Fortbewegung haben wir einen „Ghost“ implementiert. Dieser kann sich frei in der Welt bewegen und auch durch jegliche Hindernisse hindurch fliegen. Der Ghost reagiert auf festgelegte Tastenanschläge und bewegt dadurch entsprechend die Kamera. Dafür haben wir das Trait EventHandler für den Ghost implementiert. Im EventHandler kann man auf bestimmte Events, wie z.B. Tastenanschläge, reagieren. Es ist möglich zwischen verschiedenen ElementStates zu unterscheiden, so wird beim Pressen der W-Taste die boolsche Variable forward auf true, und beim Loslassen der Taste diejenige Variable wieder auf false gesetzt.

Event::KeyboardInput(ElementState::Pressed, _, Some(VirtualKeyCode::W)) => {
                self.forward = true;
                EventResponse::Continue
            }
            Event::KeyboardInput(ElementState::Released, _, Some(VirtualKeyCode::W)) => {
                self.forward = false;
                EventResponse::Continue
}

Dies geschieht für jede Taste, der eine Richtung zugewiesen wurde, sodass man sich mit W, A, S, D nach vorne, hinten, links und rechts und mit Space und Strg nach oben beziehungsweise unten bewegen kann. Außerdem wurde mit Shift eine Taste zur Regulierung der Geschwindigkeit implementiert, sodass man sich beim Pressen und Halten von Shift schneller bewegt, um sich beispielsweise schneller in verschiedene Biome zu begeben.

  Event::KeyboardInput(ElementState::Pressed, _, Some(VirtualKeyCode::LShift)) => {
                self.speed = SHIFT_SPEED;
                EventResponse::Continue
            }
            Event::KeyboardInput(ElementState::Released, _, Some(VirtualKeyCode::LShift)) => {
                self.speed = DEFAULT_SPEED;
                EventResponse::Continue
}

Die gesetzten boolschen Werte werden dann in einer update-Methode des Ghost abgefragt und je nachdem, ob das Flag gesetzt wurde, oder nicht, wird die Kamera in die jeweilige Richtung bewegt.

 pub fn update(&mut self, delta: f32) {
        let factored_speed = self.speed * delta;
        if self.forward {
            self.cam.move_forward(factored_speed);
        }
        // [...]
}

Durch die Lösung mit boolschen Flags wird ein anfängliches Problem umgangen, wobei man beim Pressen und Halten der z.B. W-Taste einen Schritt vorwärts gegangen ist, es dann eine kurze Verzögerung gab und man dann erst „richtig“ losgelaufen ist, vergleichbar zu dem Halten einer Taste in einem Texteditor. Davor war es auch nur möglich sich in eine Richtung gleichzeitig zu bewegen, nun kann man auch schräg fliegen. Ein weiteres Problem gab es bei der Maus. Der CursorState Grab, welcher den Cursor, ähnlich eines Ego-Shooters, verstecken sollte, funktioniert nicht, also haben wir uns diesen selbst gebaut. Beim einmaligen Klicken in das Fenster wird die Maus versteckt, bei erneutem Klicken wird sie wieder sichtbar. Wenn die Maus nun (nur wenn sie versteckt ist) bewegt wird, wird aus der inner_size des window die Mitte und dann die Differenz der aktuellen Mausposition zu der Mitte berechnet. Um diese Differenz wird dann die Kamera gedreht und anschließend wird die Maus wieder in die Mitte des Bildschirms gesetzt.

Event::MouseInput(ElementState::Pressed, MouseButton::Left) => {
                if !self.mouselock {
                    self.mouselock = true;
                    if let Some(window) = self.context.get_facade().get_window() {
                        window.set_cursor_state(CursorState::Hide)
                            .expect("failed to set cursor state");
                    } else {
                        warn!("Failed to obtain window from facade");
                    }
                } else if self.mouselock {
                    self.mouselock = false;

                    if let Some(window) = self.context.get_facade().get_window() {
                        window.set_cursor_state(CursorState::Normal)
                            .expect("failed to set cursor state");
                    } else {
                        warn!("Failed to obtain window from facade");
                    }
                }

                EventResponse::Continue
}
 Event::MouseMoved(x, y) => {
                if self.mouselock {
                    if let Some(window) = self.context.get_facade().get_window() {
                        if let Some(middle) = window.get_inner_size_pixels() {
                            let middle_x = (middle.0 as i32) / 2;
                            let middle_y = (middle.1 as i32) / 2;
                            let x_diff = x - middle_x;
                            let y_diff = y - middle_y;
                            self.cam.change_dir(y_diff as f32 / 300.0, -x_diff as f32 / 300.0);
                            window.set_cursor_position(middle_x as i32, middle_y as i32)
                                .expect("setting cursor position failed");
                        }
                    }
                }
                EventResponse::Continue
}

So kann man sich frei durch die Welt bewegen, mit einer Kameraführung wie in einem Ego-Shooter.

results matching ""

    No results matching ""