Reference Frames (#16)

Adds the concept of reference frames, allowing hierarchies of high
precision objects, e.g. objects in the reference frame of a planet,
which is itself rotating, and orbiting about a star.

---------

Co-authored-by: Oliver Scherer <github@oli-obk.de>
This commit is contained in:
Aevyrie 2024-04-13 22:33:45 -07:00 committed by GitHub
parent 0dafa2b83c
commit 2e69f80b8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1576 additions and 346 deletions

View File

@ -23,6 +23,7 @@ bevy = { version = "0.13", default-features = false, features = [
"tonemapping_luts",
] }
bevy_framepace = { version = "0.15", default-features = false }
rand = "0.8.5"
[features]
default = ["debug", "camera", "bevy_render"]
@ -47,3 +48,15 @@ name = "error"
path = "examples/error.rs"
required-features = ["default"]
doc-scrape-examples = true
[[example]]
name = "error_child"
path = "examples/error_child.rs"
required-features = ["default"]
doc-scrape-examples = true
[[example]]
name = "planets"
path = "examples/planets.rs"
required-features = ["default"]
doc-scrape-examples = true

View File

@ -1,7 +1,7 @@
#![allow(clippy::type_complexity)]
use bevy::prelude::*;
use big_space::{FloatingOrigin, GridCell};
use big_space::{reference_frame::ReferenceFrame, FloatingOrigin, GridCell};
fn main() {
App::new()
@ -25,22 +25,24 @@ fn movement(
Query<&mut Transform, With<Mover<1>>>,
Query<&mut Transform, With<Mover<2>>>,
Query<&mut Transform, With<Mover<3>>>,
Query<&mut Transform, With<Mover<4>>>,
)>,
) {
let delta_translation = |offset: f32| -> Vec3 {
let t_1 = time.elapsed_seconds() + offset;
let dt = time.delta_seconds();
let delta_translation = |offset: f32, scale: f32| -> Vec3 {
let t_1 = time.elapsed_seconds() * 0.1 + offset;
let dt = time.delta_seconds() * 0.1;
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 p0 = pos(t_0) * scale;
let p1 = pos(t_1) * scale;
p1 - p0
};
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);
q.p0().single_mut().translation += delta_translation(20.0, 1.0);
q.p1().single_mut().translation += delta_translation(251.0, 1.0);
q.p2().single_mut().translation += delta_translation(812.0, 1.0);
q.p3().single_mut().translation += delta_translation(863.0, 0.4);
}
#[derive(Component)]
@ -48,7 +50,7 @@ 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());
transform.rotate_z(3.0 * time.delta_seconds() * 0.2);
}
}
@ -92,16 +94,21 @@ fn setup(
..default()
},
GridCell::<i64>::default(),
ReferenceFrame::<i64>::new(0.2, 0.01),
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()
});
parent.spawn((
PbrBundle {
mesh: mesh_handle,
material: matl_handle,
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
GridCell::<i64>::default(),
Mover::<4>,
));
});
// light

View File

@ -1,10 +1,11 @@
use bevy::{
prelude::*,
transform::TransformSystem,
window::{CursorGrabMode, PrimaryWindow, WindowMode},
window::{CursorGrabMode, PrimaryWindow},
};
use big_space::{
camera::{CameraController, CameraInput},
propagation::IgnoreFloatingOrigin,
world_query::GridTransformReadOnly,
FloatingOrigin, GridCell,
};
@ -63,7 +64,8 @@ fn setup(
let mut translation = Vec3::ZERO;
for i in -16..=27 {
let j = 10_f32.powf(i as f32);
translation.x += j;
let k = 10_f32.powf((i - 1) as f32);
translation.x += j / 2.0 + k;
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
@ -109,6 +111,7 @@ fn ui_setup(mut commands: Commands) {
..default()
}),
BigSpaceDebugText,
IgnoreFloatingOrigin,
));
commands.spawn((
@ -129,6 +132,7 @@ fn ui_setup(mut commands: Commands) {
})
.with_text_justify(JustifyText::Center),
FunFactText,
IgnoreFloatingOrigin,
));
}
@ -150,8 +154,12 @@ fn highlight_nearest_sphere(
.circle_segments(128);
}
#[allow(clippy::type_complexity)]
fn ui_text_system(
mut debug_text: Query<&mut Text, (With<BigSpaceDebugText>, Without<FunFactText>)>,
mut debug_text: Query<
(&mut Text, &GlobalTransform),
(With<BigSpaceDebugText>, Without<FunFactText>),
>,
mut fun_text: Query<&mut Text, (With<FunFactText>, Without<BigSpaceDebugText>)>,
time: Res<Time>,
origin: Query<GridTransformReadOnly<i128>, With<FloatingOrigin>>,
@ -194,7 +202,9 @@ fn ui_text_system(
("".into(), "".into())
};
debug_text.single_mut().sections[0].value =
let mut debug_text = debug_text.single_mut();
debug_text.0.sections[0].value =
format!("{grid_text}\n{translation_text}\n{camera_text}\n{nearest_text}");
fun_text.single_mut().sections[0].value = fact_text
@ -255,14 +265,14 @@ fn cursor_grab_system(
if btn.just_pressed(MouseButton::Left) {
window.cursor.grab_mode = CursorGrabMode::Locked;
window.cursor.visible = false;
window.mode = WindowMode::BorderlessFullscreen;
// window.mode = WindowMode::BorderlessFullscreen;
cam.defaults_disabled = false;
}
if key.just_pressed(KeyCode::Escape) {
window.cursor.grab_mode = CursorGrabMode::None;
window.cursor.visible = true;
window.mode = WindowMode::Windowed;
// window.mode = WindowMode::Windowed;
cam.defaults_disabled = true;
}
}

View File

@ -6,7 +6,9 @@
//! origin when not using this plugin.
use bevy::prelude::*;
use big_space::{FloatingOrigin, FloatingOriginSettings, GridCell};
use big_space::{
reference_frame::RootReferenceFrame, FloatingOrigin, GridCell, IgnoreFloatingOrigin,
};
fn main() {
App::new()
@ -33,7 +35,7 @@ const DISTANCE: i128 = 21_000_000;
/// this issue.
fn toggle_plugin(
input: Res<ButtonInput<KeyCode>>,
settings: Res<big_space::FloatingOriginSettings>,
settings: Res<RootReferenceFrame<i128>>,
mut text: Query<&mut Text>,
mut disabled: Local<bool>,
mut floating_origin: Query<&mut GridCell<i128>, With<FloatingOrigin>>,
@ -43,7 +45,7 @@ fn toggle_plugin(
}
let mut origin_cell = floating_origin.single_mut();
let index_max = DISTANCE / settings.grid_edge_length() as i128;
let index_max = DISTANCE / settings.cell_edge_length() as i128;
let increment = index_max / 100;
let msg = if *disabled {
@ -60,7 +62,7 @@ fn toggle_plugin(
"Floating Origin Enabled"
};
let dist = index_max.saturating_sub(origin_cell.x) * settings.grid_edge_length() as i128;
let dist = index_max.saturating_sub(origin_cell.x) * settings.cell_edge_length() as i128;
let thousands = |num: i128| {
num.to_string()
@ -87,32 +89,35 @@ fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Rotator
}
fn setup_ui(mut commands: Commands) {
commands.spawn(TextBundle {
style: Style {
align_self: AlignSelf::FlexStart,
flex_direction: FlexDirection::Column,
commands.spawn((
TextBundle {
style: Style {
align_self: AlignSelf::FlexStart,
flex_direction: FlexDirection::Column,
..Default::default()
},
text: Text {
sections: vec![TextSection {
value: "hello: ".to_string(),
style: TextStyle {
font_size: 30.0,
color: Color::WHITE,
..default()
},
}],
..Default::default()
},
..Default::default()
},
text: Text {
sections: vec![TextSection {
value: "hello: ".to_string(),
style: TextStyle {
font_size: 30.0,
color: Color::WHITE,
..default()
},
}],
..Default::default()
},
..Default::default()
});
IgnoreFloatingOrigin,
));
}
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
settings: Res<FloatingOriginSettings>,
reference_frame: Res<RootReferenceFrame<i128>>,
) {
let mesh_handle = meshes.add(Sphere::new(1.5).mesh());
let matl_handle = materials.add(StandardMaterial {
@ -120,7 +125,7 @@ fn setup_scene(
..default()
});
let d = DISTANCE / settings.grid_edge_length() as i128;
let d = DISTANCE / reference_frame.cell_edge_length() as i128;
let distant_grid_cell = GridCell::<i128>::new(d, d, d);
// Normally, we would put the floating origin on the camera. However in this example, we want to

100
examples/error_child.rs Normal file
View File

@ -0,0 +1,100 @@
//! This example demonstrates error accumulating from parent to children in nested reference frames.
use bevy::{math::DVec3, prelude::*};
use big_space::{
reference_frame::{ReferenceFrame, RootReferenceFrame},
FloatingOrigin, GridCell,
};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
big_space::FloatingOriginPlugin::<i128>::default(),
big_space::camera::CameraControllerPlugin::<i128>::default(),
big_space::debug::FloatingOriginDebugPlugin::<i128>::default(),
))
.add_systems(Startup, setup_scene)
.run()
}
// The distance being used to test precision. A sphere is placed at this position, and a child is
// added in the opposite direction. This should sum to zero if we had infinite precision.
const DISTANT: DVec3 = DVec3::new(1e17, 0.0, 0.0);
const ORIGIN: DVec3 = DVec3::new(200.0, 0.0, 0.0);
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
root: Res<RootReferenceFrame<i128>>,
) {
let mesh_handle = meshes.add(Sphere::new(0.5).mesh());
let matl_handle = materials.add(StandardMaterial {
base_color: Color::rgb(0.8, 0.7, 0.6),
..default()
});
// A red sphere located at the origin
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: materials.add(Color::RED),
transform: Transform::from_translation(ORIGIN.as_vec3()),
..default()
},
GridCell::<i128>::default(),
));
let parent = root.translation_to_grid(DISTANT);
let child = root.translation_to_grid(-DISTANT + ORIGIN);
commands
.spawn((
// A sphere very far from the origin
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_translation(parent.1),
..default()
},
parent.0,
ReferenceFrame::<i128>::default(),
))
.with_children(|parent| {
// A green sphere that is a child of the sphere very far from the origin. This child is
// very far from its parent, and should be located exactly at the origin (if there was
// no floating point error). The distance from the green sphere to the red sphere is the
// error caused by float imprecision. Note that the sphere does not have any rendering
// artifacts, its position just has a fixed error.
parent.spawn((
PbrBundle {
mesh: mesh_handle,
material: materials.add(Color::GREEN),
transform: Transform::from_translation(child.1),
..default()
},
child.0,
));
});
// light
commands.spawn((
DirectionalLightBundle {
transform: Transform::from_xyz(4.0, -10.0, -4.0),
..default()
},
GridCell::<i128>::default(),
));
// camera
commands.spawn((
Camera3dBundle {
transform: Transform::from_translation(ORIGIN.as_vec3() + Vec3::new(0.0, 0.0, 8.0))
.looking_at(Vec3::ZERO, Vec3::Y),
..default()
},
GridCell::<i128>::default(),
FloatingOrigin,
big_space::camera::CameraController::default() // Built-in camera controller
.with_speed_bounds([10e-18, 10e35])
.with_smoothness(0.9, 0.8)
.with_speed(1.0),
));
}

