mirror of
https://github.com/FyroxEngine/Fyrox.git
synced 2026-10-12 02:02:20 +00:00
391 lines
13 KiB
Rust
391 lines
13 KiB
Rust
//! Example 02. Asynchronous scene loading.
|
|
//!
|
|
//! Difficulty: Medium.
|
|
//!
|
|
//! This example shows how to load scene in separate thread and how create standard
|
|
//! loading screen which will show progress.
|
|
|
|
pub mod shared;
|
|
|
|
use fyrox::resource::model::{Model, ModelResourceExtension};
|
|
use fyrox::{
|
|
asset::manager::ResourceManager,
|
|
core::{
|
|
algebra::{UnitQuaternion, Vector2, Vector3},
|
|
color::Color,
|
|
futures,
|
|
pool::Handle,
|
|
},
|
|
engine::{executor::Executor, GraphicsContext, GraphicsContextParams},
|
|
event::{ElementState, Event, VirtualKeyCode, WindowEvent},
|
|
event_loop::ControlFlow,
|
|
gui::{
|
|
grid::{Column, GridBuilder, Row},
|
|
message::MessageDirection,
|
|
progress_bar::{ProgressBarBuilder, ProgressBarMessage},
|
|
text::{TextBuilder, TextMessage},
|
|
widget::{WidgetBuilder, WidgetMessage},
|
|
BuildContext, HorizontalAlignment, Thickness, UiNode, VerticalAlignment,
|
|
},
|
|
plugin::{Plugin, PluginConstructor, PluginContext},
|
|
scene::{node::Node, Scene},
|
|
window::WindowAttributes,
|
|
};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use crate::shared::create_camera;
|
|
|
|
struct Interface {
|
|
root: Handle<UiNode>,
|
|
debug_text: Handle<UiNode>,
|
|
progress_bar: Handle<UiNode>,
|
|
progress_text: Handle<UiNode>,
|
|
}
|
|
|
|
fn create_ui(ctx: &mut BuildContext, screen_size: Vector2<f32>) -> Interface {
|
|
let debug_text;
|
|
let progress_bar;
|
|
let progress_text;
|
|
let root = GridBuilder::new(
|
|
WidgetBuilder::new()
|
|
.with_width(screen_size.x)
|
|
.with_height(screen_size.y)
|
|
.with_child({
|
|
debug_text =
|
|
TextBuilder::new(WidgetBuilder::new().on_row(0).on_column(0)).build(ctx);
|
|
debug_text
|
|
})
|
|
.with_child({
|
|
progress_bar =
|
|
ProgressBarBuilder::new(WidgetBuilder::new().on_row(1).on_column(1)).build(ctx);
|
|
progress_bar
|
|
})
|
|
.with_child({
|
|
progress_text = TextBuilder::new(
|
|
WidgetBuilder::new()
|
|
.on_column(1)
|
|
.on_row(0)
|
|
.with_margin(Thickness::bottom(20.0))
|
|
.with_vertical_alignment(VerticalAlignment::Bottom),
|
|
)
|
|
.with_horizontal_text_alignment(HorizontalAlignment::Center)
|
|
.build(ctx);
|
|
progress_text
|
|
}),
|
|
)
|
|
.add_row(Row::stretch())
|
|
.add_row(Row::strict(30.0))
|
|
.add_row(Row::stretch())
|
|
.add_column(Column::stretch())
|
|
.add_column(Column::strict(200.0))
|
|
.add_column(Column::stretch())
|
|
.build(ctx);
|
|
|
|
Interface {
|
|
root,
|
|
debug_text,
|
|
progress_bar,
|
|
progress_text,
|
|
}
|
|
}
|
|
|
|
struct SceneLoader {
|
|
scene: Scene,
|
|
model_handle: Handle<Node>,
|
|
}
|
|
|
|
impl SceneLoader {
|
|
async fn load_with(resource_manager: ResourceManager, context: Arc<Mutex<AsyncLoaderContext>>) {
|
|
let mut scene = Scene::new();
|
|
|
|
// Set ambient light.
|
|
scene.ambient_lighting_color = Color::opaque(200, 200, 200);
|
|
|
|
// It is important to lock context for short period of time so other thread can
|
|
// read data from it as soon as possible - not when everything was loaded.
|
|
context
|
|
.lock()
|
|
.unwrap()
|
|
.report_progress(0.0, "Creating camera...");
|
|
|
|
// Camera is our eyes in the world - you won't see anything without it.
|
|
create_camera(
|
|
resource_manager.clone(),
|
|
Vector3::new(0.0, 6.0, -12.0),
|
|
&mut scene.graph,
|
|
)
|
|
.await;
|
|
|
|
context
|
|
.lock()
|
|
.unwrap()
|
|
.report_progress(0.33, "Loading model...");
|
|
|
|
// Load model resource. Is does *not* adds anything to our scene - it just loads a
|
|
// resource then can be used later on to instantiate models from it on scene. Why
|
|
// loading of resource is separated from instantiation? Because there it is too
|
|
// inefficient to load a resource every time you trying to create instance of it -
|
|
// much more efficient is to load it one and then make copies of it. In case of
|
|
// models it is very efficient because single vertex and index buffer can be used
|
|
// for all models instances, so memory footprint on GPU will be lower.
|
|
let model_resource = resource_manager
|
|
.request::<Model, _>("examples/data/mutant/mutant.FBX")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Instantiate model on scene - but only geometry, without any animations.
|
|
// Instantiation is a process of embedding model resource data in desired scene.
|
|
let model_handle = model_resource.instantiate(&mut scene);
|
|
|
|
// Now we have whole sub-graph instantiated, we can start modifying model instance.
|
|
scene.graph[model_handle]
|
|
.local_transform_mut()
|
|
// Our model is too big, fix it by scale.
|
|
.set_scale(Vector3::new(0.05, 0.05, 0.05));
|
|
|
|
context
|
|
.lock()
|
|
.unwrap()
|
|
.report_progress(0.66, "Loading animation...");
|
|
|
|
// Add simple animation for our model. Animations are loaded from model resources -
|
|
// this is because animation is a set of skeleton bones with their own transforms.
|
|
let walk_animation_resource = resource_manager
|
|
.request::<Model, _>("examples/data/mutant/walk.fbx")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Once animation resource is loaded it must be re-targeted to our model instance.
|
|
// Why? Because animation in *resource* uses information about *resource* bones,
|
|
// not model instance bones, retarget_animations maps animations of each bone on
|
|
// model instance so animation will know about nodes it should operate on.
|
|
walk_animation_resource.retarget_animations(model_handle, &mut scene.graph);
|
|
|
|
context.lock().unwrap().report_progress(1.0, "Done");
|
|
|
|
context.lock().unwrap().data = Some(Self {
|
|
scene,
|
|
model_handle,
|
|
});
|
|
}
|
|
}
|
|
|
|
struct AsyncLoaderContext {
|
|
data: Option<SceneLoader>,
|
|
message: String,
|
|
progress: f32,
|
|
}
|
|
|
|
impl AsyncLoaderContext {
|
|
fn report_progress(&mut self, progress: f32, message: &str) {
|
|
self.progress = progress;
|
|
self.message = message.to_owned();
|
|
println!("Loading progress: {}% - {}", progress * 100.0, message);
|
|
}
|
|
|
|
fn load_with(resource_manager: ResourceManager) -> Arc<Mutex<Self>> {
|
|
// Create load context - it will be shared with caller and loader threads.
|
|
let context = Arc::new(Mutex::new(Self {
|
|
data: None,
|
|
message: "Starting..".to_string(),
|
|
progress: 0.0,
|
|
}));
|
|
let result = context.clone();
|
|
|
|
// Spawn separate thread which will create scene by loading various assets.
|
|
std::thread::spawn(move || {
|
|
// Scene will be loaded in separate thread.
|
|
futures::executor::block_on(SceneLoader::load_with(resource_manager, context))
|
|
});
|
|
|
|
// Immediately return shared context.
|
|
result
|
|
}
|
|
}
|
|
|
|
struct InputController {
|
|
rotate_left: bool,
|
|
rotate_right: bool,
|
|
}
|
|
|
|
struct GameScene {
|
|
scene: Handle<Scene>,
|
|
model_handle: Handle<Node>,
|
|
}
|
|
|
|
struct Game {
|
|
interface: Interface,
|
|
input_controller: InputController,
|
|
model_angle: f32,
|
|
game_scene: Option<GameScene>,
|
|
scene_loader: Arc<Mutex<AsyncLoaderContext>>,
|
|
}
|
|
|
|
impl Plugin for Game {
|
|
fn update(&mut self, context: &mut PluginContext, _control_flow: &mut ControlFlow) {
|
|
// Check each frame if our scene is created - here we just trying to lock context
|
|
// without blocking, it is important for main thread to be functional while other
|
|
// thread still loading data.
|
|
if let Ok(mut loader_context) = self.scene_loader.try_lock() {
|
|
if let Some(loader) = loader_context.data.take() {
|
|
self.game_scene = Some(GameScene {
|
|
scene: context.scenes.add(loader.scene),
|
|
model_handle: loader.model_handle,
|
|
});
|
|
|
|
// Once scene is loaded, we should hide progress bar and text.
|
|
context
|
|
.user_interface
|
|
.send_message(WidgetMessage::visibility(
|
|
self.interface.progress_bar,
|
|
MessageDirection::ToWidget,
|
|
false,
|
|
));
|
|
context
|
|
.user_interface
|
|
.send_message(WidgetMessage::visibility(
|
|
self.interface.progress_text,
|
|
MessageDirection::ToWidget,
|
|
false,
|
|
));
|
|
}
|
|
|
|
// Report progress in UI.
|
|
context
|
|
.user_interface
|
|
.send_message(ProgressBarMessage::progress(
|
|
self.interface.progress_bar,
|
|
MessageDirection::ToWidget,
|
|
loader_context.progress,
|
|
));
|
|
context.user_interface.send_message(TextMessage::text(
|
|
self.interface.progress_text,
|
|
MessageDirection::ToWidget,
|
|
format!(
|
|
"Loading scene: {}%\n{}",
|
|
loader_context.progress * 100.0,
|
|
loader_context.message
|
|
),
|
|
));
|
|
}
|
|
|
|
// Update scene only if it is loaded.
|
|
if let Some(game_scene) = self.game_scene.as_mut() {
|
|
// Use stored scene handle to borrow a mutable reference of scene in
|
|
// engine.
|
|
let scene = &mut context.scenes[game_scene.scene];
|
|
|
|
// Rotate model according to input controller state.
|
|
if self.input_controller.rotate_left {
|
|
self.model_angle -= 5.0f32.to_radians();
|
|
} else if self.input_controller.rotate_right {
|
|
self.model_angle += 5.0f32.to_radians();
|
|
}
|
|
|
|
scene.graph[game_scene.model_handle]
|
|
.local_transform_mut()
|
|
.set_rotation(UnitQuaternion::from_axis_angle(
|
|
&Vector3::y_axis(),
|
|
self.model_angle,
|
|
));
|
|
}
|
|
|
|
// While scene is loading, we will update progress bar.
|
|
if let GraphicsContext::Initialized(ref graphics_context) = context.graphics_context {
|
|
let fps = graphics_context.renderer.get_statistics().frames_per_second;
|
|
let debug_text = format!(
|
|
"Example 02 - Asynchronous Scene Loading\nUse [A][D] keys to rotate model.\nFPS: {}",
|
|
fps
|
|
);
|
|
context.user_interface.send_message(TextMessage::text(
|
|
self.interface.debug_text,
|
|
MessageDirection::ToWidget,
|
|
debug_text,
|
|
));
|
|
|
|
let size = graphics_context
|
|
.window
|
|
.inner_size()
|
|
.to_logical(graphics_context.window.scale_factor());
|
|
context.user_interface.send_message(WidgetMessage::width(
|
|
self.interface.root,
|
|
MessageDirection::ToWidget,
|
|
size.width,
|
|
));
|
|
context.user_interface.send_message(WidgetMessage::height(
|
|
self.interface.root,
|
|
MessageDirection::ToWidget,
|
|
size.height,
|
|
));
|
|
}
|
|
}
|
|
|
|
fn on_os_event(
|
|
&mut self,
|
|
event: &Event<()>,
|
|
_context: PluginContext,
|
|
_control_flow: &mut ControlFlow,
|
|
) {
|
|
if let Event::WindowEvent {
|
|
event: WindowEvent::KeyboardInput { input, .. },
|
|
..
|
|
} = event
|
|
{
|
|
// Handle key input events via `WindowEvent`, not via `DeviceEvent` (#32)
|
|
if let Some(key_code) = input.virtual_keycode {
|
|
match key_code {
|
|
VirtualKeyCode::A => {
|
|
self.input_controller.rotate_left = input.state == ElementState::Pressed
|
|
}
|
|
VirtualKeyCode::D => {
|
|
self.input_controller.rotate_right = input.state == ElementState::Pressed
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct GameConstructor;
|
|
|
|
impl PluginConstructor for GameConstructor {
|
|
fn create_instance(
|
|
&self,
|
|
_override_scene: Handle<Scene>,
|
|
context: PluginContext,
|
|
) -> Box<dyn Plugin> {
|
|
// Create simple user interface that will show some useful info.
|
|
let interface = create_ui(
|
|
&mut context.user_interface.build_ctx(),
|
|
Vector2::new(100.0, 100.0),
|
|
);
|
|
|
|
Box::new(Game {
|
|
interface,
|
|
input_controller: InputController {
|
|
rotate_left: false,
|
|
rotate_right: false,
|
|
},
|
|
model_angle: 180.0f32.to_radians(),
|
|
game_scene: None,
|
|
scene_loader: AsyncLoaderContext::load_with(context.resource_manager.clone()),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let mut executor = Executor::from_params(
|
|
Default::default(),
|
|
GraphicsContextParams {
|
|
window_attributes: WindowAttributes {
|
|
title: "Example - Asynchronous Scene Loading".to_string(),
|
|
..Default::default()
|
|
},
|
|
vsync: true,
|
|
},
|
|
);
|
|
executor.add_plugin_constructor(GameConstructor);
|
|
executor.run()
|
|
}
|