Seine Geschwindigkeit und Hardwarenähe prädestinieren Rust für 3D-Anwendungen und Spiele. Die 3D-Visualisierung übernimmt die Bibliothek Bevy, um die Animation kümmert sich die Physik-Library Rapier.
Der vorletzte Artikel dieser Serie, “Rust Wars” [1], hat gezeigt, wie einfach es ist, mit der Bibliothek Bevy [2] 3D-Figuren auf den Bildschirm zu bringen. Wenn Sie zudem etwas Bewegung in die 3D-Elemente bringen wollen, gibt es grundsätzlich zwei Möglichkeiten: Entweder Sie berechnen für jedes Bild die entsprechende Veränderung des Standorts der Figur, oder Sie lassen das eine spezielle Rust-Bibliothek automatisch simulieren.
Diese Module versuchen, unsere physische Welt möglichst einfach nachzubilden. Beispiel: Zu einer Kugel fügen Sie deren Gewicht und einen Wert für deren Elastizität hinzu. Daraus wird ein Ball, der realistisch vom Boden abprallt. Aber keine Angst, Sie brauchen nichts von physikalischen Formeln zu verstehen, das übernimmt netterweise die Physikbibliothek. Davon gibt es einige für Rust. Ich verwende in diesem Artikel die freie Rust-Library Rapier [3]. Diese können Sie in 2D und 3D verwenden (Abbildung 1). Darüber hinaus gibt es für die 3D-Bibliothek Bevy eine speziell abgestimmte Version.
Bewegter Ball
Wie alle neuen Bibliotheken fügen Sie Rapier-3D als Erstes in der Datei »Cargo.toml« im aktuellen Projekt hinzu (Listing 1). Mit der Einstellung »debug-render« erzeugt Rapier auf Wunsch zusätzliche Darstellungen, die beim Debuggen nützlich sind. Dazu später mehr.
Listing 1
Cargo.toml
bevy_rapier3d = { version = "*", features = [ "debug-render" ] }
Damit es losgehen kann, binden Sie im Programm selbst die Bibliothek Rapier ein (Listing 2, erste Zeile). Zusätzliche Verarbeitungsschritte, in diesem Fall die Simulation von Bewegungen, fügen Sie über Plugins hinzu (Zeile 3 und 4). Für Rapier verwenden Sie die Erweiterung »RapierPhysicsPlugin«. Sie könnten sie mit eigenen zusätzlichen Daten (»UserData«) erweitern, was aber nur im Ausnahmefall notwendig ist.
Listing 2
Plugins einbinden
use bevy_rapier3d::prelude::*;
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(RapierPhysicsPlugin::<NoUserData>::default())
commands.spawn((
Mesh3d( meshes.add(Mesh::from(Sphere{ radius:0.5 }))),
RigidBody::Dynamic, ... )
Im nächsten Schritt versehen Sie die 3D-Figuren mit zusätzlichen Informationen für die physikalische Simulation. Die Bibliothek Bevy arbeitet mit dem Konzept Entity-Component-System (ECS). Eine Kugel ist in diesem Fall eine Entität (Entity), deren Eigenschaften Sie durch beliebige Komponenten (Components) erweitern.
Beim Erzeugen der Kugel (»commands.spawn«, ab Zeile 5) kommt für die Simulation die Komponente »RigidBody::Dynamic« hinzu. Damit weiß die Bibliothek Rapier, dass sie die 3D-Figur in die Simulation einbeziehen soll. »RigidBody« (Festkörper) heißt, dass es sich bei der Kugel um einen Gegenstand handelt, der seine Form nicht verändert. Das genaue Gegenteil, ein »SoftBody«, wäre zum Beispiel eine Fahne aus Stoff, die im Wind flattert. Die Bibliothek Rapier konzentriert sich hauptsächlich auf »RigidBody«s.
Neben »RigidBody::Dynamic« gibt es noch »RigidBody::Fixed«. Solche fixen 3D-Figuren, zum Beispiel eine Bodenplatte, berücksichtigt die Simulation zwar, sie bewegen sich aber selbst nicht. Figuren, die das Rust-Programm selbst steuern möchte, aber auf die andere Figuren in der Simulation trotzdem reagieren sollen, haben den Typ (»RigidBodyType«) »KinematicPositionBased« oder »KinematicVelocityBased«. Beim ersten (»PositionBased«) verändert Rust in jedem Frame die aktuelle Position, beim zweiten (»VelocityBased«) die Geschwindigkeit.
Schwerkraft und Masse
Die Schwerkraft (»Gravity«) berücksichtigt Rapier automatisch. Das betrifft alle 3D-Körper, außer denen mit einem »RigidBodyType« von »fixed« oder »kinematic«. Manchmal ist es praktisch, wenn einzelne 3D-Figuren in Spielen schneller fallen als laut Simulation berechnet. Dafür erhöhen Sie deren Faktor für die Schwerkraft (»GravityScale«) beispielsweise von »1.0« auf »2.0« (Listing 3, erste zwei Zeilen). Dann wirkt auf diese 3D-Figur die doppelte Schwerkraft.
Entscheidend für die Bewegung einer Figur ist außerdem deren Masse (»mass«). Die ermittelt die Bibliothek Rapier automatisch aus der Geometrie und der Dichte des Materials, aus dem die Figur besteht (Zeilen 3 bis 5).
Listing 3
Schwerkraft und Masse
commands.spawn((..., GravityScale(2.0)) commands.spawn((... Collider::ball(0.5), ColliderMassProperties::Density(1.2)));
Die von Rapier für die Simulation verwendete Geometrie (»Collider«) muss nicht dieselbe sein, die Bevy auf dem Bildschirm darstellt. Bei komplexen 3D-Körpern wie Spielfiguren ist es häufig sinnvoll, sie zu vereinfachen. Je nach Anwendung genügt manchmal ein Quader für den Körper einer Spielfigur. Wer es genauer haben will, fügt zusätzliche Quader für Arme, Beine und Kopf hinzu. Je weniger respektive einfachere Geometrien Rapier bei seiner Berechnung verwendet, desto schneller läuft das Ganze.
Rapier bietet bei den Collidern Standard-3D-Figuren wie Kugeln, Quader, Zylinder und so weiter an. Darüber hinaus ist es möglich, aus diesen Figuren neue zusammenzubauen oder eigene über Dreiecks-Meshes zu definieren. Für die Kugel im Beispiel genügt der Collider »ball«.
Ein weiterer Faktor für die Masse der 3D-Figur ist deren Dichte (»Density«). Sie brauchen sich nicht allzu viele Gedanken über das Material zu machen, aus dem die Figur bestehen könnte. Die Standarddichte ist »1.0«. Soll der aktuelle Körper schwerer sein, erhöhen Sie diesen Faktor einfach auf »2.0« oder mehr. Hier sollten Sie etwas experimentieren, bis die Bewegung so ausfällt, wie Sie sich das vorstellen.
Physikalische Eigenschaften
Wie elastisch der Ball sein soll, stellen Sie bei der Definition der 3D-Figur über den »Restitution«-Koeffizienten ein (Listing 4, erste Zeile). Ist der »1.0«, springt der Ball in etwa an die Stelle zurück, von der er gestartet ist. Bei einem Koeffizienten von »0.0« bleibt er beim Kontakt mit der Grundplatte einfach liegen. Bei Werten dazwischen, wie »0.9«, erreicht der Ball nur 90 Prozent der Höhe, die er vorher hatte, und bleibt dadurch nach einigen Sprüngen liegen.
Listing 4
Physikalische Eigenschaften
Restitution::coefficient(0.9)
Damping {
linear_damping: 0.0,
angular_damping: 0.0,
}
Die Dämpfung (»Damping«) ist ein weiteres Attribut, das eine 3D-Figur immer langsamer werden lässt. Beim Ball in unserem Spiel ist eine Dämpfung nicht notwendig. Interessanter ist es bei Quadern, die über die Grundplatte gleiten: Soll der Quader immer weiter rutschen oder irgendwann einmal liegen bleiben?
Dazu schießt einem gleich der Begriff Reibung durch den Kopf. Wie stark eine Figur gleiten soll, legen Sie mit dem Attribut »Damping« fest (Listing 4, ab Zeile 2). Beim Standardwert »0.0« gibt es keine Dämpfung. Je höher der Wert ausfällt, desto stärker wird die Figur abgebremst. Rapier unterscheidet dabei die Parameter »angular_damping« für die Dämpfung von Drehbewegungen und »linear_damping« für alle anderen Bewegungen.
Debugging
Nur eine Kugel auf einer Grundplatte – da ist langweilig. Hundert oder mehr Kugeln auf einer schrägen Ebene, zudem mit Hindernissen, machen mehr Spaß. In Japan kennt man dieses Spiel unter dem Namen Pachinko und setzt es beim Glücksspiel ein. Als ich das Programm dafür erstellt habe, wollten die Kugeln aber nicht durch die Hindernisse rollen und verhielten sich ansonsten ebenfalls etwas eigenartig.
In solchen Fällen können Sie ein zusätzliches Plugin für Rapier hinzufügen (Listing 5, Zeile 15). Es zeigt zum Beispiel an, wie eine 3D-Figur ausgerichtet ist oder welcher Collider für Berechnungen definiert wird. Durch das Debug-Plugin habe ich auf einen Blick gesehen, dass der Collider für die Kugeln nicht den Radius »0.5« sondern »5.0« hatte (Abbildung 3).
Listing 5
rapier_minimal.rs
use bevy::prelude::*;
use bevy::color::palettes::css::{LIGHT_GRAY,BLUE};
use bevy_rapier3d::prelude::*;
#[derive(Component)]
struct Ball {
initial_position: Vec3,
}
#[derive(Resource)]
struct ResetTimer(Timer);
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(RapierPhysicsPlugin::<NoUserData>::default())
// .add_plugins(RapierDebugRenderPlugin::default()) for debug
.insert_resource(ResetTimer(Timer::from_seconds(4.0,TimerMode::Repeating)))
.add_systems(Startup, setup)
.add_systems(Update, auto_reset_ball)
.run();
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>
) {
/* Camera */
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-3.0, 3.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y)
));
/* Light */
commands.spawn((
DirectionalLight {
shadows_enabled: true,
..default()
},
Transform {
translation: Vec3::new(0.0,
4.0, 0.0),
rotation: Quat::from_rotation_x(-std::f32::consts::FRAC_PI_4),
..default()
}
));
/* Ground */
commands.spawn((
Mesh3d::from(meshes.add(Mesh::from(Cuboid::new(100.0, 0.1, 100.0)))),
MeshMaterial3d(materials.add(Color::Srgba(LIGHT_GRAY))),
RigidBody::Fixed,
Collider::cuboid(100.0, 0.1, 100.0),
));
/* Ball */
let ball_position = Vec3::new(0.0, 4.0, 0.0);
commands.spawn((
Mesh3d( meshes.add(Mesh::from(Sphere{ radius:0.5 }))),
MeshMaterial3d(materials.add(Color::Srgba(BLUE))),
Transform::from_translation(ball_position),
RigidBody::Dynamic,
Collider::ball(0.5),
ColliderMassProperties::Density(1.2),
Restitution::coefficient(0.9),
Damping {
linear_damping: 0.0,
angular_damping: 0.0,
},
Ball { initial_position: ball_position }
));
}
fn auto_reset_ball(
mut ball_query: Query<(&Ball, &mut Transform)>,
time:Res<Time>,
mut reset_timer: ResMut<ResetTimer>,
) {
if reset_timer.0.tick(time.delta()).just_finished() {
for (ball, mut transform) in ball_query.iter_mut() {
// reset position
transform.translation = ball.initial_position;
}
}
}
Kraft und Impuls
In den bisherigen Beispielen lief die Rapier-Simulation ohne jeden Einfluss durch den Anwender ab. Das nächste Rust-Programm ist ein einfaches Spiel, bei dem man die Kugel bewusst steuert, sodass sie das Ziel in der Mitte der Bahn berührt und bis zum Ende weiterläuft (Abbildung 4).
Eine Möglichkeit wäre es, den Typ der Kugel als »kinematic« zu definieren. Dann würde das Rust-Programm alle Berechnungen komplett übernehmen. Das ist allerdings aufwendig. Es gibt eine alternative Herangehensweise, bei der eine Physikbibliothek wie Rapier den Großteil der Arbeit übernimmt.
Im Detail: Der Anwender soll mit den Pfeiltasten die Kugel nach links oder rechts beziehungsweise nach hinten oder vorn bewegen können. Die Umsetzung dafür: Soll die Kugel nach links rollen, erhält sie rechts einen Stups, soll sie nach rechts, dann links einen Stups und so weiter.
In der Physik gibt es allerdings keinen Stups, sondern Kräfte und Impulse. Aus technischer Sicht wirken Kräfte auf die Geschwindigkeit eines Körpers und Impulse auf die Beschleunigung. Das klingt sehr abstrakt. Wenn Sie es ausprobieren, merken Sie aber sofort den Unterschied: Kräfte wirken kurz, Impulse über eine längere Zeit.
Bleiben wir bei den Kräften: Die Kugel ist vom Typ her als »dynamic« definiert. In der Funktion »input_user()« (Listing 6) überprüft das Rust-Programm bei jedem Bildaufbau (Frame), ob eine Pfeiltaste gedrückt wurde. Wurde die Pfeiltaste nach rechts gedrückt, dann benötigt man eine Kraft nach rechts. Eine Kraft ist ein 3D-Vektor, der die Richtung angibt. Je höher dessen Wert, desto stärker ist die Kraft.
Die Methode »Vec3::new« erzeugt in Zeile 21 des Listings einen 3D-Vektor mit den entsprechenden Werten für »x«, »y« und »z«. Im Beispiel steht die x-Richtung für links und rechts. Hat sie einen positiven Wert, geht es nach rechts, bei negativen Werten nach links. Diesen Vektor weisen Sie der Komponente »ExternalForces« der 3D-Figur zu. Wie bei allen Komponenten erhalten Sie mit Query einen Zugriff darauf (Zeile 20). Damit der Körper allerdings diese Komponente hat, ist sie bei dessen Erstellung zu definieren (Zeile 3).
Listing 6
input_user()
fn input_user(
keyboard_input:Res<ButtonInput<KeyCode>>,
mut query_forces: Query<&mut ExternalForce>,
){
let x = if keyboard_input.pressed(KeyCode::ArrowLeft) {
-SPEED
} else if keyboard_input.pressed(KeyCode::ArrowRight) {
SPEED
} else {
0.0
};
let z = if keyboard_input.pressed(KeyCode::ArrowUp) {
-SPEED
} else if keyboard_input.pressed(KeyCode::ArrowDown) {
SPEED
} else {
0.0
};
if x != 0.0 || z != 0.0 {
for mut ext_force in query_forces.iter_mut() {
ext_force.force = Vec3::new(x,0.0, z);
}
}
}
Kollision
Trifft die Kugel das Ziel, also den Zylinder, muss das Programm das mitbekommen und darauf reagieren. Die Bibliothek Rapier erzeugt deshalb bei jeder Kollision ein Event »CollisionEvent«. Das Programm kann es über den »EventReader« auslesen (Listing 7, Zeilen 2 und 8).
Grundsätzlich gilt es dabei, Folgendes zu unterscheiden: Wenn eine Kollision beginnt, gibt es ein Ereignis, und wenn sie endet, gibt es ein zweites (Zeile 10). Im aktuellen Beispiel interessiert nur der Beginn der Kollision. Die Variablen »e1« und »e2« enthalten die Kennung für die beteiligten 3D-Figuren (Entitäten). Ist eine davon der Ball und die andere das Ziel, reagiert das Programm darauf.
Listing 7
Kollisionserkennung
fn collision(
mut collision_events: EventReader<CollisionEvent>,
query_ball: Query<Entity, With<Ball>>,
query_cheese: Query<(Entity, &Transform), With<Cheese>>,
mut commands: Commands
){
let entity_ball = query_ball.single();
for e in collision_events.read(){
match e {
CollisionEvent::Started(e1,e2,_) => {
if e1 == &entity_ball || e2 == &entity_ball {
for (entity_cheese, cheese_transform) in query_cheese.iter(){
if e1 == &entity_cheese || e2 == &entity_cheese {
commands.entity(entity_cheese).despawn_recursive();
}
}
}
}
CollisionEvent::Stopped(_,_,_)=> {}
}
}
}
Fazit
Den Quellcode aller hier besprochenen Programme finden Sie auf Github [4]. Disclaimer für alle Physikerinnen und Physiker: Rapier ist eine einfache und dadurch schnelle Nachbildung von Bewegungen, entspricht aber keiner Simulation, wie sie in Technik und Forschung Anwendung findet. Die Bibliothek bietet neben den hier gezeigten Funktionen zusätzlich die Simulation von Gelenken sowie einen soliden Character Controller zur Steuerung von Spielfiguren an. (jlu)
Infos
- Planet Rust, Folge 10: Gerhard Völkl, “Rust Wars”, LM 03/2025, S. 78, https://www.lm-online.de/51794
- Bevy: https://bevyengine.org
- Rapier: https://rapier.rs
- Quellcodes zum Artikel: https://github.com/Rust-Ninja-Sabi/bevy-nature-of-code






