mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
Object picking (#8)
This PR implements basic object selection, with help from `bevy_mod_picking` and `bevy_mod_outline`. ### Work Done * Click to select objects in the world * Selected objects become visually highlighted * The selected entity is available globally on the `Res<Selection>` resource * Selected entities can also be queried via the `Selected` marker component * Pressing `F` will "focus" the camera on the selected entity * Pressing `Esc` will clear the active selection ### Known Issues * We're currently using `bevy_mod_picking`'s "Selection" feature, which isn't aware of our own input events. As a consequence, it will select the object under the cursor when releasing `LMB` after orbiting the camera, which is not what we want. This should be pretty easily fixable by dropping `bevy_mod_picking`'s selection feature and reading the object under the cursor in our own click listener that only runs if the LMB event is unhandled. * The skinned mesh is not currently selectable. This is likely fixable by adjusting the logic in `scene::mark_scene_objects`... somehow. The `Gltf` data structure is kind of a nightmare to work with, so this will take some investigation. ### Future Enhancements * For parity with other DCC apps, the "Focus" action should also adjust the camera zoom level to "frame" the selected object in the view. Punting on that for now because I'm not super confident about the math involved. * Group selection with Shift/Ctrl and/or marquee selection would be really nice to have, but probably not critical for now.
This commit is contained in:
@@ -6,8 +6,12 @@ use bevy::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
input::{Action, CameraMotion, InputAction, Lifecycle},
|
||||
math::{Easing, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq, Velocity},
|
||||
input::{Action, CameraMotion, InputAction, Lifecycle, Selection},
|
||||
interaction::Selected,
|
||||
math::{
|
||||
Easing, InterpPosition, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq,
|
||||
Velocity,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct CameraControllerPlugin;
|
||||
@@ -21,8 +25,8 @@ impl Plugin for CameraControllerPlugin {
|
||||
Update,
|
||||
(
|
||||
camera_control_kinematic,
|
||||
process_physical_inputs.before(InterpolationSystem),
|
||||
camera_control_physical.after(InterpolationSystem),
|
||||
(process_physical_inputs, process_focus_inputs).before(InterpolationSystem),
|
||||
(camera_control_physical, interp_camera_focus).after(InterpolationSystem),
|
||||
)
|
||||
.in_set(CameraMotionSystem),
|
||||
);
|
||||
@@ -87,7 +91,6 @@ fn camera_control_kinematic(
|
||||
InputAction(_, Action::Camera(CameraMotion::Translate(_)))
|
||||
)
|
||||
}) {
|
||||
#[allow(irrefutable_let_patterns)] // Will eventually be more variants
|
||||
if let InputAction(lifecycle, Action::Camera(motion)) = action {
|
||||
use CameraMotion::*;
|
||||
|
||||
@@ -151,6 +154,64 @@ fn process_physical_inputs(
|
||||
}
|
||||
}
|
||||
|
||||
fn process_focus_inputs(
|
||||
mut cmd: Commands,
|
||||
mut input: EventReader<InputAction>,
|
||||
mut q_camera: Query<(Entity, &CameraController, &mut Velocity)>,
|
||||
q_interp_vel: Query<&InterpVelocity>,
|
||||
mut q_interp_pos: Query<&mut InterpPosition>,
|
||||
q_selected_xform: Query<&GlobalTransform, With<Selected>>,
|
||||
) {
|
||||
if input.iter().any(|action| {
|
||||
matches!(
|
||||
action,
|
||||
InputAction(Lifecycle::Stop, Action::Selection(Selection::Focus))
|
||||
)
|
||||
}) {
|
||||
let Ok(target) = q_selected_xform
|
||||
.get_single()
|
||||
.map(|xform| xform.translation())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (ent, controller, mut velocity) in q_camera.iter_mut() {
|
||||
// Zero out any existing camera velocity or interpolation
|
||||
**velocity = Vec3::ZERO;
|
||||
|
||||
if q_interp_vel.get(ent).is_ok() {
|
||||
cmd.entity(ent).remove::<InterpVelocity>();
|
||||
}
|
||||
|
||||
let initial = controller.focus;
|
||||
let easing = Easing::SineOut;
|
||||
let duration = 0.25;
|
||||
|
||||
if let Ok(mut interp) = q_interp_pos.get_mut(ent) {
|
||||
if interp.target.nearly_eq_within(target, 1e-3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
interp.initial = initial;
|
||||
interp.target = target;
|
||||
interp.easing = easing;
|
||||
interp.t = 0.;
|
||||
interp.duration = duration;
|
||||
} else {
|
||||
cmd.entity(ent).insert(InterpPosition {
|
||||
inner: Interpolation {
|
||||
initial,
|
||||
target,
|
||||
easing,
|
||||
..default()
|
||||
},
|
||||
duration,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn camera_control_physical(
|
||||
mut q_camera: Query<(&mut CameraController, &mut Transform, &Velocity)>,
|
||||
) {
|
||||
@@ -161,6 +222,18 @@ fn camera_control_physical(
|
||||
}
|
||||
}
|
||||
|
||||
fn interp_camera_focus(
|
||||
mut q_camera: Query<(&mut CameraController, &mut Transform, &InterpPosition)>,
|
||||
) {
|
||||
for (mut controller, mut xform, interp) in q_camera.iter_mut() {
|
||||
let target_focus = interp.current();
|
||||
let delta_pos = target_focus - controller.focus;
|
||||
|
||||
controller.focus = target_focus;
|
||||
xform.translation += delta_pos;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn capture_pointer(window: &mut Window) {
|
||||
window.cursor.visible = false;
|
||||
|
||||
+46
-5
@@ -4,14 +4,22 @@ use bevy::{
|
||||
utils::HashSet,
|
||||
};
|
||||
|
||||
pub struct StudioInputPlugin;
|
||||
#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct StudioInputSystem;
|
||||
|
||||
pub struct StudioInputPlugin;
|
||||
impl Plugin for StudioInputPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<InputAction>();
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
translate_bevy_events.after(bevy::input::InputSystem),
|
||||
(
|
||||
translate_selection_events,
|
||||
translate_dcc_camera_events,
|
||||
translate_fps_camera_events,
|
||||
)
|
||||
.in_set(StudioInputSystem)
|
||||
.after(bevy::input::InputSystem),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +37,7 @@ pub enum Lifecycle {
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Action {
|
||||
Camera(CameraMotion),
|
||||
Selection(Selection),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -39,17 +48,43 @@ pub enum CameraMotion {
|
||||
Orbit(Vec2),
|
||||
Rotate(Vec2),
|
||||
}
|
||||
|
||||
impl From<CameraMotion> for Action {
|
||||
fn from(value: CameraMotion) -> Self {
|
||||
Action::Camera(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Selection {
|
||||
/// Clear the current selection
|
||||
Clear,
|
||||
/// Focus the camera on the currently selected entity
|
||||
Focus,
|
||||
}
|
||||
impl From<Selection> for Action {
|
||||
fn from(value: Selection) -> Self {
|
||||
Action::Selection(value)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make the bindings of raw device inputs to InputActions configurable
|
||||
fn translate_bevy_events(
|
||||
|
||||
fn translate_selection_events(keyboard: Res<Input<KeyCode>>, mut input: EventWriter<InputAction>) {
|
||||
if keyboard.just_pressed(KeyCode::Escape) {
|
||||
input.send(InputAction(Lifecycle::Start, Selection::Clear.into()));
|
||||
} else if keyboard.just_released(KeyCode::Escape) {
|
||||
input.send(InputAction(Lifecycle::Stop, Selection::Clear.into()));
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::F) {
|
||||
input.send(InputAction(Lifecycle::Start, Selection::Focus.into()));
|
||||
} else if keyboard.just_released(KeyCode::F) {
|
||||
input.send(InputAction(Lifecycle::Stop, Selection::Focus.into()));
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_dcc_camera_events(
|
||||
mouse_motion: EventReader<MouseMotion>,
|
||||
mut mouse_wheel: EventReader<MouseWheel>,
|
||||
mouse_buttons: Res<Input<MouseButton>>,
|
||||
keyboard: Res<Input<KeyCode>>,
|
||||
mut input: EventWriter<InputAction>,
|
||||
@@ -148,7 +183,13 @@ fn translate_bevy_events(
|
||||
CameraMotion::Rotate(Vec2::ZERO).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_fps_camera_events(
|
||||
mut mouse_wheel: EventReader<MouseWheel>,
|
||||
keyboard: Res<Input<KeyCode>>,
|
||||
mut input: EventWriter<InputAction>,
|
||||
) {
|
||||
// Camera Translation (FPS-like)
|
||||
use KeyCode::*;
|
||||
let wasd_keys = [W, A, S, D, E, Q];
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy_mod_outline::{
|
||||
AutoGenerateOutlineNormalsPlugin, OutlineBundle, OutlinePlugin, OutlineVolume,
|
||||
};
|
||||
use bevy_mod_picking::{prelude::Pointer, selection::Select};
|
||||
|
||||
use crate::{
|
||||
camera_controller::CameraMotionSystem,
|
||||
input::{self, Action, InputAction, Lifecycle},
|
||||
};
|
||||
|
||||
#[derive(Component, Clone, Copy)]
|
||||
pub struct Selectable;
|
||||
|
||||
#[derive(Component, Clone, Copy)]
|
||||
pub struct Selected;
|
||||
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
pub struct Selection(Entity);
|
||||
|
||||
pub struct InteractionPlugin;
|
||||
impl Plugin for InteractionPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins((OutlinePlugin, AutoGenerateOutlineNormalsPlugin));
|
||||
app.add_systems(Update, update_selection);
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
draw_selection_outline.run_if(resource_changed_or_removed::<Selection>()),
|
||||
draw_selection_axes
|
||||
.run_if(resource_exists::<Selection>())
|
||||
.after(CameraMotionSystem),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_selection(
|
||||
mut cmd: Commands,
|
||||
// FIXME: These events aren't aware of our input actions, so we receive a
|
||||
// selection event when LMB is released after orbiting the camera.
|
||||
// Should drop bevy_mod_picking's Selection plugin/feature and just
|
||||
// use our own conditional click listener.
|
||||
mut ev_select: EventReader<Pointer<Select>>,
|
||||
mut ev_input: EventReader<InputAction>,
|
||||
q_parents: Query<&Parent>,
|
||||
q_selectable: Query<&Selectable>,
|
||||
q_selected: Query<Entity, With<Selected>>,
|
||||
) {
|
||||
if let Some(event) = ev_select.iter().last() {
|
||||
// Clear any existing `Selected` components
|
||||
for ent in q_selected.iter() {
|
||||
cmd.entity(ent).remove::<Selected>();
|
||||
}
|
||||
|
||||
// Find the nearest Selectable parent of the target entity
|
||||
let mut ent = event.target;
|
||||
let ent_selected = loop {
|
||||
if q_selectable.get(ent).is_ok() {
|
||||
break Some(ent);
|
||||
}
|
||||
|
||||
if let Ok(parent) = q_parents.get(ent) {
|
||||
ent = parent.get();
|
||||
} else {
|
||||
break None;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ent) = ent_selected {
|
||||
cmd.insert_resource(Selection(ent));
|
||||
cmd.entity(ent).insert(Selected);
|
||||
} else {
|
||||
cmd.remove_resource::<Selection>();
|
||||
}
|
||||
} else if ev_input.iter().any(|action| {
|
||||
matches!(
|
||||
action,
|
||||
InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Clear))
|
||||
)
|
||||
}) {
|
||||
cmd.remove_resource::<Selection>();
|
||||
|
||||
for ent in q_selected.iter() {
|
||||
cmd.entity(ent).remove::<Selected>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_selection_axes(
|
||||
mut gizmos: Gizmos,
|
||||
selection: Res<Selection>,
|
||||
q_transform: Query<&GlobalTransform>,
|
||||
) {
|
||||
let Ok(xform) = q_transform.get(**selection) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let origin = xform.translation();
|
||||
|
||||
let mut draw_axis = |direction: Vec3, color: Color| {
|
||||
let end = origin + direction * 0.25;
|
||||
gizmos.line(origin, end, color);
|
||||
};
|
||||
|
||||
draw_axis(xform.right(), Color::RED);
|
||||
draw_axis(xform.up(), Color::GREEN);
|
||||
draw_axis(xform.back(), Color::BLUE);
|
||||
}
|
||||
|
||||
fn draw_selection_outline(
|
||||
mut cmd: Commands,
|
||||
q_selected: Query<&Children, With<Selected>>,
|
||||
q_meshes: Query<Entity, (With<Handle<Mesh>>, Without<OutlineVolume>)>,
|
||||
q_outlines: Query<Entity, With<OutlineVolume>>,
|
||||
) {
|
||||
// Remove existing outlines
|
||||
for ent in q_outlines.iter() {
|
||||
cmd.entity(ent).remove::<OutlineBundle>();
|
||||
}
|
||||
|
||||
for children in q_selected.iter() {
|
||||
for &child in children.iter() {
|
||||
if q_meshes.contains(child) {
|
||||
cmd.entity(child).insert(OutlineBundle {
|
||||
outline: OutlineVolume {
|
||||
visible: true,
|
||||
width: 4.,
|
||||
colour: Color::YELLOW,
|
||||
},
|
||||
..default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-3
@@ -1,8 +1,11 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
#![allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::{prelude::*, window::PrimaryWindow};
|
||||
use bevy_inspector_egui::quick::WorldInspectorPlugin;
|
||||
use bevy_mod_picking::DefaultPickingPlugins;
|
||||
use camera_controller::{CameraController, CameraControllerPlugin, CameraMotionSystem};
|
||||
use input::StudioInputPlugin;
|
||||
use interaction::InteractionPlugin;
|
||||
use math::InterpolationPlugin;
|
||||
use scene::ScenePlugin;
|
||||
use skybox::SkyboxPlugin;
|
||||
@@ -12,6 +15,7 @@ use wasm_bindgen::prelude::*;
|
||||
|
||||
mod camera_controller;
|
||||
mod input;
|
||||
mod interaction;
|
||||
mod math;
|
||||
mod scene;
|
||||
mod skybox;
|
||||
@@ -37,6 +41,9 @@ pub fn init(viewport_size: ViewportSize) {
|
||||
}),
|
||||
..default()
|
||||
}),
|
||||
DefaultPickingPlugins,
|
||||
WorldInspectorPlugin::new(),
|
||||
InteractionPlugin,
|
||||
StudioInputPlugin,
|
||||
InterpolationPlugin,
|
||||
CameraControllerPlugin,
|
||||
@@ -44,10 +51,32 @@ pub fn init(viewport_size: ViewportSize) {
|
||||
SkyboxPlugin,
|
||||
UiLayerPlugin,
|
||||
))
|
||||
.add_systems(Update, draw_world_axes.after(CameraMotionSystem))
|
||||
.insert_resource(GizmoConfig {
|
||||
enabled: true,
|
||||
depth_bias: -0.1,
|
||||
line_perspective: false,
|
||||
line_width: 2.,
|
||||
..default()
|
||||
})
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_gizmo_line_thickness,
|
||||
draw_world_axes.after(CameraMotionSystem),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn update_gizmo_line_thickness(
|
||||
mut config: ResMut<GizmoConfig>,
|
||||
q_window: Query<&Window, (Changed<Window>, With<PrimaryWindow>)>,
|
||||
) {
|
||||
if let Ok(scale_factor) = q_window.get_single().map(|win| win.scale_factor() as f32) {
|
||||
config.line_width = 2. * scale_factor;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_world_axes(mut gizmos: Gizmos, q: Query<&CameraController>) {
|
||||
let &CameraController { focus, radius, .. } = q.single();
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ where
|
||||
f32: Lerp<T>,
|
||||
T: Copy,
|
||||
{
|
||||
fn tick(&mut self, dt: f32) -> T {
|
||||
fn tick(&mut self, dt: f32) -> &mut Self {
|
||||
self.t = (self.t + dt).clamp(0., 1.);
|
||||
self.current()
|
||||
self
|
||||
}
|
||||
|
||||
pub fn current(&self) -> T {
|
||||
@@ -52,6 +52,8 @@ where
|
||||
let t = match self.easing {
|
||||
Easing::Linear => self.t,
|
||||
Easing::Sine => self.t.curve_sin(),
|
||||
Easing::SineIn => self.t.curve_sin_in(),
|
||||
Easing::SineOut => self.t.curve_sin_out(),
|
||||
};
|
||||
|
||||
t.lerp(self.initial, self.target)
|
||||
@@ -97,7 +99,7 @@ fn interp_velocity(time: Res<Time>, mut q_interp: Query<(&mut Velocity, &mut Int
|
||||
|
||||
for (mut velocity, mut interp) in q_interp.iter_mut() {
|
||||
let dt = delta_seconds / interp.accel_zero2max;
|
||||
**velocity = interp.tick(dt);
|
||||
**velocity = interp.tick(dt).current();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ mod traits;
|
||||
pub enum Easing {
|
||||
Linear,
|
||||
Sine,
|
||||
SineOut,
|
||||
#[allow(dead_code)]
|
||||
SineIn,
|
||||
}
|
||||
|
||||
impl Default for Easing {
|
||||
|
||||
@@ -73,6 +73,18 @@ pub trait CurveSin {
|
||||
/// }
|
||||
/// ```
|
||||
fn curve_sin(self) -> Self;
|
||||
|
||||
/// Works like `curve_sin`, except that only the lower half of the line is
|
||||
/// curved, with the upper half progressing linearly.
|
||||
///
|
||||
/// https://www.desmos.com/calculator/ngceukl6g5
|
||||
fn curve_sin_in(self) -> Self;
|
||||
|
||||
/// Works like `curve_sin`, except that only the upper half of the line is
|
||||
/// curved, with the lower half progressing linearly.
|
||||
///
|
||||
/// https://www.desmos.com/calculator/umvvxhkd46
|
||||
fn curve_sin_out(self) -> Self;
|
||||
}
|
||||
|
||||
impl<T, Operand> Lerp<Operand> for T
|
||||
@@ -141,6 +153,18 @@ impl CurveSin for f32 {
|
||||
|
||||
-1. * ((self * PI).cos() / 2.) + 0.5
|
||||
}
|
||||
|
||||
fn curve_sin_in(self) -> Self {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
1. - ((self * PI) / 2.).cos()
|
||||
}
|
||||
|
||||
fn curve_sin_out(self) -> Self {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
((self * PI) / 2.).sin()
|
||||
}
|
||||
}
|
||||
|
||||
impl CurveSin for f64 {
|
||||
@@ -149,4 +173,16 @@ impl CurveSin for f64 {
|
||||
|
||||
-1. * ((self * PI).cos() / 2.) + 0.5
|
||||
}
|
||||
|
||||
fn curve_sin_in(self) -> Self {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
1. - ((self * PI) / 2.).cos()
|
||||
}
|
||||
|
||||
fn curve_sin_out(self) -> Self {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
((self * PI) / 2.).sin()
|
||||
}
|
||||
}
|
||||
|
||||
+143
-24
@@ -1,18 +1,23 @@
|
||||
use bevy::{
|
||||
asset::LoadState,
|
||||
core_pipeline::{
|
||||
bloom::{BloomCompositeMode, BloomPrefilterSettings, BloomSettings},
|
||||
fxaa::Fxaa,
|
||||
tonemapping::Tonemapping,
|
||||
},
|
||||
gltf::{self, Gltf},
|
||||
math::vec3,
|
||||
pbr::NotShadowCaster,
|
||||
prelude::*,
|
||||
reflect::{TypePath, TypeUuid},
|
||||
render::render_resource::{AsBindGroup, ShaderRef},
|
||||
utils::HashSet,
|
||||
};
|
||||
use bevy_mod_picking::PickableBundle;
|
||||
|
||||
use crate::{
|
||||
camera_controller::{CameraController, CameraControllerBundle, CameraMotionSystem},
|
||||
interaction::Selectable,
|
||||
skybox::SkyboxId,
|
||||
};
|
||||
|
||||
@@ -21,8 +26,17 @@ pub struct ScenePlugin;
|
||||
impl Plugin for ScenePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(ClearColor(Color::hsl(0.0, 0.0, 0.25)))
|
||||
.insert_resource(Msaa::Off)
|
||||
.add_plugins(MaterialPlugin::<ProceduralGridMaterial>::default())
|
||||
.add_systems(Startup, spawn_scene)
|
||||
.add_systems(Startup, initialize)
|
||||
.add_systems(
|
||||
Update,
|
||||
mark_scene_objects.run_if(
|
||||
resource_exists::<NewGltf>()
|
||||
.and_then(gltf_loaded)
|
||||
.and_then(gltf_scene_spawned),
|
||||
),
|
||||
)
|
||||
.add_systems(PostUpdate, move_plane_with_camera.after(CameraMotionSystem));
|
||||
}
|
||||
}
|
||||
@@ -30,7 +44,10 @@ impl Plugin for ScenePlugin {
|
||||
#[derive(Component)]
|
||||
struct WorldGridPlane;
|
||||
|
||||
fn spawn_scene(
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
struct NewGltf(pub Handle<Gltf>);
|
||||
|
||||
fn initialize(
|
||||
mut cmd: Commands,
|
||||
assets: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
@@ -40,15 +57,20 @@ fn spawn_scene(
|
||||
cmd.insert_resource(SkyboxId("test_scene".into()));
|
||||
|
||||
// glTF scene
|
||||
cmd.spawn(SceneBundle {
|
||||
scene: assets.load("gltf/sample-room.gltf#Scene0"),
|
||||
..default()
|
||||
});
|
||||
cmd.insert_resource(NewGltf(assets.load("gltf/sample-room.gltf")));
|
||||
cmd.spawn((
|
||||
Name::new("Scene"),
|
||||
SceneBundle {
|
||||
scene: assets.load("gltf/sample-room.gltf#Scene0"),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Camera, graphics settings
|
||||
let focus = vec3(0.35011, 0.5, -0.99576);
|
||||
let camera_xform = Transform::from_xyz(-2.5, 4.5, 5.0).looking_at(focus, Vec3::Y);
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
@@ -73,30 +95,36 @@ fn spawn_scene(
|
||||
|
||||
// Main light
|
||||
// TODO: get Blender to export enough metadata to automate light placements
|
||||
cmd.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::hex("FFE5B4").unwrap(),
|
||||
intensity: 200.,
|
||||
radius: 0.1,
|
||||
shadows_enabled: true,
|
||||
cmd.spawn((
|
||||
Name::new("Ceiling Light"),
|
||||
PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::hex("FFE5B4").unwrap(),
|
||||
intensity: 200.,
|
||||
radius: 0.1,
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.35263, 2.4348, 0.19601),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.35263, 2.4348, 0.19601),
|
||||
..default()
|
||||
});
|
||||
));
|
||||
|
||||
// Monitor glow
|
||||
cmd.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::WHITE,
|
||||
intensity: 6.,
|
||||
radius: 0.25,
|
||||
shadows_enabled: false,
|
||||
cmd.spawn((
|
||||
Name::new("Monitor Glow"),
|
||||
PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::WHITE,
|
||||
intensity: 6.,
|
||||
radius: 0.25,
|
||||
shadows_enabled: false,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.42615, 1.0706, -2.0705),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.42615, 1.0706, -2.0705),
|
||||
..default()
|
||||
});
|
||||
));
|
||||
|
||||
// Plane
|
||||
let plane = meshes.add(Mesh::from(shape::Plane {
|
||||
@@ -106,6 +134,7 @@ fn spawn_scene(
|
||||
let proc_grid = mat_proc_grid.add(ProceduralGridMaterial {});
|
||||
|
||||
cmd.spawn((
|
||||
Name::new("World Grid"),
|
||||
SpatialBundle::from_transform(Transform::IDENTITY),
|
||||
WorldGridPlane,
|
||||
))
|
||||
@@ -131,6 +160,96 @@ fn spawn_scene(
|
||||
});
|
||||
}
|
||||
|
||||
fn gltf_loaded(assets: Res<AssetServer>, gltf: Res<NewGltf>) -> bool {
|
||||
assets.get_load_state(&**gltf) == LoadState::Loaded
|
||||
}
|
||||
|
||||
fn gltf_scene_spawned(
|
||||
r_assets: Res<AssetServer>,
|
||||
r_gltf: Res<NewGltf>,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
q_scenes: Query<&Handle<Scene>, (With<Name>, With<Children>)>,
|
||||
) -> bool {
|
||||
let gltf = ra_gltfs.get(&r_gltf).unwrap();
|
||||
let Some(gltf_scene_path) = gltf
|
||||
.default_scene
|
||||
.as_ref()
|
||||
.and_then(|handle| r_assets.get_handle_path(handle))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
q_scenes.iter().any(|handle| {
|
||||
let Some(path) = r_assets.get_handle_path(handle) else {
|
||||
return false;
|
||||
};
|
||||
path == gltf_scene_path
|
||||
})
|
||||
}
|
||||
|
||||
fn mark_scene_objects(
|
||||
mut cmd: Commands,
|
||||
r_gltf: Res<NewGltf>,
|
||||
r_assets: Res<AssetServer>,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
ra_gltf_nodes: Res<Assets<gltf::GltfNode>>,
|
||||
ra_gltf_meshes: Res<Assets<gltf::GltfMesh>>,
|
||||
q_parents: Query<(Entity, &Name, &Children), Without<Selectable>>,
|
||||
q_primitives: Query<&Handle<Mesh>>,
|
||||
) {
|
||||
let gltf = ra_gltfs.get(&r_gltf).unwrap();
|
||||
|
||||
for (ent_parent, name, children) in q_parents.iter() {
|
||||
// See if this name matches any of the meshes or nodes in our glTF scene
|
||||
let Some(gltf_mesh) = gltf
|
||||
.named_meshes
|
||||
.get(&**name)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
gltf.named_nodes.get(&**name).and_then(|node_handle| {
|
||||
ra_gltf_nodes
|
||||
.get(node_handle)
|
||||
.and_then(|node| node.mesh.clone())
|
||||
})
|
||||
})
|
||||
.and_then(|handle| ra_gltf_meshes.get(&handle))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Collect all of the mesh handles in the glTF mesh
|
||||
let prims_from_gltf = gltf_mesh
|
||||
.primitives
|
||||
.iter()
|
||||
.filter_map(|prim| r_assets.get_handle_path(&prim.mesh))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// Collect all of the mesh handles in this entity's children
|
||||
let prims_from_ecs = children
|
||||
.iter()
|
||||
.filter_map(|ent_child| {
|
||||
q_primitives
|
||||
.get(*ent_child)
|
||||
.ok()
|
||||
.and_then(|handle| r_assets.get_handle_path(handle))
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// If the sets match, this is the entity that represents the glTF mesh
|
||||
if prims_from_gltf == prims_from_ecs {
|
||||
cmd.entity(ent_parent).insert(Selectable);
|
||||
|
||||
for &ent_child in children.iter() {
|
||||
if q_primitives.contains(ent_child) {
|
||||
cmd.entity(ent_child).insert(PickableBundle::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd.remove_resource::<NewGltf>();
|
||||
}
|
||||
|
||||
fn move_plane_with_camera(
|
||||
mut plane: Query<(&WorldGridPlane, &mut Transform)>,
|
||||
camera: Query<&CameraController, Changed<CameraController>>,
|
||||
|
||||
@@ -19,6 +19,7 @@ struct FramerateCounter;
|
||||
fn layout(mut cmd: Commands) {
|
||||
cmd.spawn((
|
||||
FramerateCounter,
|
||||
Name::new("Framerate Counter"),
|
||||
TextBundle::from_section(
|
||||
"0 fps",
|
||||
TextStyle {
|
||||
|
||||
Reference in New Issue
Block a user