Final tidying

This commit is contained in:
Aevyrie Roessler 2023-01-29 00:05:37 -08:00
parent 280a493dcb
commit 19d6670145
No known key found for this signature in database
GPG Key ID: F975B68AD0BCCF40
7 changed files with 430 additions and 148 deletions

View File

@ -8,7 +8,6 @@ description = "A floating origin plugin for bevy"
[dependencies]
bevy = { version = "0.9", default_features = false }
bevy_rapier3d = { version = "0.19", optional = true }
bevy_polyline = "0.4"
[dev-dependencies]
@ -17,7 +16,3 @@ bevy = { version = "0.9", default_features = false, features = [
"bevy_winit",
"x11",
] }
[features]
rapier = ["bevy_rapier3d"]

135
examples/debug.rs Normal file
View File

@ -0,0 +1,135 @@
use bevy::prelude::*;
use big_space::{FloatingOrigin, GridCell};
fn main() {
App::new()
.add_plugins(DefaultPlugins.build().disable::<TransformPlugin>())
.add_plugin(big_space::FloatingOriginPlugin::<i64>::new(0.5, 0.01))
.add_plugin(big_space::debug::FloatingOriginDebugPlugin::<i64>::default())
.insert_resource(ClearColor(Color::BLACK))
.add_startup_system(setup)
.add_system(movement)
.add_system(rotation)
.run()
}
#[derive(Component)]
struct Mover<const N: usize>;
fn movement(
time: Res<Time>,
mut q: ParamSet<(
Query<&mut Transform, With<Mover<1>>>,
Query<&mut Transform, With<Mover<2>>>,
Query<&mut Transform, With<Mover<3>>>,
)>,
) {
let delta_translation = |offset: f32| -> Vec3 {
let t_1 = time.elapsed_seconds() + offset;
let dt = time.delta_seconds();
let t_0 = t_1 - dt;
let pos =
|t: f32| -> Vec3 { Vec3::new(t.cos() * 2.0, t.sin() * 2.0, (t * 1.3).sin() * 2.0) };
let p0 = pos(t_0);
let p1 = pos(t_1);
let dp = p1 - p0;
dp
};
q.p0().single_mut().translation += delta_translation(20.0);
q.p1().single_mut().translation += delta_translation(251.0);
q.p2().single_mut().translation += delta_translation(812.0);
}
#[derive(Component)]
struct Rotator;
fn rotation(time: Res<Time>, mut query: Query<&mut Transform, With<Rotator>>) {
for mut transform in &mut query {
transform.rotate_x(3.0 * time.delta_seconds());
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let mesh_handle = meshes.add(
shape::Icosphere {
radius: 0.1,
..default()
}
.try_into()
.unwrap(),
);
let matl_handle = materials.add(StandardMaterial {
base_color: Color::YELLOW,
..default()
});
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(0.0, 0.0, 1.0),
..default()
},
GridCell::<i64>::default(),
Mover::<1>,
));
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(1.0, 0.0, 0.0),
..default()
},
GridCell::<i64>::default(),
Mover::<2>,
));
commands
.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(0.0, 1.0, 0.0),
..default()
},
GridCell::<i64>::default(),
Rotator,
Mover::<3>,
))
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: mesh_handle,
material: matl_handle,
transform: Transform::from_xyz(0.0, 0.0, 1.0),
..default()
});
});
// light
commands.spawn((
PointLightBundle {
transform: Transform::from_xyz(4.0, 8.0, 4.0),
point_light: PointLight {
intensity: 10_000f32,
..default()
},
..default()
},
GridCell::<i64>::default(),
));
// camera
commands.spawn((
Camera3dBundle {
transform: Transform::from_xyz(0.0, 0.0, 8.0)
.looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
..default()
},
GridCell::<i64>::default(),
FloatingOrigin,
));
}

View File