193
examples/planets.rs Normal file
View File

@ -0,0 +1,193 @@
/// Example with spheres at the scale and distance of the earth and moon around the sun, at 1:1
/// scale. The earth is rotating on its axis, and the camera is in this reference frame, to
/// demonstrate how high precision nested reference frames work at large scales.
use bevy::{core_pipeline::bloom::BloomSettings, prelude::*, render::camera::Exposure};
use big_space::{
camera::CameraController,
reference_frame::{ReferenceFrame, RootReferenceFrame},
FloatingOrigin, GridCell,
};
use rand::Rng;
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
big_space::FloatingOriginPlugin::<i64>::default(),
big_space::debug::FloatingOriginDebugPlugin::<i64>::default(),
big_space::camera::CameraControllerPlugin::<i64>::default(),
bevy_framepace::FramepacePlugin,
))
.insert_resource(ClearColor(Color::BLACK))
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 100.0,
})
.add_systems(Startup, setup)
.add_systems(Update, rotate)
.run()
}
#[derive(Component)]
struct Rotates(f32);
fn rotate(mut rotate_query: Query<(&mut Transform, &Rotates)>) {
for (mut transform, rotates) in rotate_query.iter_mut() {
transform.rotate_local_y(rotates.0);
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
space: Res<RootReferenceFrame<i64>>,
) {
let mut sphere = |radius| meshes.add(Sphere::new(radius).mesh().ico(32).unwrap());
let star = sphere(1e10);
let star_mat = materials.add(StandardMaterial {
base_color: Color::WHITE,
emissive: Color::rgb_linear(100000., 100000., 100000.),
..default()
});
let mut rng = rand::thread_rng();
for _ in 0..500 {
commands.spawn((
GridCell::<i64>::new(
((rng.gen::<f32>() - 0.5) * 1e11) as i64,
((rng.gen::<f32>() - 0.5) * 1e11) as i64,
((rng.gen::<f32>() - 0.5) * 1e11) as i64,
),
PbrBundle {
mesh: star.clone(),
material: star_mat.clone(),
..default()
},
));
}
let sun_mat = materials.add(StandardMaterial {
base_color: Color::WHITE,
emissive: Color::rgb_linear(10000000., 10000000., 10000000.),
..default()
});
let sun_radius_m = 695_508_000.0;
commands
.spawn((
GridCell::<i64>::ZERO,
PointLightBundle {
point_light: PointLight {
intensity: 35.73e27,
range: 1e20,
radius: sun_radius_m,
shadows_enabled: true,
..default()
},
..default()
},
))
.with_children(|builder| {
builder.spawn((PbrBundle {
mesh: sphere(sun_radius_m),
material: sun_mat,
..default()
},));
});
let earth_orbit_radius_m = 149.60e9;
let earth_radius_m = 6.371e6;
let earth_mat = materials.add(StandardMaterial {
base_color: Color::BLUE,
perceptual_roughness: 0.8,
reflectance: 1.0,
..default()
});
let (earth_cell, earth_pos): (GridCell<i64>, _) =
space.imprecise_translation_to_grid(Vec3::Z * earth_orbit_radius_m);
commands
.spawn((
PbrBundle {
mesh: sphere(earth_radius_m),
material: earth_mat,
transform: Transform::from_translation(earth_pos),
..default()
},
earth_cell,
ReferenceFrame::<i64>::default(),
Rotates(0.001),
))
.with_children(|commands| {
let moon_orbit_radius_m = 385e6;
let moon_radius_m = 1.7375e6;
let moon_mat = materials.add(StandardMaterial {
base_color: Color::GRAY,
perceptual_roughness: 1.0,
reflectance: 0.0,
..default()
});
let (moon_cell, moon_pos): (GridCell<i64>, _) =
space.imprecise_translation_to_grid(Vec3::X * moon_orbit_radius_m);
commands.spawn((
PbrBundle {
mesh: sphere(moon_radius_m),
material: moon_mat,
transform: Transform::from_translation(moon_pos),
..default()
},
moon_cell,
));
let (cam_cell, cam_pos): (GridCell<i64>, _) =
space.imprecise_translation_to_grid(Vec3::X * (earth_radius_m + 1.0));
// camera
commands.spawn((
Camera3dBundle {
transform: Transform::from_translation(cam_pos)
.looking_to(Vec3::NEG_Z, Vec3::X),
camera: Camera {
hdr: true,
..default()
},
exposure: Exposure::SUNLIGHT,
..default()
},
BloomSettings::default(),
cam_cell,
FloatingOrigin, // Important: marks the floating origin entity for rendering.
CameraController::default() // Built-in camera controller
.with_speed_bounds([10e-18, 10e35])
.with_smoothness(0.9, 0.8)
.with_speed(1.0),
));
let (ball_cell, ball_pos): (GridCell<i64>, _) = space.imprecise_translation_to_grid(
Vec3::X * (earth_radius_m + 1.0) + Vec3::NEG_Z * 5.0,
);
let ball_mat = materials.add(StandardMaterial {
base_color: Color::FUCHSIA,
perceptual_roughness: 1.0,
reflectance: 0.0,
..default()
});
commands.spawn((
PbrBundle {
mesh: sphere(1.0),
material: ball_mat,
transform: Transform::from_translation(ball_pos),
..default()
},
ball_cell,
));
});
}

View File

@ -12,8 +12,8 @@ use bevy::{
use crate::{
precision::GridPrecision,
reference_frame::{local_origin::ReferenceFrames, RootReferenceFrame},
world_query::{GridTransform, GridTransformReadOnly},
FloatingOriginSettings,
};
/// Adds the `big_space` camera controller
@ -175,12 +175,14 @@ pub fn default_camera_inputs(
}
/// Find the object nearest the camera
pub fn nearest_objects<T: GridPrecision>(
settings: Res<FloatingOriginSettings>,
objects: Query<(Entity, GridTransformReadOnly<T>, &Aabb)>,
mut camera: Query<(&mut CameraController, GridTransformReadOnly<T>)>,
pub fn nearest_objects<P: GridPrecision>(
settings: Res<RootReferenceFrame<P>>,
objects: Query<(Entity, GridTransformReadOnly<P>, &Aabb)>,
mut camera: Query<(&mut CameraController, GridTransformReadOnly<P>)>,
) {
let (mut camera, cam_pos) = camera.single_mut();
let Ok((mut camera, cam_pos)) = camera.get_single_mut() else {
return;
};
let nearest_object = objects
.iter()
.map(|(entity, obj_pos, aabb)| {
@ -201,11 +203,17 @@ pub fn nearest_objects<T: GridPrecision>(
/// Uses [`CameraInput`] state to update the camera position.
pub fn camera_controller<P: GridPrecision>(
time: Res<Time>,
settings: Res<FloatingOriginSettings>,
frames: ReferenceFrames<P>,
mut input: ResMut<CameraInput>,
mut camera: Query<(GridTransform<P>, &mut CameraController)>,
mut camera: Query<(Entity, GridTransform<P>, &mut CameraController)>,
) {
for (mut position, mut controller) in camera.iter_mut() {
for (camera, mut position, mut controller) in camera.iter_mut() {
let Some(frame) = frames
.get_handle(camera)
.map(|handle| frames.resolve_handle(handle))
else {
continue;
};
let speed = match (controller.nearest_object, controller.slow_near_objects) {
(Some(nearest), true) => nearest.1.abs(),
_ => controller.speed,
@ -224,7 +232,7 @@ pub fn camera_controller<P: GridPrecision>(
let vel_t_next = cam_rot * vel_t_target; // Orients the translation to match the camera
let vel_t_next = vel_t_current.lerp(vel_t_next, lerp_translation);
// Convert the high precision translation to a grid cell and low precision translation
let (cell_offset, new_translation) = settings.translation_to_grid(vel_t_next);
let (cell_offset, new_translation) = frame.translation_to_grid(vel_t_next);
*position.cell += cell_offset;
position.transform.translation += new_translation;

View File

@ -4,7 +4,11 @@ use std::marker::PhantomData;
use bevy::prelude::*;
use crate::{precision::GridPrecision, FloatingOrigin, FloatingOriginSettings, GridCell};
use crate::{
precision::GridPrecision,
reference_frame::{local_origin::ReferenceFrames, ReferenceFrame},
FloatingOrigin, GridCell,
};
/// This plugin will render the bounds of occupied grid cells.
#[derive(Default)]
@ -13,9 +17,9 @@ impl<P: GridPrecision> Plugin for FloatingOriginDebugPlugin<P> {
fn build(&self, app: &mut App) {
app.add_systems(
PostUpdate,
update_debug_bounds::<P>
.after(crate::recenter_transform_on_grid::<P>)
.before(crate::update_global_from_grid::<P>),
(update_debug_bounds::<P>, update_reference_frame_axes::<P>)
.chain()
.after(bevy::transform::TransformSystem::TransformPropagate),
);
}
}
@ -23,20 +27,31 @@ impl<P: GridPrecision> Plugin for FloatingOriginDebugPlugin<P> {
/// Update the rendered debug bounds to only highlight occupied [`GridCell`]s.
pub fn update_debug_bounds<P: GridPrecision>(
mut gizmos: Gizmos,
settings: Res<FloatingOriginSettings>,
occupied_cells: Query<&GridCell<P>, Without<FloatingOrigin>>,
origin_cells: Query<&GridCell<P>, With<FloatingOrigin>>,
reference_frames: ReferenceFrames<P>,
occupied_cells: Query<(Entity, &GridCell<P>), Without<FloatingOrigin>>,
) {
let Ok(origin_cell) = origin_cells.get_single() else {
return;
};
for cell in occupied_cells.iter() {
let cell = cell - origin_cell;
let scale = Vec3::splat(settings.grid_edge_length * 0.999);
let translation = settings.grid_position(&cell, &Transform::IDENTITY);
gizmos.cuboid(
Transform::from_translation(translation).with_scale(scale),
Color::GREEN,
)
for (cell_entity, cell) in occupied_cells.iter() {
let Some(frame) = reference_frames.get(cell_entity) else {
continue;
};
let transform = frame.global_transform(
cell,
&Transform::from_scale(Vec3::splat(frame.cell_edge_length() * 0.999)),
);
gizmos.cuboid(transform, Color::GREEN)
}
}
/// Draw axes for reference frames.
pub fn update_reference_frame_axes<P: GridPrecision>(
mut gizmos: Gizmos,
frames: Query<(&GlobalTransform, &ReferenceFrame<P>)>,
) {
for (transform, frame) in frames.iter() {
let start = transform.translation();
let len = frame.cell_edge_length() * 1.0;
gizmos.ray(start, transform.right() * len, Color::RED);
gizmos.ray(start, transform.up() * len, Color::GREEN);
gizmos.ray(start, transform.back() * len, Color::BLUE);
}
}

View File

@ -1,5 +1,5 @@
//! This [`bevy`] plugin makes it easy to build high-precision worlds that exceed the size of the
//! observable universe, with no added dependencies, while remaining largely compatible with the
//! This [`bevy`] plugin makes it possible to build high-precision worlds that exceed the size of
//! the observable universe, with no added dependencies, while remaining largely compatible with the
//! rest of the Bevy ecosystem.
//!
//! ### Problem
@ -14,15 +14,25 @@
//!
//! ### 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.
//! While using the [`FloatingOriginPlugin`], the position of entities is now defined with the
//! [`ReferenceFrame`], [`GridCell`], and [`Transform`] components. The `ReferenceFrame` is a large
//! integer grid of cells; entities are located within this grid using the `GridCell` component.
//! Finally, the `Transform` is used to position the entity relative to the center of its
//! `GridCell`. If an entity moves into a neighboring cell, its transform will be automatically
//! 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
//! `ReferenceFrame`s can also be nested. This allows you to define moving reference frames, which
//! can make certain use cases much simpler. For example, if you have a planet rotating, and
//! orbiting around its star, it would be very annoying if you had to compute this orbit and
//! rotation for all object on the surface in high precision. Instead, you can place the planet and
//! all objects on its surface in the same reference frame. The motion of the planet will be
//! inherited by all children in that reference frame, in high precision. Entities at the root of
//! the hierarchy will be implicitly placed in the [`RootReferenceFrame`].
//!
//! The above steps are also applied 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*.
//!
@ -31,17 +41,20 @@
//! 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.
//! lose precision just because they were far from the origin at some point. The lossy calculation
//! only occurs when computing the `GlobalTransform` of entities, the high precision `GridCell` and
//! `Transform` are unaffected.
//!
//! # Getting Started
//!
//! All that's needed to start using this plugin:
//! To start using this plugin:
//! 1. Disable Bevy's transform plugin: `DefaultPlugins.build().disable::<TransformPlugin>()`
//! 2. Add the [`FloatingOriginPlugin`] to your `App`
//! 3. Add the [`GridCell`] component to all spatial entities
//! 4. Add the [`FloatingOrigin`] component to the active camera
//! 5. Add the [`IgnoreFloatingOrigin`] component
//!
//! Take a look at [`FloatingOriginSettings`] resource for some useful helper methods.
//! Take a look at [`ReferenceFrame`] component for some useful helper methods.
//!
//! # Moving Entities
//!
@ -78,7 +91,7 @@
//! 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
//! [`Transform`] of that entity using [`ReferenceFrame::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.
@ -86,17 +99,20 @@
#![allow(clippy::type_complexity)]
#![deny(missing_docs)]
use bevy::{math::DVec3, prelude::*, transform::TransformSystem};
use propagation::propagate_transforms;
use bevy::{prelude::*, transform::TransformSystem};
use propagation::{propagate_transforms, sync_simple_transforms};
use reference_frame::local_origin::ReferenceFrames;
use std::marker::PhantomData;
use world_query::{GridTransformReadOnly, GridTransformReadOnlyItem};
use world_query::GridTransformReadOnly;
pub mod grid_cell;
pub mod precision;
pub mod propagation;
pub mod reference_frame;
pub mod world_query;
pub use grid_cell::GridCell;
pub use propagation::IgnoreFloatingOrigin;
#[cfg(feature = "debug")]
pub mod debug;
@ -106,6 +122,10 @@ pub mod camera;
use precision::*;
use crate::reference_frame::{
local_origin::LocalFloatingOrigin, ReferenceFrame, RootReferenceFrame,
};
/// Add this plugin to your [`App`] for floating origin functionality.
pub struct FloatingOriginPlugin<P: GridPrecision> {
/// The edge length of a single cell.
@ -136,130 +156,44 @@ impl<P: GridPrecision> FloatingOriginPlugin<P> {
impl<P: GridPrecision + Reflect + FromReflect + TypePath> Plugin for FloatingOriginPlugin<P> {
fn build(&self, app: &mut App) {
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
struct RootGlobalTransformUpdates;
enum FloatingOriginSet {
RecenterLargeTransforms,
LocalFloatingOrigins,
RootGlobalTransforms,
PropagateTransforms,
}
app.insert_resource(FloatingOriginSettings::new(
let system_set_config = || {
(
recenter_transform_on_grid::<P>.in_set(FloatingOriginSet::RecenterLargeTransforms),
LocalFloatingOrigin::<P>::update
.in_set(FloatingOriginSet::LocalFloatingOrigins)
.after(FloatingOriginSet::RecenterLargeTransforms),
(
sync_simple_transforms::<P>,
update_grid_cell_global_transforms::<P>,
)
.in_set(FloatingOriginSet::RootGlobalTransforms)
.after(FloatingOriginSet::LocalFloatingOrigins),
propagate_transforms::<P>
.in_set(FloatingOriginSet::PropagateTransforms)
.after(FloatingOriginSet::RootGlobalTransforms),
)
.in_set(TransformSystem::TransformPropagate)
};
app.insert_resource(RootReferenceFrame::<P>(ReferenceFrame::new(
self.grid_edge_length,
self.switching_threshold,
))
)))
.register_type::<Transform>()
.register_type::<GlobalTransform>()
.register_type::<GridCell<P>>()
.register_type::<ReferenceFrame<P>>()
.register_type::<RootReferenceFrame<P>>()
.add_plugins(ValidParentCheckPlugin::<GlobalTransform>::default())
.add_systems(
PostStartup,
(
recenter_transform_on_grid::<P>.before(RootGlobalTransformUpdates),
(sync_simple_transforms::<P>, update_global_from_grid::<P>)
.in_set(RootGlobalTransformUpdates),
propagate_transforms::<P>.after(RootGlobalTransformUpdates),
)
.in_set(TransformSystem::TransformPropagate),
)
.add_systems(
PostUpdate,
(
recenter_transform_on_grid::<P>.before(RootGlobalTransformUpdates),
(sync_simple_transforms::<P>, update_global_from_grid::<P>)
.in_set(RootGlobalTransformUpdates),
propagate_transforms::<P>.after(RootGlobalTransformUpdates),
)
.in_set(TransformSystem::TransformPropagate),
);
}
}
/// Configuration settings for the floating origin plugin.
#[derive(Reflect, Clone, Resource)]
pub struct FloatingOriginSettings {
grid_edge_length: f32,
maximum_distance_from_origin: f32,
}
impl FloatingOriginSettings {
/// Construct a new [`FloatingOriginSettings`] struct. This cannot be updated after the plugin
/// is built.
pub fn new(grid_edge_length: f32, switching_threshold: f32) -> Self {
Self {
grid_edge_length,
maximum_distance_from_origin: grid_edge_length / 2.0 + switching_threshold,
}
}
/// Get the plugin's `grid_edge_length`.
pub fn grid_edge_length(&self) -> f32 {
self.grid_edge_length
}
/// Get the plugin's `maximum_distance_from_origin`.
pub fn maximum_distance_from_origin(&self) -> f32 {
self.maximum_distance_from_origin
}
/// Compute the double precision position of an entity's [`Transform`] with respect to the given
/// [`GridCell`].
pub fn grid_position_double<P: GridPrecision>(
&self,
pos: &GridCell<P>,
transform: &Transform,
) -> DVec3 {
DVec3 {
x: pos.x.as_f64() * self.grid_edge_length as f64 + transform.translation.x as f64,
y: pos.y.as_f64() * self.grid_edge_length as f64 + transform.translation.y as f64,
z: pos.z.as_f64() * self.grid_edge_length as f64 + transform.translation.z as f64,
}
}
/// Compute the single precision position of an entity's [`Transform`] with respect to the given
/// [`GridCell`].
pub fn grid_position<P: GridPrecision>(
&self,
pos: &GridCell<P>,
transform: &Transform,
) -> Vec3 {
Vec3 {
x: pos.x.as_f64() as f32 * self.grid_edge_length + transform.translation.x,
y: pos.y.as_f64() as f32 * self.grid_edge_length + transform.translation.y,
z: pos.z.as_f64() as f32 * self.grid_edge_length + transform.translation.z,
}
}
/// Convert a large translation into a small translation relative to a grid cell.
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 as f64 {
return (GridCell::default(), input.as_vec3());
}
let x_r = (x / l).round();
let y_r = (y / l).round();
let z_r = (z / l).round();
let t_x = x - x_r * l;
let t_y = y - y_r * l;
let t_z = z - z_r * l;
(
GridCell {
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 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())
.add_systems(PostStartup, system_set_config())
.add_systems(PostUpdate, system_set_config());
}
}
@ -293,86 +227,57 @@ pub struct FloatingOrigin;
/// If an entity's transform becomes larger than the specified limit, it is relocated to the nearest
/// grid cell to reduce the size of the transform.
pub fn recenter_transform_on_grid<P: GridPrecision>(
settings: Res<FloatingOriginSettings>,
mut query: Query<(&mut GridCell<P>, &mut Transform), (Changed<Transform>, Without<Parent>)>,
reference_frames: ReferenceFrames<P>,
mut changed_transform: Query<(Entity, &mut GridCell<P>, &mut Transform), Changed<Transform>>,
) {
query
changed_transform
.par_iter_mut()
.for_each(|(mut grid_pos, mut transform)| {
.for_each(|(entity, mut grid_pos, mut transform)| {
let Some(frame) = reference_frames
.get_handle(entity)
.map(|handle| reference_frames.resolve_handle(handle))
else {
return;
};
if transform.as_ref().translation.abs().max_element()
> settings.maximum_distance_from_origin
> frame.maximum_distance_from_origin()
{
let (grid_cell_delta, translation) =
settings.imprecise_translation_to_grid(transform.as_ref().translation);
frame.imprecise_translation_to_grid(transform.as_ref().translation);
*grid_pos += grid_cell_delta;
transform.translation = translation;
}
});
}
/// Compute the `GlobalTransform` relative to the floating origin's cell.
pub fn update_global_from_grid<P: GridPrecision>(
settings: Res<FloatingOriginSettings>,
origin: Query<(Ref<GridCell<P>>, Ref<FloatingOrigin>)>,
/// Update the `GlobalTransform` of entities with a [`GridCell`], using the [`ReferenceFrame`] the
/// entity belongs to.
pub fn update_grid_cell_global_transforms<P: GridPrecision>(
root: Res<RootReferenceFrame<P>>,
reference_frames: Query<(&ReferenceFrame<P>, &Children)>,
mut entities: ParamSet<(
Query<
(GridTransformReadOnly<P>, &mut GlobalTransform),
Or<(Changed<GridCell<P>>, Changed<Transform>)>,
>,
Query<(GridTransformReadOnly<P>, &mut GlobalTransform)>,
Query<(GridTransformReadOnly<P>, &mut GlobalTransform), With<Parent>>, // Node entities
Query<(GridTransformReadOnly<P>, &mut GlobalTransform), Without<Parent>>, // Root entities
)>,
) {
let Ok((origin_cell, floating_origin)) = origin.get_single() else {
return;
};
if origin_cell.is_changed() || floating_origin.is_changed() {
let mut all_entities = entities.p1();
all_entities.par_iter_mut().for_each(|(local, global)| {
update_global_from_cell_local(&settings, &origin_cell, local, global);
});
} else {
let mut moved_cell_entities = entities.p0();
moved_cell_entities
.par_iter_mut()
.for_each(|(local, global)| {
update_global_from_cell_local(&settings, &origin_cell, local, global);
});
}
}
fn update_global_from_cell_local<P: GridPrecision>(
settings: &FloatingOriginSettings,
origin_cell: &GridCell<P>,
local: GridTransformReadOnlyItem<P>,
mut global: Mut<GlobalTransform>,
) {
let grid_cell_delta = *local.cell - *origin_cell;
*global = local
.transform
.with_translation(settings.grid_position(&grid_cell_delta, local.transform))
.into();
}
/// Update [`GlobalTransform`] component of entities that aren't in the hierarchy
///
/// Third party plugins should ensure that this is used in concert with [`propagate_transforms`].
pub fn sync_simple_transforms<P: GridPrecision>(
mut query: Query<
(&Transform, &mut GlobalTransform),
(
Changed<Transform>,
Without<Parent>,
Without<Children>,
Without<GridCell<P>>,
),
>,
) {
query
// Update the GlobalTransform of GridCell entities at the root of the hierarchy
entities
.p1()
.par_iter_mut()
.for_each(|(transform, mut global_transform)| {
*global_transform = GlobalTransform::from(*transform);
.for_each(|(grid_transform, mut global_transform)| {
*global_transform =
root.global_transform(grid_transform.cell, grid_transform.transform);
});
// Update the GlobalTransform of GridCell entities that are children of a ReferenceFrame
for (frame, children) in &reference_frames {
let mut with_parent_query = entities.p0();
let mut frame_children = with_parent_query.iter_many_mut(children);
while let Some((grid_transform, mut global_transform)) = frame_children.fetch_next() {
*global_transform =
frame.global_transform(grid_transform.cell, grid_transform.transform);
}
}
}
#[cfg(test)]

View File

@ -1,95 +1,169 @@
//! Propagates transforms through the entity hierarchy.
//!
//! This is a slightly modified version of Bevy's own transform propagation system.
//! This is a modified version of Bevy's own transform propagation system.
use crate::{precision::GridPrecision, FloatingOrigin, GridCell};
use crate::{
precision::GridPrecision,
reference_frame::{ReferenceFrame, RootReferenceFrame},
GridCell,
};
use bevy::prelude::*;
/// Update [`GlobalTransform`] component of entities based on entity hierarchy and
/// [`Transform`] component.
/// Entities with this component will ignore the floating origin, and will instead propagate
/// transforms normally.
#[derive(Component, Debug, Reflect)]
pub struct IgnoreFloatingOrigin;
/// Update [`GlobalTransform`] component of entities that aren't in the hierarchy.
pub fn sync_simple_transforms<P: GridPrecision>(
root: Res<RootReferenceFrame<P>>,
mut query: ParamSet<(
Query<
(&Transform, &mut GlobalTransform, Has<IgnoreFloatingOrigin>),
(
Or<(Changed<Transform>, Added<GlobalTransform>)>,
Without<Parent>,
Without<Children>,
Without<GridCell<P>>,
),
>,
Query<
(
Ref<Transform>,
&mut GlobalTransform,
Has<IgnoreFloatingOrigin>,
),
(Without<Parent>, Without<Children>, Without<GridCell<P>>),
>,
)>,
mut orphaned: RemovedComponents<Parent>,
) {
// Update changed entities.
query.p0().par_iter_mut().for_each(
|(transform, mut global_transform, ignore_floating_origin)| {
if ignore_floating_origin {
*global_transform = GlobalTransform::from(*transform);
} else {
*global_transform = root.global_transform(&GridCell::ZERO, transform);
}
},
);
// Update orphaned entities.
let mut query = query.p1();
let mut iter = query.iter_many_mut(orphaned.read());
while let Some((transform, mut global_transform, ignore_floating_origin)) = iter.fetch_next() {
if !transform.is_changed() && !global_transform.is_added() {
if ignore_floating_origin {
*global_transform = GlobalTransform::from(*transform);
} else {
*global_transform = root.global_transform(&GridCell::ZERO, &transform);
}
}
}
}
/// Update the [`GlobalTransform`] of entities with a [`Transform`] that are children of a
/// [`ReferenceFrame`] and do not have a [`GridCell`] component, or that are children of
/// [`GridCell`]s.
pub fn propagate_transforms<P: GridPrecision>(
origin_moved: Query<
(),
(
Or<(Changed<GridCell<P>>, Changed<FloatingOrigin>)>,
With<FloatingOrigin>,
),
frames: Query<&Children, With<ReferenceFrame<P>>>,
frame_child_query: Query<(Entity, &Children, &GlobalTransform), With<GridCell<P>>>,
root_frame_query: Query<
(Entity, &Children, &GlobalTransform),
(With<GridCell<P>>, Without<Parent>),
>,
mut root_query: Query<
root_frame: Res<RootReferenceFrame<P>>,
mut root_frame_gridless_query: Query<
(
Entity,
&Children,
Ref<Transform>,
&Transform,
&mut GlobalTransform,
Option<Ref<GridCell<P>>>,
Has<IgnoreFloatingOrigin>,
),
(Without<GridCell<P>>, Without<Parent>),
>,
transform_query: Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
(
With<Parent>,
Without<GridCell<P>>,
Without<ReferenceFrame<P>>,
),
Without<Parent>,
>,
transform_query: Query<(Ref<Transform>, &mut GlobalTransform, Option<&Children>), With<Parent>>,
parent_query: Query<(Entity, Ref<Parent>)>,
) {
let origin_cell_changed = !origin_moved.is_empty();
for (entity, children, transform, mut global_transform, cell) in root_query.iter_mut() {
let cell_changed = cell.as_ref().filter(|cell| cell.is_changed()).is_some();
let transform_changed = transform.is_changed();
if transform_changed && cell.is_none() {
*global_transform = GlobalTransform::from(*transform);
}
let changed = transform_changed || cell_changed || origin_cell_changed;
let update_transforms = |(entity, children, global_transform)| {
for (child, actual_parent) in parent_query.iter_many(children) {
assert_eq!(
actual_parent.get(), entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
// SAFETY:
// - `child` must have consistent parentage, or the above assertion would panic.
// Since `child` is parented to a root entity, the entire hierarchy leading to it is consistent.
// - We may operate as if all descendants are consistent, since `propagate_recursive` will panic before
// continuing to propagate if it encounters an entity with inconsistent parentage.
// - `child` must have consistent parentage, or the above assertion would panic. Since
// `child` is parented to a root entity, the entire hierarchy leading to it is
// consistent.
// - We may operate as if all descendants are consistent, since `propagate_recursive`
// will panic before continuing to propagate if it encounters an entity with
// inconsistent parentage.
// - Since each root entity is unique and the hierarchy is consistent and forest-like,
// other root entities' `propagate_recursive` calls will not conflict with this one.
// - Since this is the only place where `transform_query` gets used, there will be no conflicting fetches elsewhere.
// - Since this is the only place where `transform_query` gets used, there will be no
// conflicting fetches elsewhere.
unsafe {
propagate_recursive(
&global_transform,
&transform_query,
&parent_query,
child,
changed || actual_parent.is_changed(),
);
propagate_recursive(&global_transform, &transform_query, &parent_query, child);
}
}
}
};
frames.par_iter().for_each(|children| {
children
.iter()
.filter_map(|child| frame_child_query.get(*child).ok())
.for_each(|(e, c, g)| update_transforms((e, c, *g)))
});
root_frame_query
.par_iter()
.for_each(|(e, c, g)| update_transforms((e, c, *g)));
root_frame_gridless_query.par_iter_mut().for_each(
|(entity, children, local, mut global, ignore_floating_origin)| {
if ignore_floating_origin {
*global = GlobalTransform::from(*local);
} else {
*global = root_frame.global_transform(&GridCell::ZERO, local);
}
update_transforms((entity, children, *global))
},
);
}
/// COPIED EXACTLY FROM BEVY
/// COPIED FROM BEVY
///
/// Recursively propagates the transforms for `entity` and all of its descendants.
///
/// # Panics
///
/// If `entity`'s descendants have a malformed hierarchy, this function will panic occur before propagating
/// the transforms of any malformed entities and their descendants.
/// If `entity`'s descendants have a malformed hierarchy, this function will panic occur before
/// propagating the transforms of any malformed entities and their descendants.
///
/// # Safety
///
/// - While this function is running, `transform_query` must not have any fetches for `entity`,
/// nor any of its descendants.
/// - The caller must ensure that the hierarchy leading to `entity`
/// is well-formed and must remain as a tree or a forest. Each entity must have at most one parent.
unsafe fn propagate_recursive(
/// - While this function is running, `transform_query` must not have any fetches for `entity`, nor
/// any of its descendants.
/// - The caller must ensure that the hierarchy leading to `entity` is well-formed and must remain
/// as a tree or a forest. Each entity must have at most one parent.
unsafe fn propagate_recursive<P: GridPrecision>(
parent: &GlobalTransform,
transform_query: &Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
With<Parent>,
(
With<Parent>,
Without<GridCell<P>>,
Without<ReferenceFrame<P>>,
),
>,
parent_query: &Query<(Entity, Ref<Parent>)>,
entity: Entity,
mut changed: bool,
) {
let (global_matrix, children) = {
let Ok((transform, mut global_transform, children)) =
@ -123,10 +197,8 @@ unsafe fn propagate_recursive(
return;
};
changed |= transform.is_changed();
if changed {
*global_transform = parent.mul_transform(*transform);
}
*global_transform = parent.mul_transform(*transform);
(*global_transform, children)
};
@ -142,13 +214,7 @@ unsafe fn propagate_recursive(
// The above assertion ensures that each child has one and only one unique parent throughout the
// entire hierarchy.
unsafe {
propagate_recursive(
&global_matrix,
transform_query,
parent_query,
child,
changed || actual_parent.is_changed(),
);
propagate_recursive(&global_matrix, transform_query, parent_query, child);
}
}
}

View File

@ -0,0 +1,735 @@
//! Describes how the floating origin's position is propagated through the hierarchy of reference
//! frames, and used to compute the floating origin's position relative to each reference frame.
use bevy::{
ecs::{
prelude::*,
system::{
lifetimeless::{Read, Write},
SystemParam,
},
},
hierarchy::prelude::*,
log::prelude::*,
math::{prelude::*, DAffine3, DQuat},
transform::prelude::*,
};
use super::{ReferenceFrame, RootReferenceFrame};
use crate::{FloatingOrigin, GridCell, GridPrecision};
pub use inner::LocalFloatingOrigin;
/// A module kept private to enforce use of setters and getters within the parent module.
mod inner {
use bevy::{
math::{prelude::*, DAffine3, DMat3, DQuat},
reflect::prelude::*,
};
use crate::{GridCell, GridPrecision};
/// An isometry that describes the location of the floating origin's grid cell's origin, in the
/// local reference frame.
///
/// Used to compute the [`GlobalTransform`](bevy::transform::components::GlobalTransform) of
/// every entity within a reference frame. Because this tells us where the floating origin cell
/// is located in the local frame, we can compute the inverse transform once, then use it to
/// transform every entity relative to the floating origin.
///
/// If the floating origin is in this local reference frame, the `float` fields will be
/// identity. The `float` fields` will be non-identity when the floating origin is in a
/// different reference frame that does not perfectly align with this one. Different reference
/// frames can be rotated and offset from each other - consider the reference frame of a planet,
/// spinning about its axis and orbiting about a star, it will not align with the reference
/// frame of the star system!
#[derive(Default, Debug, Clone, PartialEq, Reflect)]
pub struct LocalFloatingOrigin<P: GridPrecision> {
/// The local cell that the floating origin's grid cell origin falls into.
cell: GridCell<P>,
/// The translation of floating origin's grid cell relative to the origin of
/// [`LocalFloatingOrigin::cell`].
translation: Vec3,
/// The rotation of the floating origin's grid cell relative to the origin of
/// [`LocalFloatingOrigin::cell`].
rotation: DQuat,
/// Transform from the local reference frame to the floating origin's grid cell. This is
/// used to compute the `GlobalTransform` of all entities in this reference frame.
///
/// Imagine you have the local reference frame and the floating origin's reference frame
/// overlapping in space, misaligned. This transform is the smallest possible that will
/// align the two reference frame grids, going from the local frame, to the floating
/// origin's frame.
///
/// This is like a camera's "view transform", but instead of transforming an object into a
/// camera's view space, this will transform an object into the floating origin's reference
/// frame.
/// - That object must be positioned in the same [`super::ReferenceFrame`] that this
/// [`LocalFloatingOrigin`] is part of.
/// - That object's position must be relative to the same grid cell as defined by
/// [`Self::cell`].
///
/// The above requirements help to ensure this transform has a small magnitude, maximizing
/// precision, and minimizing floating point error.
reference_frame_transform: DAffine3,
}
impl<P: GridPrecision> LocalFloatingOrigin<P> {
/// The reference frame transform from the local reference frame, to the floating origin's
/// reference frame. See [Self::reference_frame_transform].
pub fn reference_frame_transform(&self) -> DAffine3 {
self.reference_frame_transform
}
/// Gets [`Self::cell`].
pub fn cell(&self) -> GridCell<P> {
self.cell
}
/// Gets [`Self::translation`].
pub fn translation(&self) -> Vec3 {
self.translation
}
/// Gets [`Self::rotation`].
pub fn rotation(&self) -> DQuat {
self.rotation
}
/// Update this local floating origin, and compute the new inverse transform.
pub fn set(
&mut self,
translation_grid: GridCell<P>,
translation_float: Vec3,
rotation_float: DQuat,
) {
self.cell = translation_grid;
self.translation = translation_float;
self.rotation = rotation_float;
self.reference_frame_transform = DAffine3 {
matrix3: DMat3::from_quat(self.rotation),
translation: self.translation.as_dvec3(),
}
.inverse()
}
/// Create a new [`LocalFloatingOrigin`].
pub fn new(cell: GridCell<P>, translation: Vec3, rotation: DQuat) -> Self {
let reference_frame_transform = DAffine3 {
matrix3: DMat3::from_quat(rotation),
translation: translation.as_dvec3(),
}
.inverse();
Self {
cell,
translation,
rotation,
reference_frame_transform,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
/// Use the [`ReferenceFrames`] [`SystemParam`] to do useful things with this handle.
///
/// A reference frame can either be a node in the entity hierarchy stored as a component, or will be
/// the root reference frame, which is tracked with a resource. This handle is used to unify access
/// to reference frames with a single lightweight type.
pub enum ReferenceFrameHandle {
/// The reference frame is a node in the hierarchy, stored in a [`ReferenceFrame`] component.
Node(Entity),
/// The root reference frame, defined in the [`RootReferenceFrame`] resource.
Root,
}
impl ReferenceFrameHandle {
/// Propagate the local origin position from `self` to `child`.
///
/// This is not a method on `References` to help prevent misuse when accidentally
/// swapping the position of arguments.
fn propagate_origin_to_child<P: GridPrecision>(
self,
reference_frames: &mut ReferenceFramesMut<P>,
child: ReferenceFrameHandle,
) {
let (this_frame, _this_cell, _this_transform) = reference_frames.get(self);
let (child_frame, child_cell, child_transform) = reference_frames.get(child);
// compute double precision translation of origin treating child as the origin grid cell. Add this to the origin's float translation in double,
let origin_cell_relative_to_child = this_frame.local_floating_origin.cell() - child_cell;
let origin_translation = this_frame.grid_position_double(
&origin_cell_relative_to_child,
&Transform::from_translation(this_frame.local_floating_origin.translation()),
);
// then combine with rotation to get a double transform from the child's cell origin to the origin.
let origin_rotation = this_frame.local_floating_origin.rotation();
let origin_transform_child_cell_local =
DAffine3::from_rotation_translation(origin_rotation, origin_translation);
// Take the inverse of the child's transform as double (this is the "view" transform of the child reference frame)
let child_view_child_cell_local = DAffine3::from_rotation_translation(
child_transform.rotation.as_dquat(),
child_transform.translation.as_dvec3(),
)
.inverse();
// then multiply this by the double transform we got of the origin. This is now a transform64 of the origin, wrt to the child.
let origin_child_affine = child_view_child_cell_local * origin_transform_child_cell_local;
// We can decompose into translation (high precision) and rotation.
let (_, origin_child_rotation, origin_child_translation) =
origin_child_affine.to_scale_rotation_translation();
let (child_origin_cell, child_origin_translation_float) =
child_frame.translation_to_grid(origin_child_translation);
reference_frames.update(child, |child_frame, _, _| {
child_frame.local_floating_origin.set(
child_origin_cell,
child_origin_translation_float,
origin_child_rotation,
);
})
}
fn propagate_origin_to_parent<P: GridPrecision>(
self,
reference_frames: &mut ReferenceFramesMut<P>,
parent: ReferenceFrameHandle,
) {
let (this_frame, this_cell, this_transform) = reference_frames.get(self);
let (parent_frame, _parent_cell, _parent_transform) = reference_frames.get(parent);
// Get this frame's double precision transform, relative to its cell. We ignore the grid
// cell here because we don't want to lose precision - we can do these calcs relative to
// this cell, then add the grid cell offset at the end.
let this_transform = DAffine3::from_rotation_translation(
this_transform.rotation.as_dquat(),
this_transform.translation.as_dvec3(),
);
// Get the origin's double position in this reference frame
let origin_translation = this_frame.grid_position_double(
&this_frame.local_floating_origin.cell(),
&Transform::from_translation(this_frame.local_floating_origin.translation()),
);
let this_local_origin_transform = DAffine3::from_rotation_translation(
this_frame.local_floating_origin.rotation(),
origin_translation,
);
// Multiply to move the origin into the parent's reference frame
let origin_affine = this_transform * this_local_origin_transform;
let (_, origin_rot, origin_trans) = origin_affine.to_scale_rotation_translation();
let (origin_cell_relative_to_this_cell, origin_translation_remainder) =
parent_frame.translation_to_grid(origin_trans);
// Up until now we have been computing as if this cell is located at the origin, to maximize
// precision. Now that we are done with floats, we can add the cell offset.
let parent_origin_cell = origin_cell_relative_to_this_cell + this_cell;
reference_frames.update(parent, |parent_frame, _, _| {
parent_frame.local_floating_origin.set(
parent_origin_cell,
origin_translation_remainder,
origin_rot,
);
});
}
}
/// Used to access a reference frame using a single system param. Needed because the reference frame
/// could either be a component or a resource (if at the root of the hierarchy).
#[derive(SystemParam)]
pub struct ReferenceFrames<'w, 's, P: GridPrecision> {
parent: Query<'w, 's, Read<Parent>>,
frame_root: Res<'w, RootReferenceFrame<P>>,
frame_query: Query<'w, 's, (Entity, Read<ReferenceFrame<P>>)>,
}
impl<'w, 's, P: GridPrecision> ReferenceFrames<'w, 's, P> {
/// Get the reference frame from a handle.
pub fn resolve_handle(&self, handle: ReferenceFrameHandle) -> &ReferenceFrame<P> {
match handle {
ReferenceFrameHandle::Node(frame_entity) => self
.frame_query
.get(frame_entity)
.map(|(_entity, frame)| frame)
.unwrap_or_else(|e| {
panic!("{} {handle:?} failed to resolve.\n\tEnsure all GridPrecision components are using the <{}> generic, and all required components are present.\n\tQuery Error: {e}", std::any::type_name::<ReferenceFrameHandle>(), std::any::type_name::<P>())
}),
ReferenceFrameHandle::Root => {
&self.frame_root
}
}
}
/// Get a handle to this entity's reference frame, if it exists.
#[inline]
pub fn get_handle(&self, this: Entity) -> Option<ReferenceFrameHandle> {
match self.parent.get(this).map(|parent| **parent) {
Err(_) => Some(ReferenceFrameHandle::Root),
Ok(parent) => match self.frame_query.contains(parent) {
true => Some(ReferenceFrameHandle::Node(parent)),
false => None,
},
}
}
/// Get a reference to this entity's reference frame, if it exists.
#[inline]
pub fn get(&self, this: Entity) -> Option<&ReferenceFrame<P>> {
self.get_handle(this)
.map(|handle| self.resolve_handle(handle))
}
}
/// Used to access a reference frame. Needed because the reference frame could either be a
/// component, or a resource if at the root of the hierarchy.
#[derive(SystemParam)]
pub struct ReferenceFramesMut<'w, 's, P: GridPrecision> {
parent: Query<'w, 's, Read<Parent>>,
children: Query<'w, 's, Read<Children>>,
frame_root: ResMut<'w, RootReferenceFrame<P>>,
frame_query: Query<
'w,
's,
(
Entity,
Read<GridCell<P>>,
Read<Transform>,
Write<ReferenceFrame<P>>,
Option<Read<Parent>>,
),
>,
}
impl<'w, 's, P: GridPrecision> ReferenceFramesMut<'w, 's, P> {
/// Get mutable access to the [`ReferenceFrame`], and run the provided function or closure,
/// optionally returning data.
///
/// ## Panics
///
/// This will panic if the handle passed in is invalid.
///
/// ## Why a closure?
///
/// This expects a closure because the reference frame could be stored as a component or a
/// resource, making it difficult (impossible?) to return a mutable reference to the reference
/// frame when the types involved are different. The main issue seems to be that the component
/// is returned as a `Mut<T>`; getting a mutable reference to the internal value requires that
/// this function return a reference to a value owned by the function.
///
/// I tried returning an enum or a boxed trait object, but ran into issues expressing the
/// lifetimes. Worth revisiting if this turns out to be annoying, but seems pretty insignificant
/// at the time of writing.
#[inline]
pub fn update<T>(
&mut self,
handle: ReferenceFrameHandle,
mut func: impl FnMut(&mut ReferenceFrame<P>, &GridCell<P>, &Transform) -> T,
) -> T {
match handle {
ReferenceFrameHandle::Node(frame_entity) => self
.frame_query
.get_mut(frame_entity)
.map(|(_entity, cell, transform, mut frame, _parent)| {
func(frame.as_mut(), cell, transform)
})
.expect("The supplied reference frame handle to node is no longer valid."),
ReferenceFrameHandle::Root => func(
&mut self.frame_root,
&GridCell::default(), // the reference frame itself is not within another.
&Transform::default(), // the reference frame itself is not within another.
),
}
}
/// Get the reference frame and the position of the reference frame from a handle.
pub fn get(
&self,
handle: ReferenceFrameHandle,
) -> (&ReferenceFrame<P>, GridCell<P>, Transform) {
match handle {
ReferenceFrameHandle::Node(frame_entity) => self
.frame_query
.get(frame_entity)
.map(|(_entity, cell, transform, frame, _parent)| (frame, *cell, *transform))
.unwrap_or_else(|e| {
panic!("{} {handle:?} failed to resolve.\n\tEnsure all GridPrecision components are using the <{}> generic, and all required components are present.\n\tQuery Error: {e}", std::any::type_name::<ReferenceFrameHandle>(), std::any::type_name::<P>())
}),
ReferenceFrameHandle::Root => {
(&self.frame_root, GridCell::default(), Transform::default())
}
}
}
/// Get a handle to this entity's reference frame, if it exists.
#[inline]
fn get_handle(&self, this: Entity) -> Option<ReferenceFrameHandle> {
match self.parent.get(this).map(|parent| **parent) {
Err(_) => Some(ReferenceFrameHandle::Root),
Ok(parent) => match self.frame_query.contains(parent) {
true => Some(ReferenceFrameHandle::Node(parent)),
false => None,
},
}
}
/// Get a handle to the parent reference frame of this reference frame, if it exists.
#[inline]
fn parent(&mut self, this: ReferenceFrameHandle) -> Option<ReferenceFrameHandle> {
match this {
ReferenceFrameHandle::Node(this) => self.get_handle(this),
ReferenceFrameHandle::Root => None,
}
}
/// Get handles to all reference frames that are children of this reference frame. Applies a
/// filter to the returned children.
#[inline]
fn children_filtered(
&mut self,
this: ReferenceFrameHandle,
mut filter: impl FnMut(Entity) -> bool,
) -> Vec<ReferenceFrameHandle> {
match this {
ReferenceFrameHandle::Node(this) => self
.children
.get(this)
.iter()
.flat_map(|c| c.iter())
.filter(|entity| filter(**entity))
.filter(|child| self.frame_query.contains(**child))
.map(|child| ReferenceFrameHandle::Node(*child))
.collect(),
ReferenceFrameHandle::Root => self
.frame_query
.iter()
.filter(|(entity, ..)| filter(*entity))
.filter(|(.., parent)| parent.is_none())
.map(|(entity, ..)| ReferenceFrameHandle::Node(entity))
.collect(),
}
}
/// Get handles to all reference frames that are children of this reference frame.
#[inline]
fn children(&mut self, this: ReferenceFrameHandle) -> Vec<ReferenceFrameHandle> {
self.children_filtered(this, |_| true)
}
/// Get handles to all reference frames that are siblings of this reference frame.
#[inline]
fn siblings(&mut self, this: ReferenceFrameHandle) -> Vec<ReferenceFrameHandle> {
match this {
ReferenceFrameHandle::Node(this_entity) => {
if let Some(parent) = self.parent(this) {
self.children_filtered(parent, |e| e != this_entity)
} else {
Vec::new()
}
}
ReferenceFrameHandle::Root => Vec::new(),
}
}
}
impl<P: GridPrecision> LocalFloatingOrigin<P> {
/// Update the [`LocalFloatingOrigin`] of every [`ReferenceFrame`] in the world. This does not
/// update any entity transforms, instead this is a preceding step that updates every reference
/// frame, so it knows where the floating origin is located with respect to that reference
/// frame. This is all done in high precision if possible, however any loss in precision will
/// only affect the rendering precision. The high precision coordinates ([`GridCell`] and
/// [`Transform`]) are the source of truth and never mutated.
pub fn update(
origin: Query<(Entity, &GridCell<P>), With<FloatingOrigin>>,
mut reference_frames: ReferenceFramesMut<P>,
mut frame_stack: Local<Vec<ReferenceFrameHandle>>,
) {
/// The maximum reference frame tree depth, defensively prevents infinite looping in case
/// there is a degenerate hierarchy. It might take a while, but at least it's not forever?
const MAX_REFERENCE_FRAME_DEPTH: usize = usize::MAX;
let (origin_entity, origin_cell) = origin
.get_single()
.expect("There can only be one entity with the `FloatingOrigin` component.");
let Some(mut this_frame) = reference_frames.get_handle(origin_entity) else {
error!("The floating origin is not in a valid reference frame. The floating origin entity must be a child of an entity with the `ReferenceFrame`, `GridCell`, and `Transform` components, or be at the root of the parent-child hierarchy.");
return;
};
// Prepare by resetting the `origin_transform` of the floating origin's reference frame.
// Because the floating origin is within this reference frame, there is no grid misalignment
// and thus no need for any floating offsets.
reference_frames.update(this_frame, |frame, _, _| {
frame
.local_floating_origin
.set(*origin_cell, Vec3::ZERO, DQuat::IDENTITY);
});
// Seed the frame stack with the floating origin's reference frame. From this point out, we
// will only look at siblings and parents, which will allow us to visit the entire tree.
frame_stack.clear();
frame_stack.push(this_frame);
// Recurse up and across the tree, updating siblings and their children.
for _ in 0..MAX_REFERENCE_FRAME_DEPTH {
// We start by propagating up to the parent of this frame, then propagating down to the
// siblings of this frame (children of the parent that are not this frame).
if let Some(parent_frame) = reference_frames.parent(this_frame) {
this_frame.propagate_origin_to_parent(&mut reference_frames, parent_frame);
for sibling_frame in reference_frames.siblings(this_frame) {
// The siblings of this frame are also the children of the parent frame.
parent_frame.propagate_origin_to_child(&mut reference_frames, sibling_frame);
frame_stack.push(sibling_frame); // We'll recurse through children next
}
}
// All of the reference frames pushed on the stack have been processed. We can now pop
// those off the stack and recursively process their children all the way out to the
// leaves of the tree.
while let Some(this_frame) = frame_stack.pop() {
for child_frame in reference_frames.children(this_frame) {
this_frame.propagate_origin_to_child(&mut reference_frames, child_frame);
frame_stack.push(child_frame) // Push processed child onto the stack
}
}
// Finally, now that the siblings of this frame have been recursively processed, we
// process the parent and set it as the current reference frame. Note that every time we
// step to a parent, "this frame" and all descendants have already been processed, so we
// only need to process the siblings.
match reference_frames.parent(this_frame) {
Some(parent_frame) => this_frame = parent_frame,
None => return, // We have reached the root of the tree, and can exit.
}
}
error!("Reached the maximum reference frame depth ({MAX_REFERENCE_FRAME_DEPTH}), and exited early to prevent an infinite loop. This might be caused by a degenerate hierarchy.")
}
}
#[cfg(test)]
mod tests {
use bevy::{ecs::system::SystemState, math::DVec3};
use super::*;
use crate::*;
/// Test that the reference frame getters do what they say they do.
#[test]
fn frame_hierarchy_getters() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i32>::default());
let frame_bundle = (
Transform::default(),
GridCell::<i32>::default(),
ReferenceFrame::<i32>::default(),
);
let child_1 = app.world.spawn(frame_bundle.clone()).id();
let child_2 = app.world.spawn(frame_bundle.clone()).id();
let parent = app.world.spawn(frame_bundle.clone()).id();
app.world
.entity_mut(parent)
.push_children(&[child_1, child_2]);
let mut state = SystemState::<ReferenceFramesMut<i32>>::new(&mut app.world);
let mut ref_frame = state.get_mut(&mut app.world);
// Children
let result = ref_frame.children(ReferenceFrameHandle::Root);
assert_eq!(result, vec![ReferenceFrameHandle::Node(parent)]);
let result = ref_frame.children(ReferenceFrameHandle::Node(parent));
assert!(result.contains(&ReferenceFrameHandle::Node(child_1)));
assert!(result.contains(&ReferenceFrameHandle::Node(child_2)));
let result = ref_frame.children(ReferenceFrameHandle::Node(child_1));
assert_eq!(result, Vec::new());
// Parent
let result = ref_frame.parent(ReferenceFrameHandle::Root);
assert_eq!(result, None);
let result = ref_frame.parent(ReferenceFrameHandle::Node(parent));
assert_eq!(result, Some(ReferenceFrameHandle::Root));
let result = ref_frame.parent(ReferenceFrameHandle::Node(child_1));
assert_eq!(result, Some(ReferenceFrameHandle::Node(parent)));
// Siblings
let result = ref_frame.siblings(ReferenceFrameHandle::Root);
assert_eq!(result, vec![]);
let result = ref_frame.siblings(ReferenceFrameHandle::Node(parent));
assert_eq!(result, vec![]);
let result = ref_frame.siblings(ReferenceFrameHandle::Node(child_1));
assert_eq!(result, vec![ReferenceFrameHandle::Node(child_2)]);
}
#[test]
fn child_propagation() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i32>::default());
let root = ReferenceFrameHandle::Root;
app.insert_resource(RootReferenceFrame(ReferenceFrame {
local_floating_origin: LocalFloatingOrigin::new(
GridCell::<i32>::new(1_000_000, -1, -1),
Vec3::ZERO,
DQuat::from_rotation_z(-std::f64::consts::FRAC_PI_2),
),
..default()
}));
let child = app
.world
.spawn((
Transform::from_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2))
.with_translation(Vec3::new(1.0, 1.0, 0.0)),
GridCell::<i32>::new(1_000_000, 0, 0),
ReferenceFrame::<i32>::default(),
))
.id();
let child = ReferenceFrameHandle::Node(child);
let mut state = SystemState::<ReferenceFramesMut<i32>>::new(&mut app.world);
let mut reference_frames = state.get_mut(&mut app.world);
// The function we are testing
root.propagate_origin_to_child(&mut reference_frames, child);
let (child_frame, ..) = reference_frames.get(child);
let computed_grid = child_frame.local_floating_origin.cell();
let correct_grid = GridCell::new(-1, 0, -1);
assert_eq!(computed_grid, correct_grid);
let computed_rot = child_frame.local_floating_origin.rotation();
let correct_rot = DQuat::from_rotation_z(std::f64::consts::PI);
let rot_error = computed_rot.angle_between(correct_rot);
assert!(rot_error < 1e-10);
// Even though we are 2 billion units from the origin, our precision is still pretty good.
// The loss of precision is coming from the affine multiplication that moves the origin into
// the child's reference frame. The good news is that precision loss only scales with the
// distance of the origin to the child (in the child's reference frame). In this test we are
// saying that the floating origin is - with respect to the root - pretty near the child.
// Even though the child and floating origin are very far from the origin, we only lose
// precision based on how for the origin is from the child.
let computed_trans = child_frame.local_floating_origin.translation();
let correct_trans = Vec3::new(-1.0, 1.0, 0.0);
let trans_error = computed_trans.distance(correct_trans);
assert!(trans_error < 1e-4);
}
#[test]
fn parent_propagation() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i64>::default());
let root = ReferenceFrameHandle::Root;
let child = app
.world
.spawn((
Transform::from_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2))
.with_translation(Vec3::new(1.0, 1.0, 0.0)),
GridCell::<i64>::new(150_000_003_000, 0, 0), // roughly radius of earth orbit
ReferenceFrame {
local_floating_origin: LocalFloatingOrigin::new(
GridCell::<i64>::new(0, 3_000, 0),
Vec3::new(5.0, 5.0, 0.0),
DQuat::from_rotation_z(-std::f64::consts::FRAC_PI_2),
),
..default()
},
))
.id();
let child = ReferenceFrameHandle::Node(child);
let mut state = SystemState::<ReferenceFramesMut<i64>>::new(&mut app.world);
let mut reference_frames = state.get_mut(&mut app.world);
// The function we are testing
child.propagate_origin_to_parent(&mut reference_frames, root);
let (root_frame, ..) = reference_frames.get(root);
let computed_grid = root_frame.local_floating_origin.cell();
let correct_grid = GridCell::new(150_000_000_000, 0, 0);
assert_eq!(computed_grid, correct_grid);
let computed_rot = root_frame.local_floating_origin.rotation();
let correct_rot = DQuat::IDENTITY;
let rot_error = computed_rot.angle_between(correct_rot);
assert!(rot_error < 1e-7);
// This is the error of the position of the floating origin if the origin was a person
// standing on earth, and their position was resampled with respect to the sun. This is 0.3
// meters, but recall that this will be the error when positioning the other planets in the
// solar system when rendering.
//
// This error scales with the distance of the floating origin from the origin of its
// reference frame, in this case the radius of the earth, not the radius of the orbit.
let computed_trans = root_frame.local_floating_origin.translation();
let correct_trans = Vec3::new(-4.0, 6.0, 0.0);
let trans_error = computed_trans.distance(correct_trans);
assert!(trans_error < 0.3);
}
#[test]
fn origin_transform() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i32>::default());
let root = ReferenceFrameHandle::Root;
app.insert_resource(RootReferenceFrame(ReferenceFrame {
local_floating_origin: LocalFloatingOrigin::new(
GridCell::<i32>::new(0, 0, 0),
Vec3::new(1.0, 1.0, 0.0),
DQuat::from_rotation_z(0.0),
),
..default()
}));
let child = app
.world
.spawn((
Transform::default()
.with_rotation(Quat::from_rotation_z(-std::f32::consts::FRAC_PI_2))
.with_translation(Vec3::new(3.0, 3.0, 0.0)),
GridCell::<i32>::new(0, 0, 0),
ReferenceFrame::<i32>::default(),
))
.id();
let child = ReferenceFrameHandle::Node(child);
let mut state = SystemState::<ReferenceFramesMut<i32>>::new(&mut app.world);
let mut reference_frames = state.get_mut(&mut app.world);
root.propagate_origin_to_child(&mut reference_frames, child);
let (child_frame, ..) = reference_frames.get(child);
let child_local_point = DVec3::new(5.0, 5.0, 0.0);
let computed_transform = child_frame
.local_floating_origin
.reference_frame_transform();
let computed_pos = computed_transform.transform_point3(child_local_point);
let correct_transform = DAffine3::from_rotation_translation(
DQuat::from_rotation_z(-std::f64::consts::FRAC_PI_2),
DVec3::new(2.0, 2.0, 0.0),
);
let correct_pos = correct_transform.transform_point3(child_local_point);
assert!((computed_pos - correct_pos).length() < 1e-6);
assert!((computed_pos - DVec3::new(7.0, -3.0, 0.0)).length() < 1e-6);
}
}

173
src/reference_frame/mod.rs Normal file
View File

@ -0,0 +1,173 @@
//! Adds the concept of hierarchical, nesting [`ReferenceFrame`]s, to group entities that move
//! through space together, like entities on a planet, rotating about the planet's axis, and,
//! orbiting a star.
use bevy::{
ecs::{component::Component, system::Resource},
math::{Affine3A, DAffine3, DVec3, Vec3},
prelude::{Deref, DerefMut},
reflect::Reflect,
transform::components::{GlobalTransform, Transform},
};
use crate::{GridCell, GridPrecision};
use self::local_origin::LocalFloatingOrigin;
pub mod local_origin;
/// All entities that have no parent are implicitly in the root [`ReferenceFrame`].
///
/// Because this relationship is implicit, it lives outside of the entity/component hierarchy, and
/// is a singleton; this is why the root reference frame is a resource unlike all other
/// [`ReferenceFrame`]s which are components.
#[derive(Debug, Clone, Resource, Reflect, Deref, DerefMut)]
pub struct RootReferenceFrame<P: GridPrecision>(pub(crate) ReferenceFrame<P>);
/// A component that defines a reference frame for children of this entity with [`GridCell`]s.
///
/// Entities without a parent are implicitly in the [`RootReferenceFrame`].
///
/// ## Motivation
///
/// Reference frames are hierarchical, allowing more precision for objects with similar relative
/// velocities. Entities in the same reference frame as the [`crate::FloatingOrigin`] will be
/// rendered with the most precision. Reference frames are transformed relative to each other
/// using 64 bit float transforms.
///
/// ## Example
///
/// You can use reference frames to ensure all entities on a planet, and the planet itself, are in
/// the same rotating reference frame, instead of moving rapidly through space around a star, or
/// worse, around the center of the galaxy.
#[derive(Debug, Clone, Reflect, Component)]
pub struct ReferenceFrame<P: GridPrecision> {
/// The high-precision position of the floating origin's current grid cell local to this
/// reference frame.
local_floating_origin: LocalFloatingOrigin<P>,
/// Defines the uniform scale of the grid by the length of the edge of a grid cell.
cell_edge_length: f32,
/// How far an entity can move from the origin before its grid cell is recomputed.
maximum_distance_from_origin: f32,
}
impl<P: GridPrecision> Default for ReferenceFrame<P> {
fn default() -> Self {
Self::new(2_000f32, 100f32)
}
}
impl<P: GridPrecision> ReferenceFrame<P> {
/// Construct a new [`ReferenceFrame`]. The properties of a reference frame cannot be changed
/// after construction.
pub fn new(cell_edge_length: f32, switching_threshold: f32) -> Self {
Self {
local_floating_origin: LocalFloatingOrigin::default(),
cell_edge_length,
maximum_distance_from_origin: cell_edge_length / 2.0 + switching_threshold,
}
}
/// Get the position of the floating origin relative to the current reference frame.
pub fn local_floating_origin(&self) -> &LocalFloatingOrigin<P> {
&self.local_floating_origin
}
/// Get the size of each cell this reference frame's grid.
pub fn cell_edge_length(&self) -> f32 {
self.cell_edge_length
}
/// Get the reference frame's [`Self::maximum_distance_from_origin`].
pub fn maximum_distance_from_origin(&self) -> f32 {
self.maximum_distance_from_origin
}
/// Compute the double precision position of an entity's [`Transform`] with respect to the given
/// [`GridCell`] within this reference frame.
pub fn grid_position_double(&self, pos: &GridCell<P>, transform: &Transform) -> DVec3 {
DVec3 {
x: pos.x.as_f64() * self.cell_edge_length as f64 + transform.translation.x as f64,
y: pos.y.as_f64() * self.cell_edge_length as f64 + transform.translation.y as f64,
z: pos.z.as_f64() * self.cell_edge_length as f64 + transform.translation.z as f64,
}
}
/// Compute the single precision position of an entity's [`Transform`] with respect to the given
/// [`GridCell`].
pub fn grid_position(&self, pos: &GridCell<P>, transform: &Transform) -> Vec3 {
Vec3 {
x: pos.x.as_f64() as f32 * self.cell_edge_length + transform.translation.x,
y: pos.y.as_f64() as f32 * self.cell_edge_length + transform.translation.y,
z: pos.z.as_f64() as f32 * self.cell_edge_length + transform.translation.z,
}
}
/// Returns the floating point position of a [`GridCell`].
pub fn grid_to_float(&self, pos: &GridCell<P>) -> DVec3 {
DVec3 {
x: pos.x.as_f64() * self.cell_edge_length as f64,
y: pos.y.as_f64() * self.cell_edge_length as f64,
z: pos.z.as_f64() * self.cell_edge_length as f64,
}
}
/// Convert a large translation into a small translation relative to a grid cell.
pub fn translation_to_grid(&self, input: impl Into<DVec3>) -> (GridCell<P>, Vec3) {
let l = self.cell_edge_length as f64;
let input = input.into();
let DVec3 { x, y, z } = 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();
let y_r = (y / l).round();
let z_r = (z / l).round();
let t_x = x - x_r * l;
let t_y = y - y_r * l;
let t_z = z - z_r * l;
(
GridCell {
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 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(&self, input: Vec3) -> (GridCell<P>, Vec3) {
self.translation_to_grid(input.as_dvec3())
}
/// Compute the [`GlobalTransform`] of an entity in this reference frame.
pub fn global_transform(
&self,
local_cell: &GridCell<P>,
local_transform: &Transform,
) -> GlobalTransform {
// The reference frame transform from the floating origin's reference frame, to the local
// reference frame.
let transform_origin = self.local_floating_origin().reference_frame_transform();
// The grid cell offset of this entity relative to the floating origin's cell in this local
// reference frame.
let cell_origin_relative = *local_cell - self.local_floating_origin().cell();
let grid_offset = self.grid_to_float(&cell_origin_relative);
let local_transform = DAffine3::from_scale_rotation_translation(
local_transform.scale.as_dvec3(),
local_transform.rotation.as_dquat(),
local_transform.translation.as_dvec3() + grid_offset,
);
let global_64 = transform_origin * local_transform;
Affine3A {
matrix3: global_64.matrix3.as_mat3().into(),
translation: global_64.translation.as_vec3a(),
}
.into()
}
}

View File

@ -5,8 +5,8 @@ use bevy::ecs::query::QueryData;
use bevy::math::DVec3;
use bevy::prelude::*;
use crate::precision::GridPrecision;
use crate::{FloatingOriginSettings, GridCell};
use crate::GridCell;
use crate::{precision::GridPrecision, reference_frame::ReferenceFrame};
#[derive(QueryData)]
#[query_data(mutable)]
@ -23,13 +23,13 @@ pub struct GridTransform<P: GridPrecision> {
impl<'w, P: GridPrecision> GridTransformItem<'w, P> {
/// Compute the global position with double precision.
pub fn position_double(&self, settings: &FloatingOriginSettings) -> DVec3 {
settings.grid_position_double(&self.cell, &self.transform)
pub fn position_double(&self, reference_frame: &ReferenceFrame<P>) -> DVec3 {
reference_frame.grid_position_double(&self.cell, &self.transform)
}
/// Compute the global position.
pub fn position(&self, settings: &FloatingOriginSettings) -> Vec3 {
settings.grid_position(&self.cell, &self.transform)
pub fn position(&self, reference_frame: &ReferenceFrame<P>) -> Vec3 {
reference_frame.grid_position(&self.cell, &self.transform)
}
/// Get a copy of the fields to work with.
@ -43,13 +43,13 @@ impl<'w, P: GridPrecision> GridTransformItem<'w, P> {
impl<'w, P: GridPrecision> GridTransformReadOnlyItem<'w, P> {
/// Compute the global position with double precision.
pub fn position_double(&self, settings: &FloatingOriginSettings) -> DVec3 {
settings.grid_position_double(self.cell, self.transform)
pub fn position_double(&self, reference_frame: &ReferenceFrame<P>) -> DVec3 {
reference_frame.grid_position_double(self.cell, self.transform)
}
/// Compute the global position.
pub fn position(&self, settings: &FloatingOriginSettings) -> Vec3 {
settings.grid_position(self.cell, self.transform)
pub fn position(&self, reference_frame: &ReferenceFrame<P>) -> Vec3 {
reference_frame.grid_position(self.cell, self.transform)
}
/// Get a copy of the fields to work with.
@ -98,12 +98,12 @@ impl<P: GridPrecision> std::ops::Add for GridTransformOwned<P> {
impl<P: GridPrecision> GridTransformOwned<P> {
/// Compute the global position with double precision.
pub fn position_double(&self, space: &FloatingOriginSettings) -> DVec3 {
space.grid_position_double(&self.cell, &self.transform)
pub fn position_double(&self, reference_frame: &ReferenceFrame<P>) -> DVec3 {
reference_frame.grid_position_double(&self.cell, &self.transform)
}
/// Compute the global position.
pub fn position(&self, space: &FloatingOriginSettings) -> Vec3 {
space.grid_position(&self.cell, &self.transform)
pub fn position(&self, reference_frame: &ReferenceFrame<P>) -> Vec3 {
reference_frame.grid_position(&self.cell, &self.transform)
}
}