mirror of
https://github.com/eliasstepanik/big_space_with_trim.git
synced 2026-01-11 09:18:27 +00:00
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:
parent
0dafa2b83c
commit
2e69f80b8d
13
Cargo.toml
13
Cargo.toml
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
100
examples/error_child.rs
Normal 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
193
examples/planets.rs
Normal 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,
|
||||
));
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
51
src/debug.rs
51
src/debug.rs
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
301
src/lib.rs
301
src/lib.rs
@ -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)]
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
735
src/reference_frame/local_origin.rs
Normal file
735
src/reference_frame/local_origin.rs
Normal 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
173
src/reference_frame/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user