React + Rust + Wasm: Play an Animated 3D Model

Nikhil Gupta
3 min readDec 14, 2022

In this article, we will use Bevy game engine in our React application from our Rust WASM library to play an animated 3D model. We will build on the previous tutorial available here.

Run a Bevy App using Rust

Last time, we used a setup_audio function with our Bevy app to play an audio file. Now, let's modify our lib.rs to create a setup_animation function that will load the model and another play_animation that actually plays the animation.

Update the setup function

#[wasm_bindgen]
pub fn run_bevy_app() {
App::new()
.add_plugins(DefaultPlugins)
.add_startup_system(setup_animation) # Defined below
.add_system(play_animation) # Define below and called once scene is loaded
.run();
}

Define setup_animation

fn setup_animation(
mut commands: Commands,
asset_server: Res<AssetServer>
) {
# ...
}

Declare a struct to load the animation clips

#[derive(Resource)]
struct Animations(Vec<Handle<AnimationClip>>);

Please note #[derive(Resource)] that is needed in the loader below.

Read the animations

fn setup_animation(
mut commands: Commands,
asset_server: Res<AssetServer>
) {
# Please ensure that Fox.glb is added to public/assets/models/animated folder
commands.insert_resource(Animations(vec![
asset_server.load("models/animated/Fox.glb#Animation2"),
asset_server.load("models/animated/Fox.glb#Animation1"),
asset_server.load("models/animated/Fox.glb#Animation0"),
]));
}

Setup the camera

fn setup_animation(
mut commands: Commands,
asset_server: Res<AssetServer>
) {
# ...
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(100.0, 100.0, 150.0)
.looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
..default()
});
}

Load the scene

fn setup_animation(
mut commands: Commands,
asset_server: Res<AssetServer>
) {
# ...
# Please ensure that Fox.glb is added to public/assets/models/animated folder
commands.spawn(SceneBundle {
scene: asset_server.load("models/animated/Fox.glb#Scene0"),
..default()
});
}

Define the function to actually play the animation

fn play_animation(
animations: Res<Animations>,
mut player: Query<&mut AnimationPlayer>,
mut done: Local<bool>,
) {
# ...
}

Use Bevy’s AnimationPlayer

fn play_animation(
animations: Res<Animations>,
mut player: Query<&mut AnimationPlayer>,
mut done: Local<bool>,
) {
if !*done {
if let Ok(mut player) = player.get_single_mut() {
player.play(animations.0[0].clone_weak()).repeat();
*done = true;
}
}
}

The code basically triggers a play command for the first animation in a loop.

Build the new wasm library

Let’s run wasm-pack again to build the updated library

wasm-pack build --target web

Call the function from the demo app

Finally, let’s add a button to load the wasm and call run_bevy_app function from our App.ts file like so:

// App.ts

import init, { run_bevy_app } from "rust-wasm-lib";
import './App.css';
function App() {
const runBevyApp = async () => {
await init();
run_bevy_app();
};

return (
<div className="App">
<button onClick={runBevyApp}>Run Bevy App</button>
</div>
);
}
export default App;

Now, if you run the updated app and click on Run Bevy App, you should see an animated fox on the canvas :)

If you liked this article, subscribe here to get the complete code and updates for the entire collection:

--

--