Skip to main content

gltf_step_anim/
gltf_step_anim.rs

1use quaso::{
2    GameLauncher,
3    animation::gltf::{
4        GltfAnimationTarget, GltfAnimationTransition, GltfAnimationTransitionController,
5        GltfAnimationTransitionLayer, GltfNodeId, GltfRenderablesOptions, GltfSceneAnimation,
6        GltfSceneAttribute, GltfSceneInstance, GltfSceneInstantiateOptions, GltfSceneRenderable,
7        GltfSceneRenderables, GltfSceneTemplate,
8    },
9    assets::{make_directory_database, shader::ShaderAsset},
10    config::Config,
11    context::GameContext,
12    coroutine::{async_game_context, async_wait_for_asset},
13    game::{GameInstance, GameState, GameStateChange},
14    third_party::{
15        keket::database::AssetDatabase,
16        nodio::{AnyIndex, graph::Graph, query::Related},
17        spitfire_core::Triangle,
18        spitfire_draw::utils::{Drawable, ShaderRef, Vertex},
19        spitfire_glow::{
20            graphics::{CameraScaling, Shader},
21            renderer::GlowBlending,
22        },
23        spitfire_input::{
24            InputActionRef, InputConsume, InputMapping, VirtualAction, VirtualKeyCode,
25        },
26        vek::{Aabr, Mat4, Vec2, Vec3},
27    },
28};
29use serde_json::Value;
30use std::{collections::HashMap, error::Error, ops::Range, pin::Pin};
31
32const HURTBOX_COLOR: [f32; 4] = [0.5, 0.5, 1.0, 0.5];
33const HITBOX_COLOR: [f32; 4] = [1.0, 0.0, 0.0, 0.5];
34const STEP_DELTA_TIME: f32 = 0.1;
35
36struct HitBox;
37struct HurtBox;
38
39fn main() -> Result<(), Box<dyn Error>> {
40    GameLauncher::new(GameInstance::new(Preloader).setup_assets(|assets| {
41        *assets = make_directory_database("./resources/").unwrap();
42    }))
43    .title("GLTF - Step animation")
44    .config(Config::load_from_file("./resources/GameConfig.toml")?)
45    .run();
46    Ok(())
47}
48
49#[derive(Default)]
50struct Preloader;
51
52impl GameState for Preloader {
53    fn timeline(
54        &mut self,
55        context: GameContext,
56    ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> {
57        context.graphics.state.color = [0.2, 0.2, 0.2, 1.0];
58        context.graphics.state.main_camera.screen_alignment = [0.5, 0.8].into();
59        context.graphics.state.main_camera.scaling = CameraScaling::FitVertical(1.5);
60        context.graphics.state.main_camera.transform.scale = Vec3::new(1.0, -1.0, 1.0);
61
62        context
63            .assets
64            .spawn(
65                "shader://color",
66                (ShaderAsset::new(
67                    Shader::COLORED_VERTEX_2D,
68                    Shader::PASS_FRAGMENT,
69                ),),
70            )
71            .unwrap();
72        context
73            .assets
74            .spawn(
75                "shader://image",
76                (ShaderAsset::new(
77                    Shader::TEXTURED_VERTEX_2D,
78                    Shader::TEXTURED_FRAGMENT,
79                ),),
80            )
81            .unwrap();
82        context
83            .assets
84            .spawn(
85                "shader://text",
86                (ShaderAsset::new(Shader::TEXT_VERTEX, Shader::TEXT_FRAGMENT),),
87            )
88            .unwrap();
89
90        let handle = context.assets.ensure("gltf://stickman.glb?binary").unwrap();
91
92        Box::pin(async move {
93            println!("Waiting for GLTF asset to load...");
94            async_wait_for_asset(handle).await;
95            println!("GLTF asset loaded.");
96
97            {
98                let context = async_game_context().await.unwrap();
99                for handle in handle.dependencies(context.assets) {
100                    println!(
101                        "Dependency: {}",
102                        handle.path(context.assets).unwrap().content()
103                    );
104                }
105            }
106
107            {
108                let context = async_game_context().await.unwrap();
109                let scene = context
110                    .assets
111                    .find("gltf-scene://stickman.glb/Scene")
112                    .unwrap();
113
114                let controller = GltfAnimationTransitionController::default();
115                let instance = scene
116                    .access::<&GltfSceneTemplate>(context.assets)
117                    .instantiate_with_options(
118                        context.assets,
119                        &GltfSceneInstantiateOptions::default().extract_extras(extract_extras),
120                    )
121                    .with_animation(
122                        "idle",
123                        GltfSceneAnimation::new(
124                            context
125                                .assets
126                                .find("gltf-anim://stickman.glb/TPose")
127                                .unwrap(),
128                            context.assets,
129                        )
130                        .unwrap()
131                        .weight(0.0)
132                        .playing(true)
133                        .looped(true),
134                    )
135                    .with_animation(
136                        "walk",
137                        GltfSceneAnimation::new(
138                            context
139                                .assets
140                                .find("gltf-anim://stickman.glb/Walk")
141                                .unwrap(),
142                            context.assets,
143                        )
144                        .unwrap()
145                        .weight(0.0)
146                        .playing(true)
147                        .looped(true),
148                    )
149                    .with_animation(
150                        "run",
151                        GltfSceneAnimation::new(
152                            context.assets.find("gltf-anim://stickman.glb/Run").unwrap(),
153                            context.assets,
154                        )
155                        .unwrap()
156                        .weight(0.0)
157                        .playing(true)
158                        .looped(true),
159                    )
160                    .with_animation_node(
161                        GltfAnimationTransition::new(controller.clone())
162                            .default_layer("idle")
163                            .layer(GltfAnimationTransitionLayer::new(
164                                "idle",
165                                GltfAnimationTarget::new("idle"),
166                            ))
167                            .layer(GltfAnimationTransitionLayer::new(
168                                "walk",
169                                GltfAnimationTarget::new("walk"),
170                            ))
171                            .layer(GltfAnimationTransitionLayer::new(
172                                "run",
173                                GltfAnimationTarget::new("run"),
174                            )),
175                    );
176
177                instance.visit_tree(&mut |level, index, id, name, transform, mesh, skin, bone| {
178                    println!(
179                        "{}Node {} | id: {} | name: {} | transform: {} | mesh: {} | skin: {} | bone: {}",
180                        "  ".repeat(level),
181                        index,
182                        id,
183                        name.map(|n| n.as_str()).unwrap_or("<unnamed>"),
184                        transform.is_some(),
185                        mesh.is_some(),
186                        skin.is_some(),
187                        bone.is_some(),
188                    );
189                    true
190                });
191                *context.state_change = GameStateChange::Swap(Box::new(State {
192                    controller,
193                    instance,
194                    idle: InputActionRef::default(),
195                    walk: InputActionRef::default(),
196                    run: InputActionRef::default(),
197                    toggle: InputActionRef::default(),
198                    prev: InputActionRef::default(),
199                    next: InputActionRef::default(),
200                    playing: true,
201                }));
202            }
203        })
204    }
205}
206
207struct State {
208    controller: GltfAnimationTransitionController,
209    instance: GltfSceneInstance,
210    idle: InputActionRef,
211    walk: InputActionRef,
212    run: InputActionRef,
213    toggle: InputActionRef,
214    prev: InputActionRef,
215    next: InputActionRef,
216    playing: bool,
217}
218
219impl GameState for State {
220    fn enter(&mut self, context: GameContext) {
221        context.input.push_mapping(
222            InputMapping::default()
223                .consume(InputConsume::Hit)
224                .action(
225                    VirtualAction::KeyButton(VirtualKeyCode::Key1),
226                    self.idle.clone(),
227                )
228                .action(
229                    VirtualAction::KeyButton(VirtualKeyCode::Key2),
230                    self.walk.clone(),
231                )
232                .action(
233                    VirtualAction::KeyButton(VirtualKeyCode::Key3),
234                    self.run.clone(),
235                )
236                .action(
237                    VirtualAction::KeyButton(VirtualKeyCode::W),
238                    self.toggle.clone(),
239                )
240                .action(
241                    VirtualAction::KeyButton(VirtualKeyCode::Q),
242                    self.prev.clone(),
243                )
244                .action(
245                    VirtualAction::KeyButton(VirtualKeyCode::E),
246                    self.next.clone(),
247                ),
248        );
249    }
250
251    fn exit(&mut self, context: GameContext) {
252        context.input.pop_mapping();
253    }
254
255    fn fixed_update(&mut self, context: GameContext, delta_time: f32) {
256        if self.idle.get().is_pressed() {
257            self.controller.change_to(["idle"]);
258        } else if self.walk.get().is_pressed() {
259            self.controller.change_to(["walk"]);
260        } else if self.run.get().is_pressed() {
261            self.controller.change_to(["run"]);
262        } else if self.toggle.get().is_pressed() {
263            self.playing = !self.playing;
264            for (_, animation) in self.instance.animations() {
265                if let Some(mut animation) = animation.write() {
266                    animation.playing = self.playing;
267                }
268            }
269        } else if self.prev.get().is_pressed() {
270            for (_, animation) in self.instance.animations() {
271                if let Some(mut animation) = animation.write() {
272                    animation.time -= STEP_DELTA_TIME;
273                    animation.sanitize_time();
274                }
275            }
276        } else if self.next.get().is_pressed() {
277            for (_, animation) in self.instance.animations() {
278                if let Some(mut animation) = animation.write() {
279                    animation.time += STEP_DELTA_TIME;
280                    animation.sanitize_time();
281                }
282            }
283        }
284
285        if self.playing {
286            self.instance
287                .update_and_apply_animations(delta_time, context.assets);
288        } else {
289            self.instance
290                .update_and_apply_animations(0.0, context.assets);
291        }
292    }
293
294    fn draw(&mut self, context: GameContext) {
295        let renderables = self
296            .instance
297            .build_renderables(
298                context.assets,
299                &GltfRenderablesOptions::default()
300                    .sort_triangles_by_max_positive_z()
301                    .sort_renderables_by_max_positive_z()
302                    .renderable_modifier(renderable_modifier)
303                    .custom_renderables(custom_renderables)
304                    .axes([0, 2]),
305            )
306            .unwrap();
307        renderables.draw(context.draw, context.graphics);
308    }
309}
310
311fn extract_extras(value: &Value, graph: &mut Graph, index: AnyIndex) {
312    if let Some(boxtype) = value.get("boxtype") {
313        match boxtype.as_str() {
314            Some("hurt") => {
315                let attr = graph.insert(HurtBox);
316                graph.relate::<GltfSceneAttribute>(index, attr);
317            }
318            Some("hit") => {
319                let attr = graph.insert(HitBox);
320                graph.relate::<GltfSceneAttribute>(index, attr);
321            }
322            _ => {}
323        }
324    }
325}
326
327fn renderable_modifier(graph: &Graph, index: AnyIndex, renderable: &mut GltfSceneRenderable) {
328    if graph
329        .query::<Related<GltfSceneAttribute, &HitBox>>(index)
330        .next()
331        .is_some()
332    {
333        renderable.shader = Some(ShaderRef::name("color"));
334        renderable.main_texture = None;
335        renderable.blending = GlowBlending::Alpha;
336        for vertex in &mut renderable.vertices {
337            vertex.color = HITBOX_COLOR;
338            vertex.uv = [0.0, 0.0, 0.0];
339        }
340    }
341
342    if graph
343        .query::<Related<GltfSceneAttribute, &HurtBox>>(index)
344        .next()
345        .is_some()
346    {
347        renderable.shader = Some(ShaderRef::name("color"));
348        renderable.main_texture = None;
349        renderable.blending = GlowBlending::Alpha;
350        for vertex in &mut renderable.vertices {
351            vertex.color = HURTBOX_COLOR;
352            vertex.uv = [0.0, 0.0, 0.0];
353        }
354    }
355}
356
357fn custom_renderables(
358    graph: &Graph,
359    index: AnyIndex,
360    _: &AssetDatabase,
361    _: &GltfRenderablesOptions,
362    _: &HashMap<GltfNodeId, Mat4<f32>>,
363    renderables: &mut GltfSceneRenderables,
364    range: Range<usize>,
365) -> Result<(), Box<dyn Error>> {
366    if graph
367        .query::<Related<GltfSceneAttribute, &HitBox>>(index)
368        .next()
369        .is_some()
370    {
371        custom_renderable(renderables, range.clone(), HITBOX_COLOR);
372    }
373
374    if graph
375        .query::<Related<GltfSceneAttribute, &HurtBox>>(index)
376        .next()
377        .is_some()
378    {
379        custom_renderable(renderables, range, HURTBOX_COLOR);
380    }
381
382    Ok(())
383}
384
385fn custom_renderable(renderables: &mut GltfSceneRenderables, range: Range<usize>, color: [f32; 4]) {
386    let aabr = renderables.renderables[range.clone()]
387        .iter()
388        .flat_map(|renderable| {
389            renderable
390                .vertices
391                .iter()
392                .map(|vertex| Vec2::from(vertex.position))
393        })
394        .fold(Option::<Aabr<f32>>::None, |aabr, position| {
395            if let Some(aabr) = aabr {
396                Some(aabr.expanded_to_contain_point(position))
397            } else {
398                Some(Aabr::new_empty(position))
399            }
400        });
401
402    if let Some(aabr) = aabr {
403        renderables.renderables.push(GltfSceneRenderable {
404            shader: Some(ShaderRef::name("color")),
405            main_texture: None,
406            blending: GlowBlending::Alpha,
407            wireframe: true,
408            triangles: vec![Triangle { a: 0, b: 1, c: 2 }, Triangle { a: 2, b: 3, c: 0 }],
409            vertices: vec![
410                Vertex {
411                    position: [aabr.min.x, aabr.min.y],
412                    uv: [0.0, 0.0, 0.0],
413                    color,
414                },
415                Vertex {
416                    position: [aabr.max.x, aabr.min.y],
417                    uv: [0.0, 0.0, 0.0],
418                    color,
419                },
420                Vertex {
421                    position: [aabr.max.x, aabr.max.y],
422                    uv: [0.0, 0.0, 0.0],
423                    color,
424                },
425                Vertex {
426                    position: [aabr.min.x, aabr.max.y],
427                    uv: [0.0, 0.0, 0.0],
428                    color,
429                },
430            ],
431        });
432    }
433}