mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-13 23:52:55 +00:00
Refactoring, reorganization and cleanup (#7)
### New `math` module * Traits for error-tolerant floating-point comparisons * Traits for interpolation and easing * Bevy plugin to manage 3D interpolations with ECS components ### `skybox` module refactor * ECS resource to enable external skybox selection/updates * System run conditions for more idiomatic/ECS-friendly control flow ### Camera controller refactor * Renamed from `orbit_cam` to `camera_controller` * Now delegates to the new interpolation plugin for WASD motion smoothing * Simple API for attaching the `CameraController` component to the camera from another module -- this resolves an awkward separation-of-concerns situation where we had to configure a bunch of render-quality settings in the camera controller module ### Scene module refactor * Renamed from `test_scene` to `scene` * This module is now responsible for configuring the skybox * This module is now responsible for spawning the camera and configuring the render settings
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
use std::f32::consts;
|
||||
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
window::{CursorGrabMode, PrimaryWindow},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
input::{Action, CameraMotion, InputAction, Lifecycle},
|
||||
math::{Easing, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq, Velocity},
|
||||
};
|
||||
|
||||
pub struct CameraControllerPlugin;
|
||||
|
||||
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct CameraMotionSystem;
|
||||
|
||||
impl Plugin for CameraControllerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
camera_control_kinematic,
|
||||
process_physical_inputs.before(InterpolationSystem),
|
||||
camera_control_physical.after(InterpolationSystem),
|
||||
)
|
||||
.in_set(CameraMotionSystem),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct CameraController {
|
||||
pub focus: Vec3,
|
||||
pub radius: f32,
|
||||
pub upside_down: bool,
|
||||
}
|
||||
|
||||
impl Default for CameraController {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
focus: Vec3::ZERO,
|
||||
radius: 10.0,
|
||||
upside_down: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct CameraControllerBundle {
|
||||
controller: CameraController,
|
||||
velocity: Velocity,
|
||||
}
|
||||
|
||||
impl CameraControllerBundle {
|
||||
pub fn new(focus: Vec3, position: Vec3) -> Self {
|
||||
Self {
|
||||
controller: CameraController {
|
||||
focus,
|
||||
radius: Vec3::distance(position, focus),
|
||||
..default()
|
||||
},
|
||||
velocity: Velocity(Vec3::ZERO),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn camera_control_kinematic(
|
||||
mut q_window: Query<&mut Window, With<PrimaryWindow>>,
|
||||
mut input: EventReader<InputAction>,
|
||||
mut q_camera: Query<(&mut CameraController, &mut Transform, &Projection)>,
|
||||
) {
|
||||
if input.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (mut controller, mut xform, proj) = q_camera.single_mut();
|
||||
let mut window = q_window.single_mut();
|
||||
|
||||
let mut any_ongoing = false;
|
||||
let mut any_started = false;
|
||||
let mut any_stopped = false;
|
||||
|
||||
for action in input.iter().filter(|action| {
|
||||
!matches!(
|
||||
action,
|
||||
InputAction(_, Action::Camera(CameraMotion::Translate(_)))
|
||||
)
|
||||
}) {
|
||||
#[allow(irrefutable_let_patterns)] // Will eventually be more variants
|
||||
if let InputAction(lifecycle, Action::Camera(motion)) = action {
|
||||
use CameraMotion::*;
|
||||
|
||||
match lifecycle {
|
||||
Lifecycle::Start => any_started = true,
|
||||
Lifecycle::Ongoing => any_ongoing = true,
|
||||
Lifecycle::Stop => any_stopped = true,
|
||||
}
|
||||
|
||||
match motion {
|
||||
Pan(delta) => do_pan(proj, &window, *delta, &mut xform, &mut controller),
|
||||
Zoom(delta) => do_zoom(&window, *delta, &mut xform, &mut controller),
|
||||
Orbit(delta) => do_orbit(&window, *delta, *lifecycle, &mut xform, &mut controller),
|
||||
Rotate(delta) => do_rotate(&window, *delta, &mut xform, &mut controller),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if any_started {
|
||||
capture_pointer(&mut window);
|
||||
} else if any_stopped && !any_ongoing {
|
||||
release_pointer(&mut window);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_physical_inputs(
|
||||
mut cmd: Commands,
|
||||
mut input: EventReader<InputAction>,
|
||||
q_camera: Query<(Entity, &Velocity), With<CameraController>>,
|
||||
mut q_interp: Query<&mut InterpVelocity>,
|
||||
) {
|
||||
for action in input.iter() {
|
||||
let &InputAction(_, Action::Camera(CameraMotion::Translate(delta))) = action else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for (ent, velocity) in q_camera.iter() {
|
||||
let target = delta * 0.075;
|
||||
|
||||
if let Ok(mut interp) = q_interp.get_mut(ent) {
|
||||
if interp.target.nearly_eq_within(target, 1e-3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
interp.initial = **velocity;
|
||||
interp.target = target;
|
||||
interp.t = 0.;
|
||||
} else {
|
||||
cmd.entity(ent).insert(InterpVelocity {
|
||||
inner: Interpolation {
|
||||
initial: **velocity,
|
||||
target,
|
||||
easing: Easing::Sine,
|
||||
..default()
|
||||
},
|
||||
accel_zero2max: 0.333,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn camera_control_physical(
|
||||
mut q_camera: Query<(&mut CameraController, &mut Transform, &Velocity)>,
|
||||
) {
|
||||
for (mut controller, mut xform, velocity) in q_camera.iter_mut() {
|
||||
let delta_pos = xform.rotation * (**velocity);
|
||||
xform.translation += delta_pos;
|
||||
controller.focus += delta_pos;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn capture_pointer(window: &mut Window) {
|
||||
window.cursor.visible = false;
|
||||
window.cursor.grab_mode = CursorGrabMode::Confined;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn release_pointer(window: &mut Window) {
|
||||
window.cursor.visible = true;
|
||||
window.cursor.grab_mode = CursorGrabMode::None;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_orbit(
|
||||
win: &Window,
|
||||
delta: Vec2,
|
||||
event_lifecycle: Lifecycle,
|
||||
xform: &mut Transform,
|
||||
controller: &mut CameraController,
|
||||
) {
|
||||
// Set `upside_down` flag if the event just started or stopped
|
||||
if matches!(event_lifecycle, Lifecycle::Start | Lifecycle::Stop) {
|
||||
controller.upside_down = xform.up().y <= 0.;
|
||||
}
|
||||
|
||||
// Translate to origin
|
||||
xform.translation += xform.forward() * controller.radius;
|
||||
|
||||
// Calculate rotation
|
||||
let delta_x = {
|
||||
let delta = delta.x / (win.physical_width() as f32) * consts::PI * 2.0;
|
||||
match controller.upside_down {
|
||||
true => -delta,
|
||||
false => delta,
|
||||
}
|
||||
};
|
||||
let delta_y = delta.y / (win.physical_height() as f32) * consts::PI;
|
||||
|
||||
let yaw = Quat::from_rotation_y(-delta_x);
|
||||
let pitch = Quat::from_rotation_x(-delta_y);
|
||||
|
||||
// Rotate around global Y and local X
|
||||
xform.rotation = yaw * xform.rotation;
|
||||
xform.rotation *= pitch;
|
||||
|
||||
// Translate back to the original position
|
||||
xform.translation += xform.back() * controller.radius;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_pan(
|
||||
proj: &Projection,
|
||||
win: &Window,
|
||||
delta: Vec2,
|
||||
xform: &mut Transform,
|
||||
controller: &mut CameraController,
|
||||
) {
|
||||
// Normalize the pan distance with respect to the window resolution
|
||||
let pan = match proj {
|
||||
Projection::Perspective(p) => {
|
||||
delta
|
||||
* Vec2 {
|
||||
x: (p.fov * p.aspect_ratio) / (win.physical_width() as f32),
|
||||
y: p.fov / (win.physical_height() as f32),
|
||||
}
|
||||
}
|
||||
_ => delta,
|
||||
};
|
||||
|
||||
// Pan along local XY plane, with the distance scaled by the radius
|
||||
let up = xform.up();
|
||||
let left = xform.left();
|
||||
let delta = (pan.y * up + pan.x * left) * controller.radius;
|
||||
|
||||
// Update camera translation and focus point
|
||||
xform.translation += delta;
|
||||
controller.focus += delta;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_zoom(win: &Window, delta: f32, xform: &mut Transform, controller: &mut CameraController) {
|
||||
let delta = (delta * 1.5) / (win.physical_height() as f32);
|
||||
controller.radius -= controller.radius * delta;
|
||||
xform.translation = controller.focus + xform.back() * controller.radius;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_rotate(win: &Window, delta: Vec2, xform: &mut Transform, controller: &mut CameraController) {
|
||||
// TODO: Move this somewhere else
|
||||
let sensitivity = 0.75;
|
||||
|
||||
let delta_x = delta.x / (win.physical_width() as f32) * consts::PI * 2.0 * sensitivity;
|
||||
let delta_y = delta.y / (win.physical_height() as f32) * consts::PI * sensitivity;
|
||||
|
||||
let yaw = Quat::from_rotation_y(-delta_x);
|
||||
let pitch = Quat::from_rotation_x(-delta_y);
|
||||
|
||||
// Rotate around global Y and local X
|
||||
xform.rotation = yaw * xform.rotation;
|
||||
xform.rotation *= pitch;
|
||||
|
||||
// Update focus point
|
||||
controller.focus = xform.translation + xform.forward() * controller.radius;
|
||||
}
|
||||
+11
-8
@@ -1,18 +1,20 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use bevy::prelude::*;
|
||||
use camera_controller::{CameraController, CameraControllerPlugin, CameraMotionSystem};
|
||||
use input::StudioInputPlugin;
|
||||
use orbit_cam::{CameraMotionSystem, OrbitCam, OrbitCamPlugin};
|
||||
use math::InterpolationPlugin;
|
||||
use scene::ScenePlugin;
|
||||
use skybox::SkyboxPlugin;
|
||||
use test_scene::TestScenePlugin;
|
||||
use ui::UiLayerPlugin;
|
||||
use viewport::ViewportSize;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod camera_controller;
|
||||
mod input;
|
||||
mod orbit_cam;
|
||||
mod math;
|
||||
mod scene;
|
||||
mod skybox;
|
||||
mod test_scene;
|
||||
mod ui;
|
||||
mod viewport;
|
||||
|
||||
@@ -36,8 +38,9 @@ pub fn init(viewport_size: ViewportSize) {
|
||||
..default()
|
||||
}),
|
||||
StudioInputPlugin,
|
||||
OrbitCamPlugin,
|
||||
TestScenePlugin,
|
||||
InterpolationPlugin,
|
||||
CameraControllerPlugin,
|
||||
ScenePlugin,
|
||||
SkyboxPlugin,
|
||||
UiLayerPlugin,
|
||||
))
|
||||
@@ -45,8 +48,8 @@ pub fn init(viewport_size: ViewportSize) {
|
||||
.run();
|
||||
}
|
||||
|
||||
fn draw_world_axes(mut gizmos: Gizmos, q: Query<&OrbitCam>) {
|
||||
let &OrbitCam { focus, radius, .. } = q.single();
|
||||
fn draw_world_axes(mut gizmos: Gizmos, q: Query<&CameraController>) {
|
||||
let &CameraController { focus, radius, .. } = q.single();
|
||||
|
||||
let mut draw_axis = |direction: Vec3, color: Color| {
|
||||
let end = focus + direction * radius / 15.;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Helper traits for floating-point equality checks with configurable tolerance.
|
||||
|
||||
use bevy::prelude::Vec3;
|
||||
|
||||
use super::internal::Zero;
|
||||
|
||||
pub trait NearlyEq<Rhs, Tolerance = Rhs> {
|
||||
const DEFAULT_TOLERANCE: Tolerance;
|
||||
|
||||
#[inline]
|
||||
fn nearly_eq(self, rhs: Rhs) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.nearly_eq_within(rhs, Self::DEFAULT_TOLERANCE)
|
||||
}
|
||||
|
||||
fn nearly_eq_within(self, rhs: Rhs, tolerance: Tolerance) -> bool;
|
||||
}
|
||||
|
||||
impl NearlyEq<f32> for f32 {
|
||||
const DEFAULT_TOLERANCE: f32 = f32::EPSILON;
|
||||
|
||||
#[inline]
|
||||
fn nearly_eq_within(self, rhs: f32, tolerance: f32) -> bool {
|
||||
(self - rhs).abs() < tolerance
|
||||
}
|
||||
}
|
||||
|
||||
impl NearlyEq<f64> for f64 {
|
||||
const DEFAULT_TOLERANCE: f64 = f64::EPSILON;
|
||||
|
||||
#[inline]
|
||||
fn nearly_eq_within(self, rhs: f64, tolerance: f64) -> bool {
|
||||
(self - rhs).abs() < tolerance
|
||||
}
|
||||
}
|
||||
|
||||
impl NearlyEq<Vec3, f32> for Vec3 {
|
||||
const DEFAULT_TOLERANCE: f32 = <f32 as NearlyEq<f32>>::DEFAULT_TOLERANCE;
|
||||
|
||||
fn nearly_eq_within(self, rhs: Vec3, tolerance: f32) -> bool {
|
||||
self.x.nearly_eq_within(rhs.x, tolerance)
|
||||
&& self.y.nearly_eq_within(rhs.y, tolerance)
|
||||
&& self.z.nearly_eq_within(rhs.z, tolerance)
|
||||
}
|
||||
}
|
||||
|
||||
impl Zero for Vec3 {
|
||||
#[inline]
|
||||
fn zero() -> Self {
|
||||
Vec3::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NearlyZero<Tolerance = Self> {
|
||||
const DEFAULT_TOLERANCE: Tolerance;
|
||||
|
||||
#[inline]
|
||||
fn nearly_zero(self) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.nearly_zero_within(Self::DEFAULT_TOLERANCE)
|
||||
}
|
||||
|
||||
fn nearly_zero_within(self, tolerance: Tolerance) -> bool;
|
||||
}
|
||||
|
||||
impl<T, Tolerance> NearlyZero<Tolerance> for T
|
||||
where
|
||||
T: Zero,
|
||||
T: NearlyEq<T, Tolerance>,
|
||||
{
|
||||
const DEFAULT_TOLERANCE: Tolerance = <Self as NearlyEq<T, Tolerance>>::DEFAULT_TOLERANCE;
|
||||
|
||||
#[inline]
|
||||
fn nearly_zero_within(self, tolerance: Tolerance) -> bool {
|
||||
self.nearly_eq_within(<Self as Zero>::zero(), tolerance)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Helpers to make it easier to implement traits for both `f32` and `f64`
|
||||
//! without needing to copy and paste identical implementations.
|
||||
|
||||
#[rustfmt::skip] pub(super) trait Zero { fn zero() -> Self; }
|
||||
#[rustfmt::skip] impl Zero for f32 { #[inline] fn zero() -> Self { 0_f32 } }
|
||||
#[rustfmt::skip] impl Zero for f64 { #[inline] fn zero() -> Self { 0_f64 } }
|
||||
|
||||
#[rustfmt::skip] pub(super) trait One { fn one() -> Self; }
|
||||
#[rustfmt::skip] impl One for f32 { #[inline] fn one() -> Self { 1_f32 } }
|
||||
#[rustfmt::skip] impl One for f64 { #[inline] fn one() -> Self { 1_f64 } }
|
||||
|
||||
pub(super) trait Clamp {
|
||||
fn clamp(self, min: Self, max: Self) -> Self;
|
||||
}
|
||||
|
||||
impl<T> Clamp for T
|
||||
where
|
||||
T: Sized + Copy + PartialOrd + core::fmt::Debug,
|
||||
{
|
||||
#[inline]
|
||||
fn clamp(mut self, min: Self, max: Self) -> Self {
|
||||
assert!(
|
||||
min <= max,
|
||||
"min > max, or either was NaN. min = {min:?}, max = {max:?}"
|
||||
);
|
||||
if self < min {
|
||||
self = min
|
||||
}
|
||||
if self > max {
|
||||
self = max
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::math::{NearlyEq, NearlyZero};
|
||||
|
||||
use super::{CurveSin, Easing, Lerp};
|
||||
|
||||
pub struct InterpolationPlugin;
|
||||
|
||||
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct InterpolationSystem;
|
||||
|
||||
impl Plugin for InterpolationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
||||
(interp_velocity, interp_position).in_set(InterpolationSystem),
|
||||
)
|
||||
.add_systems(PostUpdate, cleanup_finished_interps);
|
||||
}
|
||||
}
|
||||
|
||||
/// A `newtype` component for a `t` interpolation parameter.
|
||||
#[derive(Component, Deref, DerefMut, Debug, Clone, Copy)]
|
||||
pub struct Interp(pub f32);
|
||||
|
||||
pub struct Interpolation<T>
|
||||
where
|
||||
f32: Lerp<T>,
|
||||
{
|
||||
pub initial: T,
|
||||
pub target: T,
|
||||
pub t: f32,
|
||||
pub easing: Easing,
|
||||
}
|
||||
|
||||
impl<T> Interpolation<T>
|
||||
where
|
||||
f32: Lerp<T>,
|
||||
T: Copy,
|
||||
{
|
||||
fn tick(&mut self, dt: f32) -> T {
|
||||
self.t = (self.t + dt).clamp(0., 1.);
|
||||
self.current()
|
||||
}
|
||||
|
||||
pub fn current(&self) -> T {
|
||||
if self.t.nearly_zero() {
|
||||
self.initial
|
||||
} else if self.t.nearly_eq(1.) {
|
||||
self.target
|
||||
} else {
|
||||
let t = match self.easing {
|
||||
Easing::Linear => self.t,
|
||||
Easing::Sine => self.t.curve_sin(),
|
||||
};
|
||||
|
||||
t.lerp(self.initial, self.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Interpolation<T>
|
||||
where
|
||||
f32: Lerp<T>,
|
||||
T: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
initial: default(),
|
||||
target: default(),
|
||||
t: 0.,
|
||||
easing: Easing::Linear,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Deref, DerefMut)]
|
||||
pub struct InterpVelocity {
|
||||
#[deref]
|
||||
pub inner: Interpolation<Vec3>,
|
||||
pub accel_zero2max: f32,
|
||||
}
|
||||
|
||||
// TODO: Does this belong here?
|
||||
#[derive(Component, Deref, DerefMut)]
|
||||
pub struct Velocity(pub Vec3);
|
||||
|
||||
#[derive(Component, Deref, DerefMut)]
|
||||
pub struct InterpPosition {
|
||||
#[deref]
|
||||
pub inner: Interpolation<Vec3>,
|
||||
pub duration: f32,
|
||||
}
|
||||
|
||||
fn interp_velocity(time: Res<Time>, mut q_interp: Query<(&mut Velocity, &mut InterpVelocity)>) {
|
||||
let delta_seconds = time.delta_seconds();
|
||||
|
||||
for (mut velocity, mut interp) in q_interp.iter_mut() {
|
||||
let dt = delta_seconds / interp.accel_zero2max;
|
||||
**velocity = interp.tick(dt);
|
||||
}
|
||||
}
|
||||
|
||||
fn interp_position(time: Res<Time>, mut q_interp: Query<&mut InterpPosition>) {
|
||||
let delta_seconds = time.delta_seconds();
|
||||
|
||||
for mut interp in q_interp.iter_mut() {
|
||||
let dt = delta_seconds / interp.duration;
|
||||
interp.tick(dt);
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_finished_interps(
|
||||
mut cmd: Commands,
|
||||
q_interp_velocity: Query<(Entity, &InterpVelocity)>,
|
||||
q_interp_position: Query<(Entity, &InterpPosition)>,
|
||||
) {
|
||||
for (ent, interp) in q_interp_velocity.iter() {
|
||||
if interp.t.nearly_eq(1.) {
|
||||
cmd.entity(ent).remove::<InterpVelocity>();
|
||||
}
|
||||
}
|
||||
|
||||
for (ent, interp) in q_interp_position.iter() {
|
||||
if interp.t.nearly_eq(1.) {
|
||||
cmd.entity(ent).remove::<InterpPosition>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! Helpers for interpolating between values.
|
||||
|
||||
pub use ecs::*;
|
||||
pub use traits::*;
|
||||
|
||||
mod ecs;
|
||||
mod traits;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Easing {
|
||||
Linear,
|
||||
Sine,
|
||||
}
|
||||
|
||||
impl Default for Easing {
|
||||
fn default() -> Self {
|
||||
Self::Linear
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
use std::ops;
|
||||
|
||||
use bevy::prelude::Vec3;
|
||||
|
||||
use crate::math::{
|
||||
internal::{Clamp, One, Zero},
|
||||
NearlyZero,
|
||||
};
|
||||
|
||||
pub trait Lerp<Operand> {
|
||||
/// Interpolate linearly between `a` and `b`, using `self` as the `t` parameter.
|
||||
fn lerp(self, a: Operand, b: Operand) -> Operand;
|
||||
}
|
||||
|
||||
pub trait InvLerp<T: Sized> {
|
||||
/// Derive a `t` interpolation parameter for `self`, where `a` is the
|
||||
/// "origin" and `b` is the "destination".
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use math::NearlyEq;
|
||||
///
|
||||
/// let t = 48_f32.inv_lerp(32., 64.);
|
||||
/// // The expected result is 0.5, because 48 is halfway between 32 and 64.
|
||||
/// assert!(t.nearly_eq(0.5));
|
||||
/// ```
|
||||
fn inv_lerp(self, a: Self, b: Self) -> T;
|
||||
}
|
||||
|
||||
pub trait Remap<T: Sized>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
/// Re-map a value from its interpolated position in the range `from`, to
|
||||
/// the equivalent position in the range `to`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use math::NearlyEq;
|
||||
///
|
||||
/// let x = 48_f32.remap((32., 64.), (32., 128.));
|
||||
/// // The expected result is 80, because 48 is halfway between 32 and 64,
|
||||
/// // and 80 is halfway between 32 and 128.
|
||||
/// assert!(x.nearly_eq_within(80., 1e-3));
|
||||
/// ```
|
||||
fn remap(self, from: (Self, Self), to: (Self, Self)) -> Self;
|
||||
}
|
||||
|
||||
pub trait CurveSin {
|
||||
/// Fit a `t` interpolation parameter to a sine curve. Mostly useful for
|
||||
/// "easing" between steps in an animation.
|
||||
///
|
||||
/// https://www.desmos.com/calculator/k9x4c4cr5y
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// // This is fine, but it will feel "jerky" because the velocity is
|
||||
/// // constant from start to end.
|
||||
/// fn update_position(dt: f32, start: Vec3, end: Vec3, t: f32) -> Vec3 {
|
||||
/// (t + dt).clamp(0., 1.).lerp(start, end)
|
||||
/// }
|
||||
///
|
||||
/// // This will smoothly accelerate from a standstill at t=0, and
|
||||
/// // smoothly decelerate back to a stop at t=1.
|
||||
/// fn update_position_eased(dt: f32, start: Vec3, end: Vec3, t: f32) -> Vec3 {
|
||||
/// (t + dt)
|
||||
/// .clamp(0., 1.)
|
||||
/// .curve_sin()
|
||||
/// .lerp(start, end)
|
||||
/// }
|
||||
/// ```
|
||||
fn curve_sin(self) -> Self;
|
||||
}
|
||||
|
||||
impl<T, Operand> Lerp<Operand> for T
|
||||
where
|
||||
T: Copy,
|
||||
T: One,
|
||||
T: ops::Sub<Output = T>,
|
||||
Operand: ops::Mul<T, Output = Operand>,
|
||||
Operand: ops::Add<Output = Operand>,
|
||||
{
|
||||
#[inline]
|
||||
fn lerp(self, a: Operand, b: Operand) -> Operand {
|
||||
a * (Self::one() - self) + b * self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> InvLerp<T> for T
|
||||
where
|
||||
T: One + Zero,
|
||||
T: Clamp,
|
||||
T: NearlyZero,
|
||||
T: Copy,
|
||||
T: ops::Sub<Output = T>,
|
||||
T: ops::Div<Output = T>,
|
||||
{
|
||||
#[inline]
|
||||
fn inv_lerp(self, a: T, b: T) -> T {
|
||||
let dividend = b - a;
|
||||
if !dividend.nearly_zero() {
|
||||
((self - a) / dividend).clamp(T::zero(), T::one())
|
||||
} else {
|
||||
T::one()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vector inverse lerp requires a special implementation, yoinked from here:
|
||||
// https://discussions.unity.com/t/inverselerp-for-vector3/177038
|
||||
impl InvLerp<f32> for Vec3 {
|
||||
#[inline]
|
||||
fn inv_lerp(self, a: Vec3, b: Vec3) -> f32 {
|
||||
let ab = b - a;
|
||||
let av = self - a;
|
||||
if !ab.nearly_zero() {
|
||||
av.dot(ab) / ab.length_squared()
|
||||
} else {
|
||||
1.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Operand> Remap<T> for Operand
|
||||
where
|
||||
Operand: InvLerp<T>,
|
||||
T: Lerp<Operand>,
|
||||
{
|
||||
#[inline]
|
||||
fn remap(self, from: (Self, Self), to: (Self, Self)) -> Self {
|
||||
self.inv_lerp(from.0, from.1).lerp(to.0, to.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl CurveSin for f32 {
|
||||
fn curve_sin(self) -> Self {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
-1. * ((self * PI).cos() / 2.) + 0.5
|
||||
}
|
||||
}
|
||||
|
||||
impl CurveSin for f64 {
|
||||
fn curve_sin(self) -> Self {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
-1. * ((self * PI).cos() / 2.) + 0.5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mod float_comparison;
|
||||
mod internal;
|
||||
pub mod interp;
|
||||
|
||||
pub use float_comparison::*;
|
||||
pub use interp::*;
|
||||
@@ -1,329 +0,0 @@
|
||||
use std::{f32::consts, mem};
|
||||
|
||||
use bevy::{
|
||||
core_pipeline::{
|
||||
bloom::{BloomCompositeMode, BloomPrefilterSettings, BloomSettings},
|
||||
fxaa::Fxaa,
|
||||
tonemapping::Tonemapping,
|
||||
},
|
||||
math::vec3,
|
||||
prelude::*,
|
||||
window::{CursorGrabMode, PrimaryWindow},
|
||||
};
|
||||
|
||||
use crate::input::{Action, CameraMotion, InputAction, Lifecycle};
|
||||
|
||||
pub struct OrbitCamPlugin;
|
||||
|
||||
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct CameraMotionSystem;
|
||||
|
||||
impl Plugin for OrbitCamPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, spawn_camera);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
camera_control_kinematic.in_set(CameraMotionSystem),
|
||||
update_velocity
|
||||
.in_set(CameraMotionSystem)
|
||||
.after(camera_control_kinematic),
|
||||
camera_control_physical
|
||||
.in_set(CameraMotionSystem)
|
||||
.after(update_velocity),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: These don't really belong in this module
|
||||
#[derive(Component)]
|
||||
pub struct Velocity(pub Vec3);
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct TargetVelocity(pub Vec3);
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Interp(pub f32);
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct OrbitCam {
|
||||
pub focus: Vec3,
|
||||
pub radius: f32,
|
||||
pub upside_down: bool,
|
||||
}
|
||||
|
||||
impl Default for OrbitCam {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
focus: Vec3::ZERO,
|
||||
radius: 10.0,
|
||||
upside_down: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_camera(mut cmd: Commands) {
|
||||
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((
|
||||
// FIXME: Come up with a good way to separate the camera controller from
|
||||
// all the render settings here (all of these components need to
|
||||
// be paired with the camera)
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
camera: Camera {
|
||||
hdr: true,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
BloomSettings {
|
||||
composite_mode: BloomCompositeMode::EnergyConserving,
|
||||
intensity: 0.25,
|
||||
prefilter_settings: BloomPrefilterSettings {
|
||||
threshold: 0.8,
|
||||
threshold_softness: 0.5,
|
||||
},
|
||||
..default()
|
||||
},
|
||||
Fxaa::default(),
|
||||
OrbitCam {
|
||||
focus,
|
||||
radius: Vec3::distance(camera_xform.translation, focus),
|
||||
..default()
|
||||
},
|
||||
Velocity(Vec3::ZERO),
|
||||
TargetVelocity(Vec3::ZERO),
|
||||
Interp(1.),
|
||||
));
|
||||
}
|
||||
|
||||
fn camera_control_kinematic(
|
||||
mut q_window: Query<&mut Window, With<PrimaryWindow>>,
|
||||
mut input: EventReader<InputAction>,
|
||||
mut q_camera: Query<(
|
||||
&mut OrbitCam,
|
||||
&mut TargetVelocity,
|
||||
&mut Interp,
|
||||
&mut Transform,
|
||||
&Projection,
|
||||
)>,
|
||||
) {
|
||||
if input.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (mut orbit_cam, mut target_vel, mut t, mut xform, proj) = q_camera.single_mut();
|
||||
let mut window = q_window.single_mut();
|
||||
|
||||
let mut any_ongoing = false;
|
||||
let mut any_started = false;
|
||||
let mut any_stopped = false;
|
||||
|
||||
for action in input.iter() {
|
||||
#[allow(irrefutable_let_patterns)] // Will eventually be more variants
|
||||
if let InputAction(lifecycle, Action::Camera(motion)) = action {
|
||||
use CameraMotion::*;
|
||||
|
||||
// Ignore translation actions for the sake of pointer capture
|
||||
if mem::discriminant(motion) != mem::discriminant(&Translate(Vec3::ZERO)) {
|
||||
match lifecycle {
|
||||
Lifecycle::Start => any_started = true,
|
||||
Lifecycle::Ongoing => any_ongoing = true,
|
||||
Lifecycle::Stop => any_stopped = true,
|
||||
}
|
||||
}
|
||||
|
||||
match motion {
|
||||
Translate(delta) => do_translate(*delta, &mut target_vel, &mut t),
|
||||
Pan(delta) => do_pan(proj, &window, *delta, &mut xform, &mut orbit_cam),
|
||||
Zoom(delta) => do_zoom(&window, *delta, &mut xform, &mut orbit_cam),
|
||||
Orbit(delta) => do_orbit(&window, *delta, *lifecycle, &mut xform, &mut orbit_cam),
|
||||
Rotate(delta) => do_rotate(&window, *delta, &mut xform, &mut orbit_cam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if any_started {
|
||||
capture_pointer(&mut window);
|
||||
} else if any_stopped && !any_ongoing {
|
||||
release_pointer(&mut window);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This doesn't really belong in this module
|
||||
fn update_velocity(
|
||||
time: Res<Time>,
|
||||
mut q_velocity: Query<(&mut Velocity, &TargetVelocity, &mut Interp)>,
|
||||
) {
|
||||
let dt = time.delta_seconds();
|
||||
// TODO: Move these somewhere else -- component maybe?
|
||||
let accel_zero2max = 0.333;
|
||||
let max_speed = 0.075;
|
||||
|
||||
for (mut velocity, target_vel, mut t) in q_velocity.iter_mut() {
|
||||
let target_vel = target_vel.0 * max_speed;
|
||||
t.0 = (t.0 + dt / accel_zero2max).clamp(0., 1.);
|
||||
|
||||
velocity.0 = if t.0.nearly_eq(0.) {
|
||||
Vec3::ZERO
|
||||
} else if t.0.nearly_eq(1.) {
|
||||
target_vel
|
||||
} else {
|
||||
Vec3::lerp(velocity.0, target_vel, curve_sin(t.0))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn camera_control_physical(mut q_camera: Query<(&mut OrbitCam, &mut Transform, &Velocity)>) {
|
||||
let (mut orbit_cam, mut xform, velocity) = q_camera.single_mut();
|
||||
let delta_pos = xform.rotation * velocity.0;
|
||||
xform.translation += delta_pos;
|
||||
orbit_cam.focus += delta_pos;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn capture_pointer(window: &mut Window) {
|
||||
window.cursor.visible = false;
|
||||
window.cursor.grab_mode = CursorGrabMode::Confined;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn release_pointer(window: &mut Window) {
|
||||
window.cursor.visible = true;
|
||||
window.cursor.grab_mode = CursorGrabMode::None;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_translate(delta: Vec3, target_vel: &mut TargetVelocity, t: &mut Interp) {
|
||||
if !delta.nearly_eq(target_vel.0) {
|
||||
target_vel.0 = delta;
|
||||
t.0 = 0.;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_orbit(
|
||||
win: &Window,
|
||||
delta: Vec2,
|
||||
event_lifecycle: Lifecycle,
|
||||
xform: &mut Transform,
|
||||
orbit_cam: &mut OrbitCam,
|
||||
) {
|
||||
// Set `upside_down` flag if the event just started or stopped
|
||||
if matches!(event_lifecycle, Lifecycle::Start | Lifecycle::Stop) {
|
||||
orbit_cam.upside_down = xform.up().y <= 0.;
|
||||
}
|
||||
|
||||
// Translate to origin
|
||||
xform.translation += xform.forward() * orbit_cam.radius;
|
||||
|
||||
// Calculate rotation
|
||||
let delta_x = {
|
||||
let delta = delta.x / (win.physical_width() as f32) * consts::PI * 2.0;
|
||||
match orbit_cam.upside_down {
|
||||
true => -delta,
|
||||
false => delta,
|
||||
}
|
||||
};
|
||||
let delta_y = delta.y / (win.physical_height() as f32) * consts::PI;
|
||||
|
||||
let yaw = Quat::from_rotation_y(-delta_x);
|
||||
let pitch = Quat::from_rotation_x(-delta_y);
|
||||
|
||||
// Rotate around global Y and local X
|
||||
xform.rotation = yaw * xform.rotation;
|
||||
xform.rotation *= pitch;
|
||||
|
||||
// Translate back to the original position
|
||||
xform.translation += xform.back() * orbit_cam.radius;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_pan(
|
||||
proj: &Projection,
|
||||
win: &Window,
|
||||
delta: Vec2,
|
||||
xform: &mut Transform,
|
||||
orbit_cam: &mut OrbitCam,
|
||||
) {
|
||||
// Normalize the pan distance with respect to the window resolution
|
||||
let pan = match proj {
|
||||
Projection::Perspective(p) => {
|
||||
delta
|
||||
* Vec2 {
|
||||
x: (p.fov * p.aspect_ratio) / (win.physical_width() as f32),
|
||||
y: p.fov / (win.physical_height() as f32),
|
||||
}
|
||||
}
|
||||
_ => delta,
|
||||
};
|
||||
|
||||
// Pan along local XY plane, with the distance scaled by the radius
|
||||
let up = xform.up();
|
||||
let left = xform.left();
|
||||
let delta = (pan.y * up + pan.x * left) * orbit_cam.radius;
|
||||
|
||||
// Update camera translation and focus point
|
||||
xform.translation += delta;
|
||||
orbit_cam.focus += delta;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_zoom(win: &Window, delta: f32, xform: &mut Transform, orbit_cam: &mut OrbitCam) {
|
||||
let delta = (delta * 1.5) / (win.physical_height() as f32);
|
||||
orbit_cam.radius -= orbit_cam.radius * delta;
|
||||
xform.translation = orbit_cam.focus + xform.back() * orbit_cam.radius;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_rotate(win: &Window, delta: Vec2, xform: &mut Transform, orbit_cam: &mut OrbitCam) {
|
||||
// TODO: Move this somewhere else
|
||||
let sensitivity = 0.75;
|
||||
|
||||
let delta_x = delta.x / (win.physical_width() as f32) * consts::PI * 2.0 * sensitivity;
|
||||
let delta_y = delta.y / (win.physical_height() as f32) * consts::PI * sensitivity;
|
||||
|
||||
let yaw = Quat::from_rotation_y(-delta_x);
|
||||
let pitch = Quat::from_rotation_x(-delta_y);
|
||||
|
||||
// Rotate around global Y and local X
|
||||
xform.rotation = yaw * xform.rotation;
|
||||
xform.rotation *= pitch;
|
||||
|
||||
// Update focus point
|
||||
orbit_cam.focus = xform.translation + xform.forward() * orbit_cam.radius;
|
||||
}
|
||||
|
||||
/// Fit a [0, 1] `t` interpolation parameter to a sine curve
|
||||
///
|
||||
/// https://www.desmos.com/calculator/k9x4c4cr5y
|
||||
#[inline]
|
||||
fn curve_sin(t: f32) -> f32 {
|
||||
-1. * (f32::cos(t * consts::PI) / 2.) + 0.5
|
||||
}
|
||||
|
||||
trait NearlyEq {
|
||||
type Rhs;
|
||||
|
||||
fn nearly_eq(self, rhs: Self::Rhs) -> bool;
|
||||
}
|
||||
|
||||
impl NearlyEq for f32 {
|
||||
type Rhs = Self;
|
||||
|
||||
fn nearly_eq(self, rhs: Self::Rhs) -> bool {
|
||||
(self - rhs).abs() < Self::EPSILON
|
||||
}
|
||||
}
|
||||
|
||||
impl NearlyEq for Vec3 {
|
||||
type Rhs = Self;
|
||||
|
||||
fn nearly_eq(self, rhs: Self::Rhs) -> bool {
|
||||
self.x.nearly_eq(rhs.x) && self.y.nearly_eq(rhs.y) && self.z.nearly_eq(rhs.z)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,24 @@
|
||||
use bevy::{
|
||||
core_pipeline::{
|
||||
bloom::{BloomCompositeMode, BloomPrefilterSettings, BloomSettings},
|
||||
fxaa::Fxaa,
|
||||
tonemapping::Tonemapping,
|
||||
},
|
||||
math::vec3,
|
||||
pbr::NotShadowCaster,
|
||||
prelude::*,
|
||||
reflect::{TypePath, TypeUuid},
|
||||
render::render_resource::{AsBindGroup, ShaderRef},
|
||||
};
|
||||
|
||||
use crate::orbit_cam::{CameraMotionSystem, OrbitCam};
|
||||
use crate::{
|
||||
camera_controller::{CameraController, CameraControllerBundle, CameraMotionSystem},
|
||||
skybox::SkyboxId,
|
||||
};
|
||||
|
||||
pub struct TestScenePlugin;
|
||||
pub struct ScenePlugin;
|
||||
|
||||
impl Plugin for TestScenePlugin {
|
||||
impl Plugin for ScenePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(ClearColor(Color::hsl(0.0, 0.0, 0.25)))
|
||||
.add_plugins(MaterialPlugin::<ProceduralGridMaterial>::default())
|
||||
@@ -27,12 +36,41 @@ fn spawn_scene(
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut mat_proc_grid: ResMut<Assets<ProceduralGridMaterial>>,
|
||||
) {
|
||||
// Test scene
|
||||
// Skybox
|
||||
cmd.insert_resource(SkyboxId("test_scene".into()));
|
||||
|
||||
// glTF scene
|
||||
cmd.spawn(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((
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
camera: Camera {
|
||||
hdr: true,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
CameraControllerBundle::new(focus, camera_xform.translation),
|
||||
BloomSettings {
|
||||
composite_mode: BloomCompositeMode::EnergyConserving,
|
||||
intensity: 0.25,
|
||||
prefilter_settings: BloomPrefilterSettings {
|
||||
threshold: 0.8,
|
||||
threshold_softness: 0.5,
|
||||
},
|
||||
..default()
|
||||
},
|
||||
Fxaa::default(),
|
||||
));
|
||||
|
||||
// Main light
|
||||
// TODO: get Blender to export enough metadata to automate light placements
|
||||
cmd.spawn(PointLightBundle {
|
||||
@@ -95,9 +133,9 @@ fn spawn_scene(
|
||||
|
||||
fn move_plane_with_camera(
|
||||
mut plane: Query<(&WorldGridPlane, &mut Transform)>,
|
||||
camera: Query<&OrbitCam, Changed<OrbitCam>>,
|
||||
camera: Query<&CameraController, Changed<CameraController>>,
|
||||
) {
|
||||
if let Ok(&OrbitCam { focus, .. }) = camera.get_single() {
|
||||
if let Ok(&CameraController { focus, .. }) = camera.get_single() {
|
||||
let (_, mut xform) = plane.single_mut();
|
||||
|
||||
xform.translation.x = focus.x;
|
||||
+68
-56
@@ -11,79 +11,91 @@ pub struct SkyboxPlugin;
|
||||
|
||||
impl Plugin for SkyboxPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, setup)
|
||||
.add_systems(Update, asset_loaded);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
load_skybox.run_if(resource_exists_and_changed::<SkyboxId>()),
|
||||
spawn_skybox
|
||||
.run_if(resource_exists::<SkyboxAssets>())
|
||||
.run_if(skybox_assets_loaded),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
pub struct SkyboxId(pub String);
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct Cubemap {
|
||||
is_loaded: bool,
|
||||
image_handle: Handle<Image>,
|
||||
struct SkyboxAssets {
|
||||
cubemap: Handle<Image>,
|
||||
env_diffuse: Handle<Image>,
|
||||
env_specular: Handle<Image>,
|
||||
}
|
||||
|
||||
impl Cubemap {
|
||||
fn new(name: impl fmt::Display, assets: &Res<AssetServer>) -> Self {
|
||||
let paths = SkyboxPaths::new(name);
|
||||
|
||||
Self {
|
||||
is_loaded: false,
|
||||
image_handle: assets.load(paths.main),
|
||||
env_diffuse: assets.load(paths.env_diffuse),
|
||||
env_specular: assets.load(paths.env_specular),
|
||||
}
|
||||
}
|
||||
fn load_skybox(mut cmd: Commands, assets: Res<AssetServer>, id: Res<SkyboxId>) {
|
||||
let paths = SkyboxPaths::new(&**id);
|
||||
cmd.insert_resource(SkyboxAssets {
|
||||
cubemap: assets.load(paths.cubemap),
|
||||
env_diffuse: assets.load(paths.env_diffuse),
|
||||
env_specular: assets.load(paths.env_specular),
|
||||
});
|
||||
}
|
||||
|
||||
pub struct SkyboxPaths {
|
||||
pub main: String,
|
||||
pub env_diffuse: String,
|
||||
pub env_specular: String,
|
||||
fn skybox_assets_loaded(
|
||||
assets: Res<AssetServer>,
|
||||
skybox_assets: Option<Res<SkyboxAssets>>,
|
||||
) -> bool {
|
||||
let Some(skybox_assets) = skybox_assets else {
|
||||
return false;
|
||||
};
|
||||
|
||||
assets.get_load_state(skybox_assets.cubemap.clone_weak()) == LoadState::Loaded
|
||||
&& assets.get_load_state(skybox_assets.env_diffuse.clone_weak()) == LoadState::Loaded
|
||||
&& assets.get_load_state(skybox_assets.env_specular.clone_weak()) == LoadState::Loaded
|
||||
}
|
||||
|
||||
fn spawn_skybox(
|
||||
mut cmd: Commands,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
skybox_assets: Res<SkyboxAssets>,
|
||||
q_camera: Query<Entity, With<Camera3d>>,
|
||||
) {
|
||||
// Configure the skybox texture as a cubemap
|
||||
let image = images.get_mut(&skybox_assets.cubemap).unwrap();
|
||||
if image.texture_descriptor.array_layer_count() == 1 {
|
||||
image.reinterpret_stacked_2d_as_array(6);
|
||||
image.texture_view_descriptor = Some(TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::Cube),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
// Add components to the scene camera
|
||||
cmd.entity(q_camera.single()).insert((
|
||||
Skybox(skybox_assets.cubemap.clone()),
|
||||
EnvironmentMapLight {
|
||||
diffuse_map: skybox_assets.env_diffuse.clone(),
|
||||
specular_map: skybox_assets.env_specular.clone(),
|
||||
},
|
||||
));
|
||||
|
||||
// Remove the intermediate resource
|
||||
cmd.remove_resource::<SkyboxAssets>();
|
||||
}
|
||||
|
||||
struct SkyboxPaths {
|
||||
cubemap: String,
|
||||
env_diffuse: String,
|
||||
env_specular: String,
|
||||
}
|
||||
|
||||
impl SkyboxPaths {
|
||||
fn new(name: impl fmt::Display) -> Self {
|
||||
Self {
|
||||
main: format!("skyboxes/{name}.hdr"),
|
||||
cubemap: format!("skyboxes/{name}.hdr"),
|
||||
env_diffuse: format!("skyboxes/{name}_diffuse.zstd.ktx2"),
|
||||
env_specular: format!("skyboxes/{name}_specular.zstd.ktx2"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(mut cmd: Commands, assets: Res<AssetServer>) {
|
||||
cmd.insert_resource(Cubemap::new("test_scene", &assets));
|
||||
}
|
||||
|
||||
fn asset_loaded(
|
||||
assets: Res<AssetServer>,
|
||||
camera: Query<Entity, With<Camera3d>>,
|
||||
mut cmd: Commands,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut cubemap: ResMut<Cubemap>,
|
||||
) {
|
||||
if !cubemap.is_loaded
|
||||
&& assets.get_load_state(cubemap.image_handle.clone_weak()) == LoadState::Loaded
|
||||
{
|
||||
let image = images.get_mut(&cubemap.image_handle).unwrap();
|
||||
if image.texture_descriptor.array_layer_count() == 1 {
|
||||
image.reinterpret_stacked_2d_as_array(6);
|
||||
image.texture_view_descriptor = Some(TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::Cube),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
cmd.entity(camera.single()).insert((
|
||||
Skybox(cubemap.image_handle.clone()),
|
||||
EnvironmentMapLight {
|
||||
diffuse_map: cubemap.env_diffuse.clone(),
|
||||
specular_map: cubemap.env_specular.clone(),
|
||||
},
|
||||
));
|
||||
|
||||
cubemap.is_loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user