@ -1,127 +1,27 @@
use bevy::prelude::*;
use big_space::{FloatingOrigin, GridCell};
use bevy::{
math::Vec3A,
prelude::*,
render::primitives::{Aabb, Sphere},
};
use big_space::{camera::CameraController, FloatingOrigin, GridCell};
fn main() {
App::new()
.add_plugins(DefaultPlugins.build().disable::<TransformPlugin>())
.add_plugin(big_space::FloatingOriginPlugin::<i64>::new(0.5, 0.01))
.add_plugin(big_space::debug::FloatingOriginDebugPlugin::<i64>::default())
.add_plugin(big_space::FloatingOriginPlugin::<i128>::default())
.add_plugin(big_space::debug::FloatingOriginDebugPlugin::<i128>::default())
.add_plugin(big_space::camera::CameraControllerPlugin)
.insert_resource(ClearColor(Color::BLACK))
.add_startup_system(setup)
.add_system(movement)
.add_system(rotation)
.add_system(cursor_grab_system)
.run()
}
#[derive(Component)]
struct Mover<const N: usize>;
fn movement(
time: Res<Time>,
mut q: ParamSet<(
Query<&mut Transform, With<Mover<1>>>,
Query<&mut Transform, With<Mover<2>>>,
Query<&mut Transform, With<Mover<3>>>,
)>,
) {
let delta_translation = |offset: f32| -> Vec3 {
let t_1 = time.elapsed_seconds() + offset;
let dt = time.delta_seconds();
let t_0 = t_1 - dt;
let pos =
|t: f32| -> Vec3 { Vec3::new(t.cos() * 2.0, t.sin() * 2.0, (t * 1.3).sin() * 2.0) };
let p0 = pos(t_0);
let p1 = pos(t_1);
let dp = p1 - p0;
dp
};
q.p0().single_mut().translation += delta_translation(20.0);
q.p1().single_mut().translation += delta_translation(251.0);
q.p2().single_mut().translation += delta_translation(812.0);
}
#[derive(Component)]
struct Rotator;
fn rotation(time: Res<Time>, mut query: Query<&mut Transform, With<Rotator>>) {
for mut transform in &mut query {
transform.rotate_x(3.0 * time.delta_seconds());
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let mesh_handle = meshes.add(
shape::Icosphere {
radius: 0.1,
..default()
}
.try_into()
.unwrap(),
);
let matl_handle = materials.add(StandardMaterial {
base_color: Color::YELLOW,
..default()
});
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(0.0, 0.0, 1.0),
..default()
},
GridCell::<i64>::default(),
Mover::<1>,
));
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(1.0, 0.0, 0.0),
..default()
},
GridCell::<i64>::default(),
Mover::<2>,
));
commands
.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(0.0, 1.0, 0.0),
..default()
},
GridCell::<i64>::default(),
Rotator,
Mover::<3>,
))
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: mesh_handle,
material: matl_handle,
transform: Transform::from_xyz(0.0, 0.0, 1.0),
..default()
});
});
// light
commands.spawn((
PointLightBundle {
transform: Transform::from_xyz(4.0, 8.0, 4.0),
point_light: PointLight {
intensity: 10_000f32,
..default()
},
..default()
},
GridCell::<i64>::default(),
));
// camera
commands.spawn((
Camera3dBundle {
@ -129,7 +29,73 @@ fn setup(
.looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
..default()
},
GridCell::<i64>::default(),
FloatingOrigin,
GridCell::<i128>::default(), // All spatial entities need this component
FloatingOrigin, // Important: marks this as the entity to use as teh floating origin
CameraController {
max_speed: 10e12,
smoothness: 0.9,
..default()
}, // Built-in camera controller
));
let mesh_handle = meshes.add(
shape::Icosphere {
radius: 0.5,
subdivisions: 16,
}
.try_into()
.unwrap(),
);
let matl_handle = materials.add(StandardMaterial {
base_color: Color::YELLOW,
unlit: false,
..default()
});
let mut translation = Vec3::ZERO;
for i in 1..100i128 {
let j = i.pow(10) as f32 + 1.0;
translation.x += j;
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_scale(Vec3::splat(j)).with_translation(translation),
..default()
},
Aabb::from(Sphere {
center: Vec3A::ZERO,
radius: j / 2.0,
}),
GridCell::<i128>::default(),
));
}
// light
commands.spawn((DirectionalLightBundle {
directional_light: DirectionalLight {
illuminance: 100_000.0,
..default()
},
..default()
},));
}
fn cursor_grab_system(
mut windows: ResMut<Windows>,
btn: Res<Input<MouseButton>>,
key: Res<Input<KeyCode>>,
) {
let window = windows.get_primary_mut().unwrap();
use bevy::window::CursorGrabMode;
if btn.just_pressed(MouseButton::Left) {
window.set_cursor_grab_mode(CursorGrabMode::Locked);
window.set_cursor_visibility(false);
}
if key.just_pressed(KeyCode::Escape) {
window.set_cursor_grab_mode(CursorGrabMode::None);
window.set_cursor_visibility(true);
}
}

