199 lines
6.7 KiB
Rust
199 lines
6.7 KiB
Rust
//! A simple FBX scene viewer made with Bevy.
|
|
//!
|
|
//! Just run `cargo run --release --example scene_viewer_fbx --features="fbx" /path/to/model.fbx`,
|
|
//! replacing the path as appropriate.
|
|
//! With no arguments it will load a default FBX model if available.
|
|
//! Pass `--help` to see all the supported arguments.
|
|
//!
|
|
//! If you want to hot reload asset changes, enable the `file_watcher` cargo feature.
|
|
|
|
use argh::FromArgs;
|
|
use bevy::{
|
|
asset::UnapprovedPathMode,
|
|
core_pipeline::prepass::{DeferredPrepass, DepthPrepass},
|
|
pbr::DefaultOpaqueRendererMethod,
|
|
prelude::*,
|
|
render::{
|
|
experimental::occlusion_culling::OcclusionCulling,
|
|
primitives::{Aabb, Sphere},
|
|
},
|
|
};
|
|
|
|
#[path = "../../helpers/camera_controller.rs"]
|
|
mod camera_controller;
|
|
|
|
mod fbx_viewer_plugin;
|
|
|
|
use camera_controller::{CameraController, CameraControllerPlugin};
|
|
use fbx_viewer_plugin::{FbxSceneHandle, FbxViewerPlugin};
|
|
|
|
/// A simple FBX scene viewer made with Bevy
|
|
#[derive(FromArgs, Resource)]
|
|
struct Args {
|
|
/// the path to the FBX scene
|
|
#[argh(positional, default = "\"assets/models/cube/cube.fbx\".to_string()")]
|
|
scene_path: String,
|
|
/// enable a depth prepass
|
|
#[argh(switch)]
|
|
depth_prepass: Option<bool>,
|
|
/// enable occlusion culling
|
|
#[argh(switch)]
|
|
occlusion_culling: Option<bool>,
|
|
/// enable deferred shading
|
|
#[argh(switch)]
|
|
deferred: Option<bool>,
|
|
/// spawn a light even if the scene already has one
|
|
#[argh(switch)]
|
|
add_light: Option<bool>,
|
|
}
|
|
|
|
fn main() {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
let args: Args = argh::from_env();
|
|
#[cfg(target_arch = "wasm32")]
|
|
let args: Args = Args::from_args(&[], &[]).unwrap();
|
|
|
|
let deferred = args.deferred;
|
|
|
|
let mut app = App::new();
|
|
app.add_plugins((
|
|
DefaultPlugins
|
|
.set(WindowPlugin {
|
|
primary_window: Some(Window {
|
|
title: "bevy fbx scene viewer".to_string(),
|
|
..default()
|
|
}),
|
|
..default()
|
|
})
|
|
.set(AssetPlugin {
|
|
file_path: std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()),
|
|
// Allow scenes to be loaded from anywhere on disk
|
|
unapproved_path_mode: UnapprovedPathMode::Allow,
|
|
..default()
|
|
}),
|
|
CameraControllerPlugin,
|
|
FbxViewerPlugin,
|
|
))
|
|
.insert_resource(args)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(PreUpdate, setup_scene_after_load);
|
|
|
|
// If deferred shading was requested, turn it on.
|
|
if deferred == Some(true) {
|
|
app.insert_resource(DefaultOpaqueRendererMethod::deferred());
|
|
}
|
|
|
|
app.run();
|
|
}
|
|
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
|
|
let scene_path = &args.scene_path;
|
|
info!("Loading FBX file: {}", scene_path);
|
|
|
|
commands.insert_resource(FbxSceneHandle::new(asset_server.load(scene_path.clone())));
|
|
}
|
|
|
|
fn setup_scene_after_load(
|
|
mut commands: Commands,
|
|
mut setup: Local<bool>,
|
|
mut scene_handle: ResMut<FbxSceneHandle>,
|
|
asset_server: Res<AssetServer>,
|
|
args: Res<Args>,
|
|
meshes: Query<(&GlobalTransform, Option<&Aabb>), With<Mesh3d>>,
|
|
) {
|
|
if scene_handle.is_loaded && !*setup {
|
|
*setup = true;
|
|
// Find an approximate bounding box of the scene from its meshes
|
|
if meshes.iter().any(|(_, maybe_aabb)| maybe_aabb.is_none()) {
|
|
return;
|
|
}
|
|
|
|
let mut min = Vec3A::splat(f32::MAX);
|
|
let mut max = Vec3A::splat(f32::MIN);
|
|
for (transform, maybe_aabb) in &meshes {
|
|
let aabb = maybe_aabb.unwrap();
|
|
// If the Aabb had not been rotated, applying the non-uniform scale would produce the
|
|
// correct bounds. However, it could very well be rotated and so we first convert to
|
|
// a Sphere, and then back to an Aabb to find the conservative min and max points.
|
|
let sphere = Sphere {
|
|
center: Vec3A::from(transform.transform_point(Vec3::from(aabb.center))),
|
|
radius: transform.radius_vec3a(aabb.half_extents),
|
|
};
|
|
let aabb = Aabb::from(sphere);
|
|
min = min.min(aabb.min());
|
|
max = max.max(aabb.max());
|
|
}
|
|
|
|
let size = (max - min).length();
|
|
let aabb = Aabb::from_min_max(Vec3::from(min), Vec3::from(max));
|
|
|
|
info!("Spawning a controllable 3D perspective camera");
|
|
let mut projection = PerspectiveProjection::default();
|
|
projection.far = projection.far.max(size * 10.0);
|
|
|
|
let walk_speed = size * 3.0;
|
|
let camera_controller = CameraController {
|
|
walk_speed,
|
|
run_speed: 3.0 * walk_speed,
|
|
..default()
|
|
};
|
|
|
|
// Display the controls of the scene viewer
|
|
info!("{}", camera_controller);
|
|
info!("{}", *scene_handle);
|
|
|
|
let mut camera = commands.spawn((
|
|
Camera3d::default(),
|
|
Projection::from(projection),
|
|
Transform::from_translation(Vec3::from(aabb.center) + size * Vec3::new(0.5, 0.25, 0.5))
|
|
.looking_at(Vec3::from(aabb.center), Vec3::Y),
|
|
Camera {
|
|
is_active: false,
|
|
..default()
|
|
},
|
|
EnvironmentMapLight {
|
|
diffuse_map: asset_server
|
|
.load("assets/environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
|
|
specular_map: asset_server
|
|
.load("assets/environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
|
|
intensity: 150.0,
|
|
..default()
|
|
},
|
|
camera_controller,
|
|
));
|
|
|
|
// If occlusion culling was requested, include the relevant components.
|
|
// The Z-prepass is currently required.
|
|
if args.occlusion_culling == Some(true) {
|
|
camera.insert((DepthPrepass, OcclusionCulling));
|
|
}
|
|
|
|
// If the depth prepass was requested, include it.
|
|
if args.depth_prepass == Some(true) {
|
|
camera.insert(DepthPrepass);
|
|
}
|
|
|
|
// If deferred shading was requested, include the prepass.
|
|
if args.deferred == Some(true) {
|
|
camera
|
|
.insert(Msaa::Off)
|
|
.insert(DepthPrepass)
|
|
.insert(DeferredPrepass);
|
|
}
|
|
|
|
// Spawn a default light if the scene does not have one
|
|
if !scene_handle.has_light || args.add_light == Some(true) {
|
|
info!("Spawning a directional light");
|
|
let mut light = commands.spawn((
|
|
DirectionalLight::default(),
|
|
Transform::from_xyz(1.0, 1.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
if args.occlusion_culling == Some(true) {
|
|
light.insert(OcclusionCulling);
|
|
}
|
|
|
|
scene_handle.has_light = true;
|
|
}
|
|
}
|
|
}
|