View File

@ -26,7 +26,7 @@ fn main() {
///
/// This plugin can function much further from the origin without any issues. Try setting this to:
/// 10_000_000_000_000_000_000_000_000_000_000_000_000
const DISTANCE: f32 = 20_000_000.0;
const DISTANCE: f32 = 10_000_000.0;
/// Move the floating origin back to the "true" origin when the user presses the spacebar to emulate
/// disabling the plugin. Normally you would make your active camera the floating origin to avoid

146
src/camera.rs Normal file
View File

@ -0,0 +1,146 @@
//! Provides a camera controller compatible with the floating origin plugin.
use bevy::{
input::mouse::MouseMotion, prelude::*, render::primitives::Aabb, transform::TransformSystem,
utils::HashMap,
};
/// Adds the `big_space` camera controller
pub struct CameraControllerPlugin;
impl Plugin for CameraControllerPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<CameraInput>()
.add_system(default_camera_inputs.before(camera_controller))
.add_system_to_stage(
CoreStage::PostUpdate,
camera_controller.after(TransformSystem::TransformPropagate),
);
}
}
/// Per-camera settings for the `big_space` floating origin camera controller.
#[derive(Clone, Debug, Reflect, Component)]
pub struct CameraController {
/// Smoothness of motion, from `0.0` to `1.0`.
pub smoothness: f32,
/// Maximum possible speed.
pub max_speed: f32,
/// Whether the camera should slow down when approaching an entity's [`Aabb`].
pub slow_near_objects: bool,
}
impl Default for CameraController {
fn default() -> Self {
Self {
smoothness: 0.2,
max_speed: 10e8,
slow_near_objects: true,
}
}
}
/// Input state used to command camera motion. Reset every time the values are read to update the
/// camera. Allows you to map any input to camera motions. Uses aircraft principle axes conventions.
#[derive(Clone, Debug, Default, Reflect, Resource)]
pub struct CameraInput {
/// Z-negative
pub forward: f32,
/// Y-positive
pub up: f32,
/// X-positive
pub right: f32,
/// Positive = right wing down
pub roll: f32,
/// Positive = nose up
pub pitch: f32,
/// Positive = nose right
pub yaw: f32,
}
impl CameraInput {
/// Reset the controller back to zero to ready fro the next frame.
pub fn reset(&mut self) {
*self = CameraInput::default();
}
/// Returns the desired velocity transform.
pub fn target_velocity(&self, speed: f32, dt: f32) -> Transform {
let mut new_transform = Transform::from_rotation(Quat::from_euler(
EulerRot::XYZ,
self.pitch * dt,
self.yaw * dt,
self.roll * dt,
));
let delta = Vec3::new(self.right, self.up, self.forward) * speed * dt;
new_transform.translation = delta;
new_transform
}
}
/// Provides sensible keyboard and mouse input defaults
pub fn default_camera_inputs(
keyboard: Res<Input<KeyCode>>,
mut mouse_move: EventReader<MouseMotion>,
mut cam: ResMut<CameraInput>,
) {
keyboard.pressed(KeyCode::W).then(|| cam.forward -= 1.0);
keyboard.pressed(KeyCode::S).then(|| cam.forward += 1.0);
keyboard.pressed(KeyCode::A).then(|| cam.right -= 1.0);
keyboard.pressed(KeyCode::D).then(|| cam.right += 1.0);
keyboard.pressed(KeyCode::Space).then(|| cam.up += 1.0);
keyboard.pressed(KeyCode::LControl).then(|| cam.up -= 1.0);
keyboard.pressed(KeyCode::Q).then(|| cam.roll += 1.0);
keyboard.pressed(KeyCode::E).then(|| cam.roll -= 1.0);
if let Some(total_mouse_motion) = mouse_move.iter().map(|e| e.delta).reduce(|sum, i| sum + i) {
cam.pitch += total_mouse_motion.y * -0.1;
cam.yaw += total_mouse_motion.x * -0.1;
}
}
/// Uses [`CameraInput`] state to update the camera position.
pub fn camera_controller(
time: Res<Time>,
mut input: ResMut<CameraInput>,
mut camera: Query<(
Entity,
&mut Transform,
&GlobalTransform,
&mut CameraController,
)>,
objects: Query<(&GlobalTransform, &Aabb)>,
mut velocities: Local<HashMap<Entity, Transform>>,
) {
let (entity, mut cam_transform, cam_global_transform, controller) = camera.single_mut();
let speed = if controller.slow_near_objects {
let mut nearest_object = f32::MAX;
for (transform, aabb) in &objects {
let distance = (transform.translation() + Vec3::from(aabb.center)
- cam_global_transform.translation())
.length()
- aabb.half_extents.max_element();
nearest_object = nearest_object.min(distance);
}
nearest_object.abs().clamp(1.0, controller.max_speed)
} else {
controller.max_speed
};
let lerp_val = 1.0 - controller.smoothness.clamp(0.0, 0.99999999); // The lerp factor
let v_current = velocities.entry(entity).or_default();
let v_target = input.target_velocity(speed, time.delta_seconds());
let v_next = Transform {
translation: v_current.translation.lerp(v_target.translation, lerp_val),
rotation: v_current.rotation.slerp(v_target.rotation, lerp_val),
..default()
};
let cam_rot = cam_transform.rotation;
cam_transform.translation += cam_rot * v_next.translation;
cam_transform.rotation *= v_next.rotation;
*v_current = v_next;
input.reset();
}

View File

@ -161,7 +161,6 @@ pub fn build_cube(
let polyline = polylines.add(Polyline {
vertices: vertices.into(),
..Default::default()
});
let material = polyline_materials.add(PolylineMaterial {

View File

@ -2,17 +2,46 @@
//! observable universe, with no added dependencies, while remaining largely compatible with the
//! rest of the Bevy ecosystem.
//!
//! ## Problem
//! ### Problem
//!
//! Objects far from the origin suffer from reduced precision, causing rendered meshes to jitter and
//! jiggle, and transformation calculations to encounter catastrophic cancellation.
//!
//! ## Solution
//! As the camera moves farther from the origin, the scale of floats needed to describe the position
//! of meshes and the camera get larger, which in turn means there is less precision available.
//! Consequently, when the matrix math is done to compute the position of objects in view space,
//! mesh vertices will be displaced due to this lost precision.
//!
//! While using the [`FloatingOriginPlugin`], entities are placed into a large fixed precision grid,
//! and their [`Transform`]s are recomputed to be relative to their current grid cell. If an entity
//! moves into a neighboring cell, its transform will be recomputed. This prevents `Transforms` from
//! ever becoming larger than a single grid cell.
//! ### Solution
//!
//! While using the [`FloatingOriginPlugin`], entities are placed into a [`GridCell`] in a large
//! fixed precision grid. Inside a `GridCell`, an entity's `Transform` is relative to the center of
//! that grid cell. If an entity moves into a neighboring cell, its transform will be recomputed
//! relative to the center of that new cell. This prevents `Transforms` from ever becoming larger
//! than a single grid cell, and thus prevents floating point precision artifacts.
//!
//! The same thing happens to the entity marked with the [`FloatingOrigin`] component. The only
//! difference is that the `GridCell` of the floating origin is used when computing the
//! `GlobalTransform` of all other entities. To an outside observer, as the floating origin camera
//! moves through space and reaches the limits of its `GridCell`, it would appear to teleport to the
//! opposite side of the cell, similar to the spaceship in the game *Asteroids*.
//!
//! The `GlobalTransform` of all entities is computed relative to the floating origin's grid cell.
//! Because of this, entities very far from the origin will have very large, imprecise positions.
//! However, this is always relative to the camera (floating origin), so these artifacts will always
//! be too far away to be seen, no matter where the camera moves. Because this only affects the
//! `GlobalTransform` and not the `Transform`, this also means that entities will never permanently
//! lose precision just because they were far from the origin at some point.
//!
//! # Getting Started
//!
//! All that's needed to start using this plugin:
//! 1. Add the [`FloatingOriginPlugin`] to your `App`
//! 2. Add the [`GridCell`] component to all spatial entities
//! 3. Add the [`FloatingOrigin`] component to the active camera
//!
//! Take a look at [`FloatingOriginSettings`] resource for configuration options, as well as some
//! useful helper methods.
//!
//! # Moving Entities
//!
@ -38,28 +67,29 @@
//!
//! If you are updating the position of an entity with absolute positions, and the position exceeds
//! the bounds of the entity's grid cell, the floating origin plugin will recenter that entity into
//! its new cell. Every time you update that entity, you will be fighting with the floating origin
//! plugin as it constantly recenters your entity. This can especially cause problems with camera
//! controllers which may not expect the large discontinuity in position as an entity moves between
//! cells.
//! its new cell. Every time you update that entity, you will be fighting with the plugin as it
//! constantly recenters your entity. This can especially cause problems with camera controllers
//! which may not expect the large discontinuity in position as an entity moves between cells.
//!
//! The other reason to avoid this is you will likely run into precision issues! This plugin exists
//! because single precision is limited, and the larger the position coordinates get, the less
//! precision you have.
//!
//! However, if you have something that cannot accumulate error, like the orbit of a planet, you can
//! instead do the orbital calculation (position as a function of time) to compute the absolute
//! position of the planet, then directly compute the [`GridCell`] and [`Transform`] of that entity
//! using [`FloatingOriginSettings::translation_to_grid`]. If the star this planet is orbiting
//! around is also moving through space, note that you can add/subtract grid cells. This means you
//! can do each calculation in the reference frame of the moving body, and sum up the computed
//! translations and grid cell offsets to get a more precise result.
//! However, if you have something that must not accumulate error, like the orbit of a planet, you
//! can instead do the orbital calculation (position as a function of time) to compute the absolute
//! position of the planet with high precision, then directly compute the [`GridCell`] and
//! [`Transform`] of that entity using [`FloatingOriginSettings::translation_to_grid`]. If the star
//! this planet is orbiting around is also moving through space, note that you can add/subtract grid
//! cells. This means you can do each calculation in the reference frame of the moving body, and sum
//! up the computed translations and grid cell offsets to get a more precise result.
#![allow(clippy::type_complexity)]
#![deny(missing_docs)]
use bevy::{math::DVec3, prelude::*, transform::TransformSystem};
use std::marker::PhantomData;
pub mod camera;
pub mod debug;
pub mod precision;
@ -173,12 +203,16 @@ impl FloatingOriginSettings {
}
/// Convert a large translation into a small translation relative to a grid cell.
pub fn translation_to_grid<P: GridPrecision>(&self, input: Vec3) -> (GridCell<P>, Vec3) {
let l = self.grid_edge_length;
let Vec3 { x, y, z } = input;
pub fn translation_to_grid<P: GridPrecision>(
&self,
input: impl Into<DVec3>,
) -> (GridCell<P>, Vec3) {
let l = self.grid_edge_length as f64;
let input = input.into();
let DVec3 { x, y, z } = input;
if input.abs().max_element() < self.maximum_distance_from_origin {
return (GridCell::default(), input);
if input.abs().max_element() < self.maximum_distance_from_origin as f64 {
return (GridCell::default(), input.as_vec3());
}
let x_r = (x / l).round();
@ -190,13 +224,21 @@ impl FloatingOriginSettings {
(
GridCell {
x: P::from_f32(x_r),
y: P::from_f32(y_r),
z: P::from_f32(z_r),
x: P::from_f32(x_r as f32),
y: P::from_f32(y_r as f32),
z: P::from_f32(z_r as f32),
},
Vec3::new(t_x, t_y, t_z),
Vec3::new(t_x as f32, t_y as f32, t_z as f32),
)
}
/// Convert a large translation into a small translation relative to a grid cell.
pub fn imprecise_translation_to_grid<P: GridPrecision>(
&self,
input: Vec3,
) -> (GridCell<P>, Vec3) {
self.translation_to_grid(input.as_dvec3())
}
}
impl Default for FloatingOriginSettings {
@ -333,7 +375,7 @@ pub fn recenter_transform_on_grid<P: GridPrecision>(
> settings.maximum_distance_from_origin
{
let (grid_cell_delta, translation) =
settings.translation_to_grid(transform.as_ref().translation);
settings.imprecise_translation_to_grid(transform.as_ref().translation);
*grid_pos = *grid_pos + grid_cell_delta;
transform.translation = translation;
}
@ -376,7 +418,6 @@ fn update_global_from_cell_local<P: GridPrecision>(
) {
let grid_cell_delta = entity_cell - origin_cell;
*global = local
.clone()
.with_translation(settings.grid_position(&grid_cell_delta, local))
.into();
}
@ -450,7 +491,7 @@ pub fn transform_propagate_system<P: GridPrecision>(
changed |= changed_children;
for child in children {
let _ = propagate_recursive(
&global_transform,
global_transform,
&mut transform_query,
&children_query,
*child,