Explicit BigSpaces (#22)

Changes the design of the plugin to work with multiple, independent
high-precision hierarchies at the root with the `BigSpace` component at
the root of each of these hierarchies.

Closes #17
Closes #19 
Closes #21
This commit is contained in:
Aevyrie 2024-06-17 01:41:03 -07:00 committed by GitHub
parent 14db5acb64
commit 8721911b49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 2949 additions and 1334 deletions

View File

@ -15,13 +15,17 @@ bevy = { version = "0.13", default_features = false }
[dev-dependencies]
bevy = { version = "0.13", default-features = false, features = [
"bevy_scene",
"bevy_gltf",
"bevy_winit",
"default_font",
"bevy_ui",
"bevy_pbr",
"x11",
"tonemapping_luts",
"multi-threaded",
] }
bevy-inspector-egui = "0.24"
bevy_framepace = { version = "0.15", default-features = false }
rand = "0.8.5"
@ -60,3 +64,9 @@ name = "planets"
path = "examples/planets.rs"
required-features = ["default"]
doc-scrape-examples = true
[[example]]
name = "split_screen"
path = "examples/split_screen.rs"
required-features = ["default"]
doc-scrape-examples = true

View File

@ -19,9 +19,9 @@ https://user-images.githubusercontent.com/2632925/215318129-5bab3095-a7dd-455b-a
Lots of space to play in.
This is a simple floating origin plugin, useful if you want to work with very, very large scales. It works with bevy's existing `f32`-based `Transform`s, which means it's largely compatible with the bevy ecosystem. The plugin positions entities within large fixed precision grids, effectively adding precision to the location of objects.
This is a floating origin plugin, useful if you want to work with very large or very small scales. It works with bevy's existing `f32`-based `Transform`s, which means it's largely compatible with the bevy ecosystem. The plugin positions entities within large fixed precision grids, effectively adding precision to the location of objects.
Additionally, you can use reference frames to nest high precision coordinate systems. For example you might want to put all entities on a planet into the same reference frame. You can then rotate this reference frame with the planet, and orbit that planet around a star.
Additionally, you can use reference frames to nest high precision coordinate systems. For example you might want to put all entities on a planet's surface into the same reference frame. You can then rotate this reference frame with the planet, and orbit that planet around a star.
The plugin is generic over a few integer types, to trade off scale and precision for memory use. Some fun numbers with a worst case precision of 0.5mm:
- `i8`: 2,560 km = 74% of the diameter of the Moon
@ -30,12 +30,12 @@ The plugin is generic over a few integer types, to trade off scale and precision
- `i64`: 19.5 million light years = ~100 times the width of the milky way galaxy
- `i128`: 3.6e+26 light years = ~3.9e+15 times the width of the observable universe
From the docs: https://docs.rs/big_space/latest/big_space/struct.GridCell.html
This can also be used for small scales. With a cell edge length of `1e-11`, and using `i128`, there is enough precision to render objects the size of quarks anywhere in the observable universe.
From the docs: https://docs.rs/big_space/latest/big_space/precision/trait.GridPrecision.html
# Bevy Version Support
I intend to track the `main` branch of Bevy. PRs supporting this are welcome!
| bevy | big_space |
| ---- | --------- |
| 0.13 | 0.5, 0.6 |

View File

@ -0,0 +1,11 @@
Model Information:
* title: Low Poly Spaceship
* source: https://sketchfab.com/3d-models/low-poly-spaceship-f854128cf78d4dafb28d16b3c15001ba
* author: FriendlyCreep (https://sketchfab.com/FriendlyCreep)
Model License:
* license type: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
* requirements: Author must be credited. Commercial use is allowed.
If you use this 3D model in your project be sure to copy paste this credit wherever you share it:
This work is based on "Low Poly Spaceship" (https://sketchfab.com/3d-models/low-poly-spaceship-f854128cf78d4dafb28d16b3c15001ba) by FriendlyCreep (https://sketchfab.com/FriendlyCreep) licensed under CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)

Binary file not shown.

View File

@ -0,0 +1,470 @@
{
"accessors": [
{
"bufferView": 2,
"componentType": 5126,
"count": 712,
"max": [
4.986988067626953,
0.5560435652732849,
9.616311073303223
],
"min": [
-4.986988067626953,
-1.969908356666565,
-4.9558634757995605
],
"type": "VEC3"
},
{
"bufferView": 2,
"byteOffset": 8544,
"componentType": 5126,
"count": 712,
"max": [
1.0,
0.9917697906494141,
0.9999937415122986
],
"min": [
-1.0,
-0.9730851054191589,
-0.9999937415122986
],
"type": "VEC3"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 712,
"max": [
0.7559658885002136,
0.75
],
"min": [
0.1249999925494194,
0.24999994039535522
],
"type": "VEC2"
},
{
"bufferView": 0,
"componentType": 5125,
"count": 1164,
"type": "SCALAR"
},
{
"bufferView": 2,
"byteOffset": 17088,
"componentType": 5126,
"count": 1768,
"max": [
5.272348403930664,
0.6022237539291382,
9.626778602600098
],
"min": [
-5.272348403930664,
-2.1259989738464355,
-4.9558634757995605
],
"type": "VEC3"
},
{
"bufferView": 2,
"byteOffset": 38304,
"componentType": 5126,
"count": 1768,
"max": [
1.0,
1.0,
1.0
],
"min": [
-1.0,
-1.0,
-1.0
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 5696,
"componentType": 5126,
"count": 1768,
"max": [
0.875,
1.0
],
"min": [
0.0,
0.0
],
"type": "VEC2"
},
{
"bufferView": 0,
"byteOffset": 4656,
"componentType": 5125,
"count": 5070,
"type": "SCALAR"
},
{
"bufferView": 2,
"byteOffset": 59520,
"componentType": 5126,
"count": 866,
"max": [
3.563509702682495,
0.6022237539291382,
9.626778602600098
],
"min": [
-3.563509702682495,
-1.5118601322174072,
-4.9558634757995605
],
"type": "VEC3"
},
{
"bufferView": 2,
"byteOffset": 69912,
"componentType": 5126,
"count": 866,
"max": [
1.0,
1.0,
1.0
],
"min": [
-1.0,
-1.0,
-1.0
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 19840,
"componentType": 5126,
"count": 866,
"max": [
0.875,
0.75
],
"min": [
0.0,
0.0
],
"type": "VEC2"
},
{
"bufferView": 0,
"byteOffset": 24936,
"componentType": 5125,
"count": 1476,
"type": "SCALAR"
},
{
"bufferView": 2,
"byteOffset": 80304,
"componentType": 5126,
"count": 296,
"max": [
4.863146781921387,
0.5145567655563354,
9.446455955505371
],
"min": [
-4.863146781921387,
-1.8699951171875,
-4.738320827484131
],
"type": "VEC3"
},
{
"bufferView": 2,
"byteOffset": 83856,
"componentType": 5126,
"count": 296,
"max": [
1.0,
1.0,
0.9979816675186157
],
"min": [
-0.8001577854156494,
-1.0,
-1.0
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 26768,
"componentType": 5126,
"count": 296,
"max": [
0.7559658885002136,
0.735460638999939
],
"min": [
0.0,
0.0
],
"type": "VEC2"
},
{
"bufferView": 0,
"byteOffset": 30840,
"componentType": 5125,
"count": 600,
"type": "SCALAR"
}
],
"asset": {
"extras": {
"author": "FriendlyCreep (https://sketchfab.com/FriendlyCreep)",
"license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)",
"source": "https://sketchfab.com/3d-models/low-poly-spaceship-f854128cf78d4dafb28d16b3c15001ba",
"title": "Low Poly Spaceship"
},
"generator": "Sketchfab-12.68.0",
"version": "2.0"
},
"bufferViews": [
{
"buffer": 0,
"byteLength": 33240,
"name": "floatBufferViews",
"target": 34963
},
{
"buffer": 0,
"byteLength": 29136,
"byteOffset": 33240,
"byteStride": 8,
"name": "floatBufferViews",
"target": 34962
},
{
"buffer": 0,
"byteLength": 87408,
"byteOffset": 62376,
"byteStride": 12,
"name": "floatBufferViews",
"target": 34962
}
],
"buffers": [
{
"byteLength": 149784,
"uri": "scene.bin"
}
],
"materials": [
{
"doubleSided": true,
"name": "Material.001",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.0,
0.0,
0.0,
1.0
],
"metallicFactor": 0.0,
"roughnessFactor": 0.5
}
},
{
"doubleSided": true,
"name": "Material.002",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.226583,
0.226583,
0.226583,
1.0
],
"metallicFactor": 0.0,
"roughnessFactor": 0.5
}
},
{
"doubleSided": true,
"name": "Material.003",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.8,
0.0113789,
0.0,
1.0
],
"metallicFactor": 0.0,
"roughnessFactor": 0.5
}
},
{
"doubleSided": true,
"emissiveFactor": [
0.0,
0.161466,
1.0
],
"name": "Material.004"
}
],
"meshes": [
{
"name": "Object_0",
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 0,
"TEXCOORD_0": 2
},
"indices": 3,
"material": 0,
"mode": 4
}
]
},
{
"name": "Object_1",
"primitives": [
{
"attributes": {
"NORMAL": 5,
"POSITION": 4,
"TEXCOORD_0": 6
},
"indices": 7,
"material": 1,
"mode": 4
}
]
},
{
"name": "Object_2",
"primitives": [
{
"attributes": {
"NORMAL": 9,
"POSITION": 8,
"TEXCOORD_0": 10
},
"indices": 11,
"material": 2,
"mode": 4
}
]
},
{
"name": "Object_3",
"primitives": [
{
"attributes": {
"NORMAL": 13,
"POSITION": 12,
"TEXCOORD_0": 14
},
"indices": 15,
"material": 3,
"mode": 4
}
]
}
],
"nodes": [
{
"children": [
1
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
2.220446049250313e-16,
-1.0,
0.0,
0.0,
1.0,
2.220446049250313e-16,
0.0,
0.0,
0.0,
0.0,
1.0
],
"name": "Sketchfab_model"
},
{
"children": [
2
],
"name": "root"
},
{
"children": [
3
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
2.220446049250313e-16,
1.0,
0.0,
0.0,
-1.0,
2.220446049250313e-16,
0.0,
0.0,
0.0,
0.0,
1.0
],
"name": "GLTF_SceneRootNode"
},
{
"children": [
4,
5,
6,
7
],
"name": "Cube_0"
},
{
"mesh": 0,
"name": "Object_4"
},
{
"mesh": 1,
"name": "Object_5"
},
{
"mesh": 2,
"name": "Object_6"
},
{
"mesh": 3,
"name": "Object_7"
}
],
"scene": 0,
"scenes": [
{
"name": "Sketchfab_Scene",
"nodes": [
0
]
}
]
}

View File

@ -1,13 +1,13 @@
#![allow(clippy::type_complexity)]
use bevy::prelude::*;
use big_space::{reference_frame::ReferenceFrame, FloatingOrigin, GridCell};
use big_space::{commands::BigSpaceCommands, reference_frame::ReferenceFrame, FloatingOrigin};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
big_space::FloatingOriginPlugin::<i64>::new(0.5, 0.01),
big_space::BigSpacePlugin::<i64>::default(),
big_space::debug::FloatingOriginDebugPlugin::<i64>::default(),
))
.insert_resource(ClearColor(Color::BLACK))
@ -65,69 +65,63 @@ fn setup(
..default()
});
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(0.0, 0.0, 1.0),
..default()
},
GridCell::<i64>::default(),
Mover::<1>,
));
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(1.0, 0.0, 0.0),
..default()
},
GridCell::<i64>::default(),
Mover::<2>,
));
commands
.spawn((
commands.spawn_big_space(ReferenceFrame::<i64>::new(1.0, 0.01), |root| {
root.spawn_spatial((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(0.0, 1.0, 0.0),
transform: Transform::from_xyz(0.0, 0.0, 1.0),
..default()
},
GridCell::<i64>::default(),
ReferenceFrame::<i64>::new(0.2, 0.01),
Rotator,
Mover::<3>,
))
.with_children(|parent| {
parent.spawn((
Mover::<1>,
));
root.spawn_spatial((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(1.0, 0.0, 0.0),
..default()
},
Mover::<2>,
));
root.with_frame(ReferenceFrame::new(0.2, 0.01), |new_frame| {
new_frame.insert((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_xyz(0.0, 1.0, 0.0),
..default()
},
Rotator,
Mover::<3>,
));
new_frame.spawn_spatial((
PbrBundle {
mesh: mesh_handle,
material: matl_handle,
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
GridCell::<i64>::default(),
Mover::<4>,
));
});
// light
commands.spawn((
PointLightBundle {
// light
root.spawn_spatial((PointLightBundle {
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
},
GridCell::<i64>::default(),
));
},));
// camera
commands.spawn((
Camera3dBundle {
transform: Transform::from_xyz(0.0, 0.0, 8.0)
.looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
..default()
},
GridCell::<i64>::default(),
FloatingOrigin,
));
// camera
root.spawn_spatial((
Camera3dBundle {
transform: Transform::from_xyz(0.0, 0.0, 8.0)
.looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
..default()
},
FloatingOrigin,
));
});
}

View File

@ -5,27 +5,27 @@ use bevy::{
};
use big_space::{
camera::{CameraController, CameraInput},
propagation::IgnoreFloatingOrigin,
reference_frame::RootReferenceFrame,
commands::BigSpaceCommands,
reference_frame::{local_origin::ReferenceFrames, ReferenceFrame},
world_query::GridTransformReadOnly,
FloatingOrigin, GridCell,
FloatingOrigin,
};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
big_space::FloatingOriginPlugin::<i128>::default(),
big_space::BigSpacePlugin::<i128>::default(),
big_space::debug::FloatingOriginDebugPlugin::<i128>::default(),
big_space::camera::CameraControllerPlugin::<i128>::default(),
bevy_framepace::FramepacePlugin,
))
.insert_resource(ClearColor(Color::BLACK))
.add_systems(Startup, (setup, ui_setup))
.add_systems(PreUpdate, cursor_grab_system)
.add_systems(PreUpdate, (cursor_grab_system, ui_text_system))
.add_systems(
PostUpdate,
(highlight_nearest_sphere, ui_text_system).after(TransformSystem::TransformPropagate),
highlight_nearest_sphere.after(TransformSystem::TransformPropagate),
)
.run()
}
@ -35,57 +35,56 @@ fn setup(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// camera
commands.spawn((
Camera3dBundle {
transform: Transform::from_xyz(0.0, 0.0, 8.0)
.looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
projection: Projection::Perspective(PerspectiveProjection {
near: 1e-18,
commands.spawn_big_space(ReferenceFrame::<i128>::default(), |root| {
root.spawn_spatial((
Camera3dBundle {
transform: Transform::from_xyz(0.0, 0.0, 8.0)
.looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
projection: Projection::Perspective(PerspectiveProjection {
near: 1e-18,
..default()
}),
..default()
}),
},
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 mesh_handle = meshes.add(Sphere::new(0.5).mesh().ico(32).unwrap());
let matl_handle = materials.add(StandardMaterial {
base_color: Color::BLUE,
perceptual_roughness: 0.8,
reflectance: 1.0,
..default()
},
GridCell::<i128>::default(), // All spatial entities need this component
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 mesh_handle = meshes.add(Sphere::new(0.5).mesh().ico(32).unwrap());
let matl_handle = materials.add(StandardMaterial {
base_color: Color::BLUE,
perceptual_roughness: 0.8,
reflectance: 1.0,
..default()
});
let mut translation = Vec3::ZERO;
for i in -16..=27 {
let j = 10_f32.powf(i as f32);
let k = 10_f32.powf((i - 1) as f32);
translation.x += j / 2.0 + k;
translation.y = j / 2.0;
let mut translation = Vec3::ZERO;
for i in -16..=27 {
let j = 10_f32.powf(i as f32);
let k = 10_f32.powf((i - 1) as f32);
translation.x += j / 2.0 + k;
commands.spawn((
PbrBundle {
root.spawn_spatial(PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_scale(Vec3::splat(j)).with_translation(translation),
..default()
},
GridCell::<i128>::default(),
));
}
});
}
// light
commands.spawn((DirectionalLightBundle {
directional_light: DirectionalLight {
illuminance: 100_000.0,
// light
root.spawn_spatial(DirectionalLightBundle {
directional_light: DirectionalLight {
illuminance: 100_000.0,
..default()
},
..default()
},
..default()
},));
});
});
}
#[derive(Component, Reflect)]
@ -112,7 +111,6 @@ fn ui_setup(mut commands: Commands) {
..default()
}),
BigSpaceDebugText,
IgnoreFloatingOrigin,
));
commands.spawn((
@ -133,7 +131,6 @@ fn ui_setup(mut commands: Commands) {
})
.with_text_justify(JustifyText::Center),
FunFactText,
IgnoreFloatingOrigin,
));
}
@ -162,18 +159,18 @@ fn ui_text_system(
(With<BigSpaceDebugText>, Without<FunFactText>),
>,
mut fun_text: Query<&mut Text, (With<FunFactText>, Without<BigSpaceDebugText>)>,
ref_frames: ReferenceFrames<i128>,
time: Res<Time>,
origin: Query<GridTransformReadOnly<i128>, With<FloatingOrigin>>,
origin: Query<(Entity, GridTransformReadOnly<i128>), With<FloatingOrigin>>,
camera: Query<&CameraController>,
objects: Query<&Transform, With<Handle<Mesh>>>,
reference_frame: Res<RootReferenceFrame<i128>>,
) {
let origin = origin.single();
let translation = origin.transform.translation;
let (origin_entity, origin_pos) = origin.single();
let translation = origin_pos.transform.translation;
let grid_text = format!(
"GridCell: {}x, {}y, {}z",
origin.cell.x, origin.cell.y, origin.cell.z
origin_pos.cell.x, origin_pos.cell.y, origin_pos.cell.z
);
let translation_text = format!(
@ -181,7 +178,11 @@ fn ui_text_system(
translation.x, translation.y, translation.z
);
let real_position = reference_frame.grid_position_double(origin.cell, origin.transform);
let Some(ref_frame) = ref_frames.parent_frame(origin_entity) else {
return;
};
let real_position = ref_frame.grid_position_double(origin_pos.cell, origin_pos.transform);
let real_position_f64_text = format!(
"Combined (f64): {}x, {}y, {}z",
real_position.x, real_position.y, real_position.z

View File

@ -7,14 +7,16 @@
use bevy::prelude::*;
use big_space::{
reference_frame::RootReferenceFrame, FloatingOrigin, GridCell, IgnoreFloatingOrigin,
commands::BigSpaceCommands,
reference_frame::{local_origin::ReferenceFrames, ReferenceFrame},
FloatingOrigin, GridCell,
};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
big_space::FloatingOriginPlugin::<i128>::new(10.0, 1.0),
big_space::BigSpacePlugin::<i128>::default(),
))
.add_systems(Startup, (setup_scene, setup_ui))
.add_systems(Update, (rotator_system, toggle_plugin))
@ -24,45 +26,52 @@ fn main() {
/// You can put things really, really far away from the origin. The distance we use here is actually
/// quite small, because we want the mesh to still be visible when the floating origin is far from
/// the camera. If you go much further than this, the mesh will simply disappear in a *POOF* of
/// floating point error.
/// floating point error when we disable this plugin.
///
/// This plugin can function much further from the origin without any issues. Try setting this to:
/// 10_000_000_000_000_000_000_000_000_000_000_000_000
const DISTANCE: i128 = 21_000_000;
const DISTANCE: i128 = 2_000_000;
/// Move the floating origin back to the "true" origin when the user presses the spacebar to emulate
/// disabling the plugin. Normally you would make your active camera the floating origin to avoid
/// this issue.
fn toggle_plugin(
input: Res<ButtonInput<KeyCode>>,
settings: Res<RootReferenceFrame<i128>>,
ref_frames: ReferenceFrames<i128>,
mut text: Query<&mut Text>,
mut disabled: Local<bool>,
mut floating_origin: Query<&mut GridCell<i128>, With<FloatingOrigin>>,
mut floating_origin: Query<(Entity, &mut GridCell<i128>), With<FloatingOrigin>>,
) {
if input.just_pressed(KeyCode::Space) {
*disabled = !*disabled;
}
let mut origin_cell = floating_origin.single_mut();
let index_max = DISTANCE / settings.cell_edge_length() as i128;
let this_frame = ref_frames.parent_frame(floating_origin.single().0).unwrap();
let mut origin_cell = floating_origin.single_mut().1;
let index_max = DISTANCE / this_frame.cell_edge_length() as i128;
let increment = index_max / 100;
let msg = if *disabled {
if origin_cell.x > 0 {
origin_cell.x = 0.max(origin_cell.x - increment);
origin_cell.y = 0.max(origin_cell.y - increment);
origin_cell.z = 0.max(origin_cell.z - increment);
"Disabling..."
} else {
"Floating Origin Disabled"
}
} else if origin_cell.x < index_max {
origin_cell.x = index_max.min(origin_cell.x.saturating_add(increment));
origin_cell.y = index_max.min(origin_cell.y.saturating_add(increment));
origin_cell.z = index_max.min(origin_cell.z.saturating_add(increment));
"Enabling..."
} else {
"Floating Origin Enabled"
};
let dist = index_max.saturating_sub(origin_cell.x) * settings.cell_edge_length() as i128;
let dist = index_max.saturating_sub(origin_cell.x) * this_frame.cell_edge_length() as i128;
let thousands = |num: i128| {
num.to_string()
@ -84,96 +93,69 @@ struct Rotator;
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Rotator>>) {
for mut transform in &mut query {
transform.rotate_x(time.delta_seconds());
transform.rotate_y(time.delta_seconds());
}
}
fn setup_ui(mut commands: Commands) {
commands.spawn((
TextBundle {
style: Style {
align_self: AlignSelf::FlexStart,
flex_direction: FlexDirection::Column,
..Default::default()
commands.spawn(
TextBundle::from_section(
"",
TextStyle {
font_size: 30.0,
..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,
));
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
}
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
reference_frame: Res<RootReferenceFrame<i128>>,
) {
let mesh_handle = meshes.add(Sphere::new(1.5).mesh());
let matl_handle = materials.add(StandardMaterial {
base_color: Color::rgb(0.8, 0.7, 0.6),
..default()
});
fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_big_space(ReferenceFrame::<i128>::default(), |root| {
let d = DISTANCE / root.frame().cell_edge_length() as i128;
let distant_grid_cell = GridCell::<i128>::new(d, d, d);
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 show what happens as the camera is far from the origin, to emulate what
// happens when this plugin isn't used.
root.spawn_spatial((distant_grid_cell, FloatingOrigin));
// Normally, we would put the floating origin on the camera. However in this example, we want to
// show what happens as the camera is far from the origin, to emulate what happens when this
// plugin isn't used.
commands.spawn((
PbrBundle {
mesh: meshes.add(Sphere::default().mesh()),
material: materials.add(StandardMaterial::from(Color::RED)),
transform: Transform::from_scale(Vec3::splat(10000.0)),
..default()
},
distant_grid_cell,
FloatingOrigin,
));
commands
.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
root.spawn_spatial((
SceneBundle {
scene: asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0"),
transform: Transform::from_scale(Vec3::splat(0.2)),
..default()
},
distant_grid_cell,
Rotator,
))
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: mesh_handle,
material: matl_handle,
transform: Transform::from_xyz(0.0, 0.0, 4.0),
parent.spawn(SceneBundle {
scene: asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0"),
transform: Transform::from_xyz(0.0, 0.0, 20.0),
..default()
});
});
// light
commands.spawn((
DirectionalLightBundle {
transform: Transform::from_xyz(4.0, -10.0, -4.0),
..default()
},
distant_grid_cell,
));
// camera
commands.spawn((
Camera3dBundle {
transform: Transform::from_xyz(8.0, -8.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
},
distant_grid_cell,
));
// light
root.spawn_spatial((
DirectionalLightBundle {
transform: Transform::from_xyz(4.0, -10.0, -4.0),
..default()
},
distant_grid_cell,
));
// camera
root.spawn_spatial((
Camera3dBundle {
transform: Transform::from_xyz(8.0, 8.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
},
distant_grid_cell,
));
});
}

View File

@ -1,105 +1,107 @@
//! 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,
};
use big_space::{commands::BigSpaceCommands, reference_frame::ReferenceFrame, FloatingOrigin};
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(),
bevy_inspector_egui::quick::WorldInspectorPlugin::new(),
big_space::BigSpacePlugin::<i64>::default(),
big_space::camera::CameraControllerPlugin::<i64>::default(),
big_space::debug::FloatingOriginDebugPlugin::<i64>::default(),
))
.add_systems(Startup, setup_scene)
.run()
}
// The nearby object is 200 meters away from us. The distance object is 100 quadrillion meters away
// from us, and has a child that is 100 quadrillion meters toward us (relative its parent) minus 200
// meters. If we had infinite precision, the child of the distant object would be at the same
// position as the nearby object, only 200 meters away.
const DISTANT: DVec3 = DVec3::new(1e17, 1e17, 0.0);
const NEARBY: DVec3 = DVec3::new(200.0, 200.0, 0.0);
// The nearby object is NEARBY meters away from us. The distance object is DISTANT meters away from
// us, and has a child that is DISTANT meters toward us (relative its parent) minus NEARBY meters.
const DISTANT: DVec3 = DVec3::new(1e10, 1e10, 1e10);
const SPHERE_RADIUS: f32 = 10.0;
const NEARBY: Vec3 = Vec3::new(SPHERE_RADIUS * 20.0, SPHERE_RADIUS * 20.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 mesh_handle = meshes.add(Sphere::new(SPHERE_RADIUS).mesh());
let matl_handle = materials.add(StandardMaterial {
base_color: Color::rgb(0.8, 0.7, 0.6),
..default()
});
// A red sphere located nearby
commands.spawn((
PbrBundle {
mesh: mesh_handle.clone(),
material: materials.add(Color::RED),
transform: Transform::from_translation(NEARBY.as_vec3()),
..default()
},
GridCell::<i128>::default(),
));
let parent = root.translation_to_grid(DISTANT);
// This function introduces a small amount of error, because it can only work up to double
// precision floats. (f64).
let child = root.translation_to_grid(-DISTANT + NEARBY);
commands
.spawn((
// A sphere very far from the origin
PbrBundle {
commands.spawn_big_space(
ReferenceFrame::<i64>::new(SPHERE_RADIUS * 100.0, 0.0),
|root_frame| {
root_frame.spawn_spatial(PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_translation(parent.1),
material: materials.add(Color::BLUE),
transform: Transform::from_translation(NEARBY),
..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),
});
let parent = root_frame.frame().translation_to_grid(DISTANT);
root_frame.with_frame(
ReferenceFrame::new(SPHERE_RADIUS * 100.0, 0.0),
|parent_frame| {
// This function introduces a small amount of error, because it can only work up to
// double precision floats. (f64).
let child = parent_frame
.frame()
.translation_to_grid(-DISTANT + NEARBY.as_dvec3());
parent_frame.insert(PbrBundle {
mesh: mesh_handle.clone(),
material: matl_handle.clone(),
transform: Transform::from_translation(parent.1),
..default()
});
parent_frame.insert(parent.0);
parent_frame.with_children(|child_builder| {
// 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.
child_builder.spawn((
PbrBundle {
mesh: mesh_handle,
material: materials.add(Color::GREEN),
transform: Transform::from_translation(child.1),
..default()
},
child.0,
));
});
},
);
root_frame.spawn_spatial(DirectionalLightBundle {
transform: Transform::from_xyz(4.0, -10.0, -4.0),
..default()
});
root_frame.spawn_spatial((
Camera3dBundle {
transform: Transform::from_translation(
NEARBY + Vec3::new(0.0, 0.0, SPHERE_RADIUS * 10.0),
)
.looking_at(NEARBY, Vec3::Y),
projection: Projection::Perspective(PerspectiveProjection {
near: (SPHERE_RADIUS * 0.1).min(0.1),
..default()
}),
..default()
},
child.0,
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),
));
});
// 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(NEARBY.as_vec3() + Vec3::new(0.0, 0.0, 20.0))
.looking_at(NEARBY.as_vec3(), 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),
));
);
}

View File

@ -1,11 +1,21 @@
use std::collections::VecDeque;
/// 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 bevy::{
core_pipeline::bloom::BloomSettings,
math::DVec3,
pbr::{CascadeShadowConfigBuilder, NotShadowCaster},
prelude::*,
render::camera::Exposure,
transform::TransformSystem,
};
use big_space::{
camera::CameraController,
reference_frame::{ReferenceFrame, RootReferenceFrame},
FloatingOrigin, GridCell,
camera::{CameraController, CameraInput},
commands::BigSpaceCommands,
reference_frame::ReferenceFrame,
FloatingOrigin,
};
use rand::Rng;
@ -13,22 +23,52 @@ fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
big_space::FloatingOriginPlugin::<i64>::default(),
big_space::debug::FloatingOriginDebugPlugin::<i64>::default(),
// bevy_inspector_egui::quick::WorldInspectorPlugin::new(),
big_space::BigSpacePlugin::<i64>::new(true),
// 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,
brightness: 200.0,
})
.add_systems(Startup, setup)
.add_systems(Update, rotate)
.add_systems(Startup, spawn_solar_system)
.add_systems(
PostUpdate,
(
rotate,
lighting
.in_set(TransformSystem::TransformPropagate)
.after(bevy::transform::systems::sync_simple_transforms)
.after(bevy::transform::systems::propagate_transforms)
.after(big_space::FloatingOriginSet::PropagateLowPrecision),
cursor_grab_system,
springy_ship
.after(big_space::camera::default_camera_inputs)
.before(big_space::camera::camera_controller::<i64>),
),
)
.register_type::<Sun>()
.register_type::<Rotates>()
.run()
}
#[derive(Component)]
const EARTH_ORBIT_RADIUS_M: f64 = 149.60e9;
const EARTH_RADIUS_M: f64 = 6.371e6;
const SUN_RADIUS_M: f64 = 695_508_000_f64;
const MOON_RADIUS_M: f64 = 1.7375e6;
#[derive(Component, Reflect)]
struct Sun;
#[derive(Component, Reflect)]
struct PrimaryLight;
#[derive(Component, Reflect)]
struct Spaceship;
#[derive(Component, Reflect)]
struct Rotates(f32);
fn rotate(mut rotate_query: Query<(&mut Transform, &Rotates)>) {
@ -37,157 +77,251 @@ fn rotate(mut rotate_query: Query<(&mut Transform, &Rotates)>) {
}
}
fn setup(
fn lighting(
mut light: Query<(&mut Transform, &mut GlobalTransform), With<PrimaryLight>>,
sun: Query<&GlobalTransform, (With<Sun>, Without<PrimaryLight>)>,
) {
let sun_pos = sun.single().translation();
let (mut light_tr, mut light_gt) = light.single_mut();
light_tr.look_at(-sun_pos, Vec3::Y);
*light_gt = (*light_tr).into();
}
fn springy_ship(
cam_input: Res<CameraInput>,
mut ship: Query<&mut Transform, With<Spaceship>>,
mut desired_dir: Local<(Vec3, Quat)>,
mut smoothed_rot: Local<VecDeque<Vec3>>,
) {
desired_dir.0 = DVec3::new(cam_input.right, cam_input.up, -cam_input.forward).as_vec3()
* (1.0 + cam_input.boost as u8 as f32);
smoothed_rot.truncate(15);
smoothed_rot.push_front(DVec3::new(cam_input.pitch, cam_input.yaw, cam_input.roll).as_vec3());
let avg_rot = smoothed_rot.iter().sum::<Vec3>() / smoothed_rot.len() as f32;
use std::f32::consts::*;
desired_dir.1 = Quat::IDENTITY.slerp(
Quat::from_euler(
EulerRot::XYZ,
avg_rot.x.clamp(-FRAC_PI_4, FRAC_PI_4),
avg_rot.y.clamp(-FRAC_PI_4, FRAC_PI_4),
avg_rot.z.clamp(-FRAC_PI_4, FRAC_PI_4),
),
0.2,
) * Quat::from_rotation_y(PI);
ship.single_mut().translation = ship
.single_mut()
.translation
.lerp(desired_dir.0 * Vec3::new(0.5, 0.5, -2.0), 0.02);
ship.single_mut().rotation = ship.single_mut().rotation.slerp(desired_dir.1, 0.02);
}
fn spawn_solar_system(
asset_server: Res<AssetServer>,
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 sun_mesh_handle = meshes.add(Sphere::new(SUN_RADIUS_M as f32).mesh().ico(6).unwrap());
let earth_mesh_handle = meshes.add(Sphere::new(1.0).mesh().ico(35).unwrap());
let moon_mesh_handle = meshes.add(Sphere::new(MOON_RADIUS_M as f32).mesh().ico(15).unwrap());
let ball_mesh_handle = meshes.add(Sphere::new(5.0).mesh().ico(5).unwrap());
let plane_mesh_handle = meshes.add(Plane3d::new(Vec3::X));
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(),
commands.spawn((
PrimaryLight,
DirectionalLightBundle {
directional_light: DirectionalLight {
color: Color::WHITE,
illuminance: 120_000.,
shadows_enabled: true,
..default()
},
));
}
cascade_shadow_config: CascadeShadowConfigBuilder {
num_cascades: 4,
minimum_distance: 0.1,
maximum_distance: 10_000.0,
first_cascade_far_bound: 100.0,
overlap_proportion: 0.2,
}
.build(),
..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((
commands.spawn_big_space(ReferenceFrame::<i64>::default(), |root_frame| {
root_frame.with_frame_default(|sun| {
sun.insert((Sun, Name::new("Sun")));
sun.spawn_spatial((
PbrBundle {
mesh: sphere(moon_radius_m),
material: moon_mat,
transform: Transform::from_translation(moon_pos),
mesh: sun_mesh_handle,
material: materials.add(StandardMaterial {
base_color: Color::WHITE,
emissive: Color::rgb_linear(100000000., 100000000., 100000000.),
..default()
}),
..default()
},
moon_cell,
NotShadowCaster,
));
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,
let earth_pos = DVec3::Z * EARTH_ORBIT_RADIUS_M;
let (earth_cell, earth_pos) = sun.frame().translation_to_grid(earth_pos);
sun.with_frame_default(|earth| {
earth.insert((
Name::new("Earth"),
earth_cell,
PbrBundle {
mesh: earth_mesh_handle,
material: materials.add(StandardMaterial {
base_color: Color::BLUE,
perceptual_roughness: 0.8,
reflectance: 1.0,
..default()
}),
transform: Transform::from_translation(earth_pos)
.with_scale(Vec3::splat(EARTH_RADIUS_M as f32)),
..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),
));
Rotates(0.000001),
));
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 moon_orbit_radius_m = 385e6;
let moon_pos = DVec3::NEG_Z * moon_orbit_radius_m;
let (moon_cell, moon_pos) = earth.frame().translation_to_grid(moon_pos);
earth.spawn_spatial((
Name::new("Moon"),
PbrBundle {
mesh: moon_mesh_handle,
material: materials.add(StandardMaterial {
base_color: Color::GRAY,
perceptual_roughness: 1.0,
reflectance: 0.0,
..default()
}),
transform: Transform::from_translation(moon_pos),
..default()
},
moon_cell,
));
let ball_mat = materials.add(StandardMaterial {
base_color: Color::FUCHSIA,
perceptual_roughness: 1.0,
reflectance: 0.0,
..default()
let ball_pos =
DVec3::X * (EARTH_RADIUS_M + 1.0) + DVec3::NEG_Z * 30.0 + DVec3::Y * 10.0;
let (ball_cell, ball_pos) = earth.frame().translation_to_grid(ball_pos);
earth
.spawn_spatial((ball_cell, Transform::from_translation(ball_pos)))
.with_children(|children| {
children.spawn((PbrBundle {
mesh: ball_mesh_handle,
material: materials.add(StandardMaterial {
base_color: Color::WHITE,
..default()
}),
..default()
},));
children.spawn((PbrBundle {
mesh: plane_mesh_handle,
material: materials.add(StandardMaterial {
base_color: Color::DARK_GREEN,
perceptual_roughness: 1.0,
reflectance: 0.0,
..default()
}),
transform: Transform::from_scale(Vec3::splat(100.0))
.with_translation(Vec3::X * -5.0),
..default()
},));
});
let cam_pos = DVec3::X * (EARTH_RADIUS_M + 1.0);
let (cam_cell, cam_pos) = earth.frame().translation_to_grid(cam_pos);
earth.with_frame_default(|camera| {
camera.insert((
Transform::from_translation(cam_pos).looking_to(Vec3::NEG_Z, Vec3::X),
CameraController::default() // Built-in camera controller
.with_speed_bounds([0.1, 10e35])
.with_smoothness(0.98, 0.98)
.with_speed(1.0),
cam_cell,
));
camera.spawn_spatial((
FloatingOrigin,
Camera3dBundle {
transform: Transform::from_xyz(0.0, 4.0, 22.0),
camera: Camera {
hdr: true,
..default()
},
exposure: Exposure::SUNLIGHT,
..default()
},
BloomSettings::default(),
));
camera.with_children(|camera| {
camera.spawn((
Spaceship,
SceneBundle {
scene: asset_server
.load("models/low_poly_spaceship/scene.gltf#Scene0"),
transform: Transform::from_rotation(Quat::from_rotation_y(
std::f32::consts::PI,
)),
..default()
},
));
});
});
});
});
commands.spawn((
PbrBundle {
mesh: sphere(1.0),
material: ball_mat,
transform: Transform::from_translation(ball_pos),
..default()
},
ball_cell,
let star_mat = materials.add(StandardMaterial {
base_color: Color::WHITE,
emissive: Color::rgb_linear(100000., 100000., 100000.),
..default()
});
let star_mesh_handle = meshes.add(Sphere::new(1e10).mesh().ico(5).unwrap());
let mut rng = rand::thread_rng();
(0..1000).for_each(|_| {
root_frame.spawn_spatial((
star_mesh_handle.clone(),
star_mat.clone(),
Transform::from_xyz(
(rng.gen::<f32>() - 0.5) * 1e14,
(rng.gen::<f32>() - 0.5) * 1e14,
(rng.gen::<f32>() - 0.5) * 1e14,
),
));
});
});
}
fn cursor_grab_system(
mut windows: Query<&mut Window, With<bevy::window::PrimaryWindow>>,
mut cam: ResMut<big_space::camera::CameraInput>,
btn: Res<ButtonInput<MouseButton>>,
key: Res<ButtonInput<KeyCode>>,
) {
let Some(mut window) = windows.get_single_mut().ok() else {
return;
};
if btn.just_pressed(MouseButton::Right) {
window.cursor.grab_mode = bevy::window::CursorGrabMode::Locked;
window.cursor.visible = false;
// window.mode = WindowMode::BorderlessFullscreen;
cam.defaults_disabled = false;
}
if key.just_pressed(KeyCode::Escape) {
window.cursor.grab_mode = bevy::window::CursorGrabMode::None;
window.cursor.visible = true;
// window.mode = WindowMode::Windowed;
cam.defaults_disabled = true;
}
}

270
examples/split_screen.rs Normal file
View File

@ -0,0 +1,270 @@
//! Demonstrates how a single bevy world can contain multiple big_space hierarchies, each rendered
//! relative to a floating origin inside that big space.
//!
//! This takes the simplest approach, of simply duplicating the worlds and players for each split
//! screen, and synchronizing the player locations between both.
use bevy::{
prelude::*,
render::{camera::Viewport, view::RenderLayers},
transform::TransformSystem,
};
use big_space::{
camera::{CameraController, CameraControllerPlugin},
commands::BigSpaceCommands,
reference_frame::ReferenceFrame,
world_query::{GridTransform, GridTransformReadOnly},
BigSpacePlugin, FloatingOrigin,
};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::<i32>::default(),
big_space::debug::FloatingOriginDebugPlugin::<i32>::default(),
CameraControllerPlugin::<i32>::default(),
))
.add_systems(Startup, setup)
.add_systems(Update, set_camera_viewports)
.add_systems(
PostUpdate,
update_cameras
.after(big_space::camera::camera_controller::<i32>)
.before(TransformSystem::TransformPropagate),
)
.run()
}
#[derive(Component)]
struct LeftCamera;
#[derive(Component)]
struct RightCamera;
#[derive(Component)]
struct LeftCameraReplicated;
#[derive(Component)]
struct RightCameraReplicated;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
DirectionalLightBundle {
transform: Transform::default().looking_to(Vec3::NEG_ONE, Vec3::Y),
..default()
},
RenderLayers::all(),
));
// Big Space 1
commands.spawn_big_space(ReferenceFrame::<i32>::default(), |root_frame| {
root_frame
.spawn_spatial((
Camera3dBundle {
transform: Transform::from_xyz(1_000_000.0 - 10.0, 100_005.0, 0.0)
.looking_to(Vec3::NEG_X, Vec3::Y),
..default()
},
CameraController::default().with_smoothness(0.8, 0.8),
RenderLayers::layer(2),
LeftCamera,
FloatingOrigin,
))
.with_children(|child_builder| {
child_builder.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 2.0, 1.0)),
material: materials.add(StandardMaterial {
base_color: Color::YELLOW,
..default()
}),
..default()
},
RenderLayers::layer(2),
));
});
root_frame.spawn_spatial((
RightCameraReplicated,
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 2.0, 1.0)),
material: materials.add(StandardMaterial {
base_color: Color::PINK,
..default()
}),
..default()
},
RenderLayers::layer(2),
));
root_frame.spawn_spatial((
PbrBundle {
mesh: meshes.add(Sphere::new(1.0).mesh().ico(35).unwrap()),
material: materials.add(StandardMaterial {
base_color: Color::BLUE,
..default()
}),
transform: Transform::from_xyz(1_000_000.0, 0.0, 0.0)
.with_scale(Vec3::splat(100_000.0)),
..default()
},
RenderLayers::layer(2),
));
root_frame.spawn_spatial((
PbrBundle {
mesh: meshes.add(Sphere::new(1.0).mesh().ico(35).unwrap()),
material: materials.add(StandardMaterial {
base_color: Color::GREEN,
..default()
}),
transform: Transform::from_xyz(-1_000_000.0, 0.0, 0.0)
.with_scale(Vec3::splat(100_000.0)),
..default()
},
RenderLayers::layer(2),
));
});
// Big Space 2
commands.spawn_big_space(ReferenceFrame::<i32>::default(), |root_frame| {
root_frame
.spawn_spatial((
Camera3dBundle {
transform: Transform::from_xyz(1_000_000.0, 100_005.0, 0.0)
.looking_to(Vec3::NEG_X, Vec3::Y),
camera: Camera {
order: 1,
clear_color: ClearColorConfig::None,
..default()
},
..default()
},
RenderLayers::layer(1),
RightCamera,
FloatingOrigin,
))
.with_children(|child_builder| {
child_builder.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 2.0, 1.0)),
material: materials.add(StandardMaterial {
base_color: Color::PINK,
..default()
}),
..default()
},
RenderLayers::layer(1),
));
});
root_frame.spawn_spatial((
LeftCameraReplicated,
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 2.0, 1.0)),
material: materials.add(StandardMaterial {
base_color: Color::YELLOW,
..default()
}),
..default()
},
RenderLayers::layer(1),
));
root_frame.spawn_spatial((
PbrBundle {
mesh: meshes.add(Sphere::new(1.0).mesh().ico(35).unwrap()),
material: materials.add(StandardMaterial {
base_color: Color::BLUE,
..default()
}),
transform: Transform::from_xyz(1_000_000.0, 0.0, 0.0)
.with_scale(Vec3::splat(100_000.0)),
..default()
},
RenderLayers::layer(1),
));
root_frame.spawn_spatial((
PbrBundle {
mesh: meshes.add(Sphere::new(1.0).mesh().ico(35).unwrap()),
material: materials.add(StandardMaterial {
base_color: Color::GREEN,
..default()
}),
transform: Transform::from_xyz(-1_000_000.0, 0.0, 0.0)
.with_scale(Vec3::splat(100_000.0)),
..default()
},
RenderLayers::layer(1),
));
});
}
#[allow(clippy::type_complexity)]
fn update_cameras(
left: Query<GridTransformReadOnly<i32>, With<LeftCamera>>,
mut left_rep: Query<
GridTransform<i32>,
(
With<LeftCameraReplicated>,
Without<RightCameraReplicated>,
Without<LeftCamera>,
Without<RightCamera>,
),
>,
right: Query<GridTransformReadOnly<i32>, With<RightCamera>>,
mut right_rep: Query<
GridTransform<i32>,
(
With<RightCameraReplicated>,
Without<LeftCameraReplicated>,
Without<LeftCamera>,
Without<RightCamera>,
),
>,
) {
*left_rep.single_mut().cell = *left.single().cell;
*left_rep.single_mut().transform = *left.single().transform;
*right_rep.single_mut().cell = *right.single().cell;
*right_rep.single_mut().transform = *right.single().transform;
}
fn set_camera_viewports(
windows: Query<&Window>,
mut resize_events: EventReader<bevy::window::WindowResized>,
mut left_camera: Query<&mut Camera, (With<LeftCamera>, Without<RightCamera>)>,
mut right_camera: Query<&mut Camera, With<RightCamera>>,
) {
// We need to dynamically resize the camera's viewports whenever the window size changes
// so then each camera always takes up half the screen.
// A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup.
for resize_event in resize_events.read() {
let window = windows.get(resize_event.window).unwrap();
let mut left_camera = left_camera.single_mut();
left_camera.viewport = Some(Viewport {
physical_position: UVec2::new(0, 0),
physical_size: UVec2::new(
window.resolution.physical_width() / 2,
window.resolution.physical_height(),
),
..default()
});
let mut right_camera = right_camera.single_mut();
right_camera.viewport = Some(Viewport {
physical_position: UVec2::new(window.resolution.physical_width() / 2, 0),
physical_size: UVec2::new(
window.resolution.physical_width() / 2,
window.resolution.physical_height(),
),
..default()
});
}
}

69
src/bundles.rs Normal file
View File

@ -0,0 +1,69 @@
//! Component bundles for big_space.
use crate::{precision::GridPrecision, reference_frame::ReferenceFrame, BigSpace, GridCell};
use bevy::prelude::*;
/// Minimal bundle needed to position an entity in floating origin space.
///
/// This is the floating origin equivalent of the [`bevy::prelude::SpatialBundle`].
#[derive(Bundle, Default)]
pub struct BigSpatialBundle<P: GridPrecision> {
/// The visibility of the entity.
#[cfg(feature = "bevy_render")]
pub visibility: Visibility,
/// The inherited visibility of the entity.
#[cfg(feature = "bevy_render")]
pub inherited: InheritedVisibility,
/// The view visibility of the entity.
#[cfg(feature = "bevy_render")]
pub view: ViewVisibility,
/// The transform of the entity.
pub transform: Transform,
/// The global transform of the entity.
pub global_transform: GlobalTransform,
/// The grid position of the entity
pub cell: GridCell<P>,
}
/// A [`SpatialBundle`] that also has a reference frame, allowing other high precision spatial
/// bundles to be nested within that reference frame.
///
/// This is the floating origin equivalent of the [`SpatialBundle`].
#[derive(Bundle, Default)]
pub struct BigReferenceFrameBundle<P: GridPrecision> {
/// The visibility of the entity.
#[cfg(feature = "bevy_render")]
pub visibility: Visibility,
/// The inherited visibility of the entity.
#[cfg(feature = "bevy_render")]
pub inherited: InheritedVisibility,
/// The view visibility of the entity.
#[cfg(feature = "bevy_render")]
pub view: ViewVisibility,
/// The transform of the entity.
pub transform: Transform,
/// The global transform of the entity for rendering, computed relative to the floating origin.
pub global_transform: GlobalTransform,
/// The grid position of the entity within
pub cell: GridCell<P>,
/// The reference frame
pub reference_frame: ReferenceFrame<P>,
}
/// The root of any [`BigSpace`] needs these components to function.
#[derive(Bundle, Default)]
pub struct BigSpaceRootBundle<P: GridPrecision> {
/// The visibility of the entity.
#[cfg(feature = "bevy_render")]
pub visibility: Visibility,
/// The inherited visibility of the entity.
#[cfg(feature = "bevy_render")]
pub inherited: InheritedVisibility,
/// The view visibility of the entity.
#[cfg(feature = "bevy_render")]
pub view: ViewVisibility,
/// The root reference frame
pub reference_frame: ReferenceFrame<P>,
/// Tracks the current floating origin
pub root: BigSpace,
}

View File

@ -6,14 +6,14 @@ use bevy::{
input::mouse::MouseMotion,
math::{DQuat, DVec3},
prelude::*,
render::primitives::Aabb,
render::{primitives::Aabb, view::RenderLayers},
transform::TransformSystem,
utils::hashbrown::HashSet,
};
use crate::{
precision::GridPrecision,
reference_frame::{local_origin::ReferenceFrames, RootReferenceFrame},
world_query::{GridTransform, GridTransformReadOnly},
precision::GridPrecision, reference_frame::local_origin::ReferenceFrames,
world_query::GridTransform,
};
/// Adds the `big_space` camera controller
@ -27,7 +27,7 @@ impl<P: GridPrecision> Plugin for CameraControllerPlugin<P> {
default_camera_inputs
.before(camera_controller::<P>)
.run_if(|input: Res<CameraInput>| !input.defaults_disabled),
nearest_objects::<P>.before(camera_controller::<P>),
nearest_objects_in_frame::<P>.before(camera_controller::<P>),
camera_controller::<P>.before(TransformSystem::TransformPropagate),
),
);
@ -93,7 +93,7 @@ impl Default for CameraController {
Self {
smoothness: 0.8,
rotational_smoothness: 0.5,
speed: 10e8,
speed: 1.0,
speed_bounds: [1e-17, 1e30],
slow_near_objects: true,
nearest_object: None,
@ -163,8 +163,8 @@ pub fn default_camera_inputs(
keyboard
.pressed(KeyCode::ControlLeft)
.then(|| cam.up -= 1.0);
keyboard.pressed(KeyCode::KeyQ).then(|| cam.roll += 1.0);
keyboard.pressed(KeyCode::KeyE).then(|| cam.roll -= 1.0);
keyboard.pressed(KeyCode::KeyQ).then(|| cam.roll += 2.0);
keyboard.pressed(KeyCode::KeyE).then(|| cam.roll -= 2.0);
keyboard
.pressed(KeyCode::ShiftLeft)
.then(|| cam.boost = true);
@ -174,25 +174,43 @@ pub fn default_camera_inputs(
}
}
/// Find the object nearest the camera
pub fn nearest_objects<P: GridPrecision>(
settings: Res<RootReferenceFrame<P>>,
objects: Query<(Entity, GridTransformReadOnly<P>, &Aabb)>,
mut camera: Query<(&mut CameraController, GridTransformReadOnly<P>)>,
/// Find the object nearest the camera, within the same reference frame as the camera.
pub fn nearest_objects_in_frame<P: GridPrecision>(
objects: Query<(
Entity,
&Transform,
&GlobalTransform,
&Aabb,
Option<&RenderLayers>,
)>,
mut camera: Query<(
Entity,
&mut CameraController,
&GlobalTransform,
Option<&RenderLayers>,
)>,
children: Query<&Children>,
) {
let Ok((mut camera, cam_pos)) = camera.get_single_mut() else {
let Ok((cam_entity, mut camera, cam_pos, cam_layer)) = camera.get_single_mut() else {
return;
};
let cam_layer = cam_layer.copied().unwrap_or(RenderLayers::all());
let cam_children: HashSet<Entity> = children.iter_descendants(cam_entity).collect();
let nearest_object = objects
.iter()
.map(|(entity, obj_pos, aabb)| {
let center_distance = settings
.grid_position_double(&(*obj_pos.cell - *cam_pos.cell), obj_pos.transform)
- cam_pos.transform.translation.as_dvec3();
.filter(|(entity, ..)| !cam_children.contains(entity))
.filter(|(.., obj_layer)| {
let obj_layer = obj_layer.copied().unwrap_or(RenderLayers::layer(0));
cam_layer.intersects(&obj_layer)
})
.map(|(entity, object_local, obj_pos, aabb, _)| {
let center_distance =
obj_pos.translation().as_dvec3() - cam_pos.translation().as_dvec3();
let nearest_distance = center_distance.length()
- (aabb.half_extents.as_dvec3() * obj_pos.transform.scale.as_dvec3())
- (aabb.half_extents.as_dvec3() * object_local.scale.as_dvec3())
.abs()
.max_element();
.min_element();
(entity, nearest_distance)
})
.filter(|v| v.1.is_finite())
@ -208,10 +226,7 @@ pub fn camera_controller<P: GridPrecision>(
mut camera: Query<(Entity, GridTransform<P>, &mut CameraController)>,
) {
for (camera, mut position, mut controller) in camera.iter_mut() {
let Some(frame) = frames
.get_handle(camera)
.map(|handle| frames.resolve_handle(handle))
else {
let Some(frame) = frames.parent_frame(camera) else {
continue;
};
let speed = match (controller.nearest_object, controller.slow_near_objects) {

211
src/commands.rs Normal file
View File

@ -0,0 +1,211 @@
//! Adds `big_space`-specific commands to bevy's `Commands`.
use std::marker::PhantomData;
use crate::{reference_frame::ReferenceFrame, *};
use bevy::prelude::*;
use self::precision::GridPrecision;
/// Adds `big_space` commands to bevy's `Commands`.
pub trait BigSpaceCommands<P: GridPrecision> {
/// Spawn a root [`BigSpace`] [`ReferenceFrame`].
fn spawn_big_space(
&mut self,
root_frame: ReferenceFrame<P>,
child_builder: impl FnOnce(&mut ReferenceFrameCommands<P>),
);
}
impl<P: GridPrecision> BigSpaceCommands<P> for Commands<'_, '_> {
fn spawn_big_space(
&mut self,
reference_frame: ReferenceFrame<P>,
root_frame: impl FnOnce(&mut ReferenceFrameCommands<P>),
) {
let mut entity_commands = self.spawn((
#[cfg(feature = "bevy_render")]
Visibility::default(),
#[cfg(feature = "bevy_render")]
InheritedVisibility::default(),
#[cfg(feature = "bevy_render")]
ViewVisibility::default(),
BigSpace::default(),
));
let mut cmd = ReferenceFrameCommands {
entity: entity_commands.id(),
commands: entity_commands.commands(),
reference_frame,
};
root_frame(&mut cmd);
}
}
/// Build [`big_space`](crate) hierarchies more easily, with access to reference frames.
pub struct ReferenceFrameCommands<'a, P: GridPrecision> {
entity: Entity,
commands: Commands<'a, 'a>,
reference_frame: ReferenceFrame<P>,
}
impl<'a, P: GridPrecision> ReferenceFrameCommands<'a, P> {
/// Get a reference to the current reference frame.
pub fn frame(&mut self) -> &ReferenceFrame<P> {
&self.reference_frame
}
/// Insert a component on this reference frame
pub fn insert(&mut self, bundle: impl Bundle) -> &mut Self {
self.commands.entity(self.entity).insert(bundle);
self
}
/// Add a high-precision spatial entity ([`GridCell`]) to this reference frame, and insert the
/// provided bundle.
pub fn spawn_spatial(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> {
let mut entity_commands = self.commands.entity(self.entity);
let parent = entity_commands.id();
let mut commands = entity_commands.commands();
let entity = commands
.spawn((
#[cfg(feature = "bevy_render")]
Visibility::default(),
#[cfg(feature = "bevy_render")]
InheritedVisibility::default(),
#[cfg(feature = "bevy_render")]
ViewVisibility::default(),
Transform::default(),
GlobalTransform::default(),
GridCell::<P>::default(),
))
.insert(bundle)
.id();
commands.entity(entity).set_parent(parent);
SpatialEntityCommands {
entity,
commands: self.commands.reborrow(),
phantom: PhantomData,
}
}
/// Returns the [`Entity``] id of the entity.
pub fn id(&self) -> Entity {
self.entity
}
/// Add a high-precision spatial entity ([`GridCell`]) to this reference frame, and apply entity commands to it via the closure. This allows you to insert bundles on this new spatial entities, and add more children to it.
pub fn with_spatial(
&mut self,
spatial: impl FnOnce(&mut SpatialEntityCommands<P>),
) -> &mut Self {
spatial(&mut self.spawn_spatial(()));
self
}
/// Add a high-precision spatial entity ([`GridCell`]) to this reference frame, and apply entity commands to it via the closure. This allows you to insert bundles on this new spatial entities, and add more children to it.
pub fn with_frame(
&mut self,
new_frame: ReferenceFrame<P>,
builder: impl FnOnce(&mut ReferenceFrameCommands<P>),
) -> &mut Self {
builder(&mut self.spawn_frame(new_frame, ()));
self
}
/// Same as [`Self::with_frame`], but using the default [`ReferenceFrame`] value.
pub fn with_frame_default(
&mut self,
builder: impl FnOnce(&mut ReferenceFrameCommands<P>),
) -> &mut Self {
self.with_frame(ReferenceFrame::default(), builder)
}
/// Spawn a reference frame as a child of the current reference frame.
pub fn spawn_frame(
&mut self,
new_frame: ReferenceFrame<P>,
bundle: impl Bundle,
) -> ReferenceFrameCommands<P> {
let mut entity_commands = self.commands.entity(self.entity);
let parent = entity_commands.id();
let mut commands = entity_commands.commands();
let entity = commands
.spawn((
#[cfg(feature = "bevy_render")]
Visibility::default(),
#[cfg(feature = "bevy_render")]
InheritedVisibility::default(),
#[cfg(feature = "bevy_render")]
ViewVisibility::default(),
Transform::default(),
GlobalTransform::default(),
GridCell::<P>::default(),
ReferenceFrame::<P>::default(),
))
.insert(bundle)
.id();
commands.entity(entity).set_parent(parent);
ReferenceFrameCommands {
entity,
commands: self.commands.reborrow(),
reference_frame: new_frame,
}
}
/// Spawn a reference frame as a child of the current reference frame. The first argument in the
/// closure is the paren't reference frame.
pub fn spawn_frame_default(&mut self, bundle: impl Bundle) -> ReferenceFrameCommands<P> {
self.spawn_frame(ReferenceFrame::default(), bundle)
}
/// Takes a closure which provides this reference frame and a [`ChildBuilder`].
pub fn with_children(&mut self, spawn_children: impl FnOnce(&mut ChildBuilder)) -> &mut Self {
self.commands
.entity(self.entity)
.with_children(|child_builder| spawn_children(child_builder));
self
}
}
/// Insert the reference frame on drop.
impl<'a, P: GridPrecision> Drop for ReferenceFrameCommands<'a, P> {
fn drop(&mut self) {
self.commands
.entity(self.entity)
.insert(std::mem::take(&mut self.reference_frame));
}
}
/// Build [`big_space`](crate) hierarchies more easily, with access to reference frames.
pub struct SpatialEntityCommands<'a, P: GridPrecision> {
entity: Entity,
commands: Commands<'a, 'a>,
phantom: PhantomData<P>,
}
impl<'a, P: GridPrecision> SpatialEntityCommands<'a, P> {
/// Insert a component on this reference frame
pub fn insert(&mut self, bundle: impl Bundle) -> &mut Self {
self.commands.entity(self.entity).insert(bundle);
self
}
/// Takes a closure which provides a [`ChildBuilder`].
pub fn with_children(&mut self, spawn_children: impl FnOnce(&mut ChildBuilder)) -> &mut Self {
self.commands
.entity(self.entity)
.with_children(|child_builder| spawn_children(child_builder));
self
}
/// Returns the [`Entity``] id of the entity.
pub fn id(&self) -> Entity {
self.entity
}
}

View File

@ -28,17 +28,21 @@ impl<P: GridPrecision> Plugin for FloatingOriginDebugPlugin<P> {
pub fn update_debug_bounds<P: GridPrecision>(
mut gizmos: Gizmos,
reference_frames: ReferenceFrames<P>,
occupied_cells: Query<(Entity, &GridCell<P>), Without<FloatingOrigin>>,
occupied_cells: Query<(Entity, &GridCell<P>, Option<&FloatingOrigin>)>,
) {
for (cell_entity, cell) in occupied_cells.iter() {
let Some(frame) = reference_frames.get(cell_entity) else {
for (cell_entity, cell, origin) in occupied_cells.iter() {
let Some(frame) = reference_frames.parent_frame(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)
if origin.is_none() {
gizmos.cuboid(transform, Color::GREEN)
} else {
// gizmos.cuboid(transform, Color::rgba(0.0, 0.0, 1.0, 0.5))
}
}
}
@ -49,7 +53,7 @@ pub fn update_reference_frame_axes<P: GridPrecision>(
) {
for (transform, frame) in frames.iter() {
let start = transform.translation();
let len = frame.cell_edge_length() * 1.0;
let len = frame.cell_edge_length() * 2.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);

87
src/floating_origins.rs Normal file
View File

@ -0,0 +1,87 @@
//! A floating origin for camera-relative rendering, to maximize precision when converting to f32.
use bevy::{log::error, prelude::*, reflect::Reflect, utils::hashbrown::HashMap};
/// Marks the entity to use as the floating origin.
///
/// The [`GlobalTransform`] of all entities within this [`BigSpace`] will be computed relative to
/// this floating origin. There should always be exactly one entity marked with this component
/// within a [`BigSpace`].
#[derive(Component, Reflect)]
pub struct FloatingOrigin;
/// A "big space" is a hierarchy of high precision reference frames, rendered with a floating
/// origin. It is the root of this high precision hierarchy, and tracks the [`FloatingOrigin`]
/// inside this hierarchy.
///
/// This component must also be paired with a [`ReferenceFrame`](crate::ReferenceFrame), which
/// defines the properties of this root reference frame. A hierarchy can have many nested
/// `ReferenceFrame`s, but only one `BigSpace`, at the root.
///
/// Your world can have multiple [`BigSpace`]s, and they will remain completely independent. Each
/// big space uses the floating origin contained within it to compute the [`GlobalTransform`] of all
/// spatial entities within that `BigSpace`.
#[derive(Debug, Default, Component, Reflect)]
pub struct BigSpace {
/// Set the entity to use as the floating origin within this high precision hierarchy.
pub floating_origin: Option<Entity>,
}
impl BigSpace {
/// Return the this reference frame's floating origin if it exists and is a descendent of this
/// root.
///
/// `this_entity`: the entity this component belongs to.
pub(crate) fn validate_floating_origin(
&self,
this_entity: Entity,
parents: &Query<&Parent>,
) -> Option<Entity> {
let floating_origin = self.floating_origin?;
let origin_root_entity = parents.iter_ancestors(floating_origin).last()?;
Some(floating_origin).filter(|_| origin_root_entity == this_entity)
}
/// Automatically update all [`BigSpace`]s, finding the current floating origin entity within
/// their hierarchy. There should be one, and only one, [`FloatingOrigin`] component in a
/// `BigSpace` hierarchy.
pub fn find_floating_origin(
floating_origins: Query<Entity, With<FloatingOrigin>>,
parent_query: Query<&Parent>,
mut big_spaces: Query<(Entity, &mut BigSpace)>,
) {
let mut spaces_set = HashMap::new();
// Reset all floating origin fields, so we know if any are missing.
for (entity, mut space) in &mut big_spaces {
space.floating_origin = None;
spaces_set.insert(entity, 0);
}
// Navigate to the root of the hierarchy, starting from each floating origin. This is faster than the reverse direction because it is a tree, and an entity can only have a single parent, but many children. The root should have an empty floating_origin field.
for origin in &floating_origins {
let maybe_root = parent_query.iter_ancestors(origin).last();
if let Some((root, mut space)) =
maybe_root.and_then(|root| big_spaces.get_mut(root).ok())
{
let space_origins = spaces_set.entry(root).or_default();
*space_origins += 1;
if *space_origins > 1 {
error!(
"BigSpace {root:#?} has multiple floating origins. There must be exactly one. Resetting this big space and disabling the floating origin to avoid unexpected propagation behavior.",
);
space.floating_origin = None
} else {
space.floating_origin = Some(origin);
}
continue;
}
}
// Check if any big spaces did not have a floating origin.
for space in spaces_set
.iter()
.filter(|(_k, v)| **v == 0)
.map(|(k, _v)| k)
{
error!("BigSpace {space:#?} has no floating origins. There must be exactly one. Transform propagation will not work until there is a FloatingOrigin in the hierarchy.",)
}
}
}

View File

@ -2,35 +2,18 @@
use bevy::prelude::*;
use crate::precision::GridPrecision;
use crate::*;
/// Defines the grid cell this entity's `Transform` is relative to.
///
/// This component is generic over a few integer types to allow you to select the grid size you
/// need. These correspond to a total usable volume of a cube with the following edge lengths:
///
/// **Assuming you are using a grid cell edge length of 10,000 meters, and `1.0` == 1 meter**
///
/// - i8: 2,560 km = 74% of the diameter of the Moon
/// - i16: 655,350 km = 85% of the diameter of the Moon's orbit around Earth
/// - i32: 0.0045 light years = ~4 times the width of the solar system
/// - i64: 19.5 million light years = ~100 times the width of the milky way galaxy
/// - i128: 3.6e+26 light years = ~3.9e+15 times the width of the observable universe
///
/// where
///
/// `usable_edge_length = 2^(integer_bits) * grid_cell_edge_length`
///
/// # Note
///
/// Be sure you are using the same grid index precision everywhere. It might be a good idea to
/// define a type alias!
///
/// ```
/// # use big_space::GridCell;
/// type GalacticGrid = GridCell<i64>;
/// ```
use self::{precision::GridPrecision, reference_frame::ReferenceFrame};
/// The cell index an entity within a [`crate::ReferenceFrame`]'s grid. The [`Transform`] of an
/// entity with this component is a transformation from the center of this cell.
///
/// This component adds precision to the translation of an entity's [`Transform`]. In a
/// high-precision [`BigSpace`] world, the position of an entity is described by a [`Transform`]
/// *and* a [`GridCell`]. This component is the index of a cell inside a large grid defined by the
/// [`ReferenceFrame`], and the transform is the position of the entity relative to the center of
/// that cell.
#[derive(Component, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Reflect)]
#[reflect(Component, Default, PartialEq)]
pub struct GridCell<P: GridPrecision> {
@ -61,7 +44,32 @@ impl<P: GridPrecision> GridCell<P> {
y: P::ONE,
z: P::ONE,
};
/// If an entity's transform translation becomes larger than the limit specified in its
/// [`ReferenceFrame`], it will be relocated to the nearest grid cell to reduce the size of the
/// transform.
pub fn recenter_large_transforms(
reference_frames: Query<&ReferenceFrame<P>>,
mut changed_transform: Query<(&mut Self, &mut Transform, &Parent), Changed<Transform>>,
) {
changed_transform
.par_iter_mut()
.for_each(|(mut grid_pos, mut transform, parent)| {
let Ok(reference_frame) = reference_frames.get(parent.get()) else {
return;
};
if transform.as_ref().translation.abs().max_element()
> reference_frame.maximum_distance_from_origin()
{
let (grid_cell_delta, translation) = reference_frame
.imprecise_translation_to_grid(transform.as_ref().translation);
*grid_pos += grid_cell_delta;
transform.translation = translation;
}
});
}
}
impl<P: GridPrecision> std::ops::Add for GridCell<P> {
type Output = GridCell<P>;
@ -73,6 +81,7 @@ impl<P: GridPrecision> std::ops::Add for GridCell<P> {
}
}
}
impl<P: GridPrecision> std::ops::Sub for GridCell<P> {
type Output = GridCell<P>;
@ -84,6 +93,7 @@ impl<P: GridPrecision> std::ops::Sub for GridCell<P> {
}
}
}
impl<P: GridPrecision> std::ops::Add for &GridCell<P> {
type Output = GridCell<P>;
@ -91,6 +101,7 @@ impl<P: GridPrecision> std::ops::Add for &GridCell<P> {
(*self).add(*rhs)
}
}
impl<P: GridPrecision> std::ops::Sub for &GridCell<P> {
type Output = GridCell<P>;
@ -112,3 +123,23 @@ impl<P: GridPrecision> std::ops::SubAssign for GridCell<P> {
*self = self.sub(rhs);
}
}
impl<P: GridPrecision> std::ops::Mul<P> for GridCell<P> {
type Output = GridCell<P>;
fn mul(self, rhs: P) -> Self::Output {
GridCell {
x: self.x.mul(rhs),
y: self.y.mul(rhs),
z: self.z.mul(rhs),
}
}
}
impl<P: GridPrecision> std::ops::Mul<P> for &GridCell<P> {
type Output = GridCell<P>;
fn mul(self, rhs: P) -> Self::Output {
(*self).mul(rhs)
}
}

View File

@ -2,19 +2,70 @@
//! the observable universe, with no added dependencies, while remaining largely compatible with the
//! rest of the Bevy ecosystem.
//!
//! The next section explains the problem this solves in more detail, how this plugin works, and a
//! list of other solutions that were considered. If you'd like, you can instead skip ahead to
//! [Usage](crate#usage).
//!
//! ### Problem
//!
//! Objects far from the origin suffer from reduced precision, causing rendered meshes to jitter and
//! jiggle, and transformation calculations to encounter catastrophic cancellation.
//!
//! As the camera moves farther from the origin, the scale of floats needed to describe the position
//! of meshes and the camera get larger, which in turn means there is less precision available.
//! Consequently, when the matrix math is done to compute the position of objects in view space,
//! mesh vertices will be displaced due to this lost precision.
//! As a camera moves far from the origin, the values describing its x/y/z coordinates become large,
//! leaving less precision to the right of the decimal place. Consequently, when computing the
//! position of objects in view space, mesh vertices will be displaced due to this lost precision.
//!
//! This is a great little tool to calculate how much precision a floating point value has at a
//! given scale: <http://www.ehopkinson.com/floatprecision.html>.
//!
//! ### Possible Solutions
//!
//! There are many ways to solve this problem!
//!
//! - Periodic recentering: every time the camera moves far enough away from the origin, move it
//! back to the origin and apply the same offset to all other entities.
//! - Problem: Objects far from the camera will drift and accumulate error.
//! - Problem: No fixed reference frame.
//! - Problem: Recentering triggers change detection even for objects that did not move.
//! - Camera-relative coordinates: don't move the camera, move the world around the camera.
//! - Problem: Objects far from the camera will drift and accumulate error.
//! - Problem: No fixed reference frame
//! - Problem: Math is more complex when everything is relative to the camera.
//! - Problem: Rotating the camera requires recomputing transforms for everything.
//! - Problem: Camera movement triggers change detection even for objects that did not move.
//! - Problem: Incompatible with existing plugins that use `Transform`.
//! - Double precision coordinates: Store transforms in double precision
//! - Problem: Rendering still requires positions be in single precision, which either requires
//! using one of the above techniques, or emulating 64 bit precision in shaders.
//! - Problem: Updating double precision transforms is more expensive than single precision.
//! - Problem: Computing the `GlobalTransform` is more expensive than single precision.
//! - Problem: Size is limited to approximately the orbit of Saturn at human scales.
//! - Problem: Incompatible with existing plugins that use `Transform`.
//! - Chunks: Place objects in a large grid, and track the grid cell they are in,
//! - Problem: Requires a component to track the grid cell, in addition to the `Transform`.
//! - Problem: Computing the `GlobalTransform` is more expensive than single precision.
//!
//! ### Solution
//!
//! While using the [`FloatingOriginPlugin`], the position of entities is now defined with the
//! This plugin uses the last solution listed above. The most significant benefits of this method
//! over the others are:
//! - Absolute high-precision positions in space that do not change when the camera moves. The only
//! component that is affected by precision loss is the `GlobalTransform` used for rendering. The
//! `GridCell` and `Transform` only change when an entity moves. This is especially useful for
//! multiplayer - the server needs a source of truth for position that doesn't drift over time.
//! - Virtually limitless volume and scale; you can work at the scale of subatomic particles, across
//! the width of the observable universe. Double precision is downright suffocating in comparison.
//! - Uniform precision across the play area. Unlike double precision, the available precision
//! does not decrease as you move to the edge of the play area, it is instead relative to the
//! distance from the origin of the current grid cell.
//! - High precision coordinates are invisible if you don't need them. You can move objects using
//! their `Transform` alone, which results in decent ecosystem compatibility.
//! - High precision is completely opt-in. If you don't add the `GridCell` component to an entity,
//! it behaves like a normal single precision transform, with the same performance cost, yet it
//! can exist in the high precision hierarchy. This allows you to load in GLTFs or other
//! low-precision entity hierarchies with no added effort or cost.
//!
//! While using the [`BigSpacePlugin`], 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
@ -22,44 +73,72 @@
//! 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.
//!
//! `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
//! The grid adds precision to your transforms. If you are using (32-bit) `Transform`s on an `i32`
//! grid, you will have 64 bits of precision: 32 bits to address into a large integer grid, and 32
//! bits of floating point precision within a grid cell. This plugin is generic up to `i128` grids,
//! giving you up tp 160 bits of precision of translation.
//!
//! `ReferenceFrame`s - grids - can 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
//! rotation for all objects 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`].
//! inherited by all children in that reference frame, in high precision.
//!
//! 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*.
//! Entities at the root of bevy's entity hierarchy are not in any reference frame. This allows
//! plugins from the rest of the ecosystem to operate normally, such as bevy_ui, which relies on the
//! built in transform propagation system. This also means that if you don't need to place entities
//! in a high-precision reference frame, you don't have to, as the process is opt-in. The
//! high-precision hierarchical reference frames are explicit. Each high-precision tree must have a
//! [`BigSpaceRootBundle`] at the root, and each `BigSpace` is independent. This means that each
//! `BigSpace` has its own floating origin, which allows you to do things like rendering two players
//! on opposite ends of the universe simultaneously.
//!
//! The `GlobalTransform` of all entities is computed relative to the floating origin's grid cell.
//! Because of this, entities very far from the origin will have very large, imprecise positions.
//! However, this is always relative to the camera (floating origin), so these artifacts will always
//! be too far away to be seen, no matter where the camera moves. Because this only affects the
//! `GlobalTransform` and not the `Transform`, this also means that entities will never permanently
//! lose precision just because they were far from the origin at some point. The lossy calculation
//! only occurs when computing the `GlobalTransform` of entities, the high precision `GridCell` and
//! `Transform` are unaffected.
//! All of the above applies to the entity marked with the [`FloatingOrigin`] component. The
//! floating origin can be any high-precision entity in a `BigSpace`, it doesn't need to be a
//! camera. The only thing special about the entity marked as the floating origin, is that it is
//! used to compute the [`GlobalTransform`] of all other entities in that `BigSpace`. To an outside
//! observer, every high-precision entity within a `BigSpace` is confined to a box the size of a
//! grid cell - like a game of *Asteroids*. Only once you render the `BigSpace` from the point of
//! view of the floating origin, by calculating their `GlobalTransform`s, do entities appear very
//! distant from the floating origin.
//!
//! # Getting Started
//! As described above. the `GlobalTransform` of all entities is computed relative to the floating
//! origin's grid cell. Because of this, entities very far from the origin will have very large,
//! imprecise positions. However, this is always relative to the camera (floating origin), so these
//! artifacts will always be too far away to be seen, no matter where the camera moves. Because this
//! only affects the `GlobalTransform` and not the `Transform`, this also means that entities will
//! never permanently lose precision just because they were far from the origin at some point. The
//! lossy calculation only occurs when computing the `GlobalTransform` of entities, the high
//! precision `GridCell` and `Transform` are never touched.
//!
//! # Usage
//!
//! To start using this plugin, you will first need to choose how big your world should be! Do you
//! need an i8, or an i128? See [`GridPrecision`](crate::precision::GridPrecision) for more details
//! and documentation.
//!
//! 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
//! 2. Add the [`BigSpacePlugin`] to your `App`
//! 3. Spawn a [`BigSpace`] with [`spawn_big_space`](BigSpaceCommands::spawn_big_space), and spawn
//! entities in it.
//! 4. Add the [`FloatingOrigin`] to your active camera in the [`BigSpace`].
//!
//! Take a look at [`ReferenceFrame`] component for some useful helper methods.
//! To add more levels to the hierarchy, you can use [`ReferenceFrame`]s, which themselves can
//! contain high-precision spatial entities. Reference frames are useful when you want all objects
//! to move together in space, for example, objects on the surface of a planet rotating on its axis
//! and orbiting a star.
//!
//! Take a look at the [`ReferenceFrame`] component for some useful helper methods. The component
//! defines the scale of the grid, which is very important when computing distances between objects
//! in different cells. Note that the root [`BigSpace`] also has a [`ReferenceFrame`] component.
//!
//! # Moving Entities
//!
//! For the most part, you can update the position of entities normally while using this plugin, and
//! it will automatically handle the tricky bits. However, there is one big caveat:
//! it will automatically handle the tricky bits. If you move an entity too far from the center of
//! its grid cell, the plugin will automatically move it into the correct cell for you. However,
//! there is one big caveat:
//!
//! **Avoid setting position absolutely, instead prefer applying a relative delta**
//!
@ -91,281 +170,37 @@
//! 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 [`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.
//! [`Transform`] of that entity using [`ReferenceFrame::translation_to_grid`].
//!
//! # Next Steps
//!
//! Take a look at the examples to see usage, as well as explanation of these use cases and topics.
#![allow(clippy::type_complexity)]
#![deny(missing_docs)]
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;
#![warn(missing_docs)]
pub mod bundles;
pub mod commands;
pub mod floating_origins;
pub mod grid_cell;
pub mod plugin;
pub mod precision;
pub mod propagation;
pub mod reference_frame;
pub mod validation;
pub mod world_query;
pub use grid_cell::GridCell;
pub use propagation::IgnoreFloatingOrigin;
#[cfg(feature = "debug")]
pub mod debug;
#[cfg(feature = "camera")]
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.
pub grid_edge_length: f32,
/// How far past the extents of a cell an entity must travel before a grid recentering occurs.
/// This prevents entities from rapidly switching between cells when moving along a boundary.
pub switching_threshold: f32,
phantom: PhantomData<P>,
}
impl<P: GridPrecision> Default for FloatingOriginPlugin<P> {
fn default() -> Self {
Self::new(2_000f32, 100f32)
}
}
impl<P: GridPrecision> FloatingOriginPlugin<P> {
/// Construct a new plugin with the following settings.
pub fn new(grid_edge_length: f32, switching_threshold: f32) -> Self {
FloatingOriginPlugin {
grid_edge_length,
switching_threshold,
phantom: PhantomData,
}
}
}
impl<P: GridPrecision + Reflect + FromReflect + TypePath> Plugin for FloatingOriginPlugin<P> {
fn build(&self, app: &mut App) {
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
enum FloatingOriginSet {
RecenterLargeTransforms,
LocalFloatingOrigins,
RootGlobalTransforms,
PropagateTransforms,
}
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, system_set_config())
.add_systems(PostUpdate, system_set_config());
}
}
/// Minimal bundle needed to position an entity in floating origin space.
///
/// This is the floating origin equivalent of the [`SpatialBundle`].
#[derive(Bundle, Default)]
pub struct FloatingSpatialBundle<P: GridPrecision> {
/// The visibility of the entity.
#[cfg(feature = "bevy_render")]
pub visibility: Visibility,
/// The inherited visibility of the entity.
#[cfg(feature = "bevy_render")]
pub inherited: InheritedVisibility,
/// The view visibility of the entity.
#[cfg(feature = "bevy_render")]
pub view: ViewVisibility,
/// The transform of the entity.
pub transform: Transform,
/// The global transform of the entity.
pub global_transform: GlobalTransform,
/// The grid position of the entity
pub grid_position: GridCell<P>,
}
/// Marks the entity to use as the floating origin. All other entities will be positioned relative
/// to this entity's [`GridCell`].
#[derive(Component, Reflect)]
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>(
reference_frames: ReferenceFrames<P>,
mut changed_transform: Query<(Entity, &mut GridCell<P>, &mut Transform), Changed<Transform>>,
) {
changed_transform
.par_iter_mut()
.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()
> frame.maximum_distance_from_origin()
{
let (grid_cell_delta, translation) =
frame.imprecise_translation_to_grid(transform.as_ref().translation);
*grid_pos += grid_cell_delta;
transform.translation = translation;
}
});
}
/// 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), With<Parent>>, // Node entities
Query<(GridTransformReadOnly<P>, &mut GlobalTransform), Without<Parent>>, // Root entities
)>,
) {
// Update the GlobalTransform of GridCell entities at the root of the hierarchy
entities
.p1()
.par_iter_mut()
.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(feature = "debug")]
pub mod debug;
#[cfg(test)]
mod tests {
use super::*;
mod tests;
#[test]
fn changing_floating_origin_updates_global_transform() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i32>::default());
use bevy::prelude::*;
let first = app
.world
.spawn((
TransformBundle::from_transform(Transform::from_translation(Vec3::new(
150.0, 0.0, 0.0,
))),
GridCell::<i32>::new(5, 0, 0),
FloatingOrigin,
))
.id();
let second = app
.world
.spawn((
TransformBundle::from_transform(Transform::from_translation(Vec3::new(
0.0, 0.0, 300.0,
))),
GridCell::<i32>::new(0, -15, 0),
))
.id();
app.update();
app.world.entity_mut(first).remove::<FloatingOrigin>();
app.world.entity_mut(second).insert(FloatingOrigin);
app.update();
let second_global_transform = app.world.get::<GlobalTransform>(second).unwrap();
assert_eq!(
second_global_transform.translation(),
Vec3::new(0.0, 0.0, 300.0)
);
}
#[test]
fn child_global_transforms_are_updated_when_floating_origin_changes() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i32>::default());
let first = app
.world
.spawn((
TransformBundle::from_transform(Transform::from_translation(Vec3::new(
150.0, 0.0, 0.0,
))),
GridCell::<i32>::new(5, 0, 0),
FloatingOrigin,
))
.id();
let second = app
.world
.spawn((
TransformBundle::from_transform(Transform::from_translation(Vec3::new(
0.0, 0.0, 300.0,
))),
GridCell::<i32>::new(0, -15, 0),
))
.with_children(|parent| {
parent.spawn((TransformBundle::from_transform(
Transform::from_translation(Vec3::new(0.0, 0.0, 300.0)),
),));
})
.id();
app.update();
app.world.entity_mut(first).remove::<FloatingOrigin>();
app.world.entity_mut(second).insert(FloatingOrigin);
app.update();
let child = app.world.get::<Children>(second).unwrap()[0];
let child_transform = app.world.get::<GlobalTransform>(child).unwrap();
assert_eq!(child_transform.translation(), Vec3::new(0.0, 0.0, 600.0));
}
}
pub use bundles::{BigReferenceFrameBundle, BigSpaceRootBundle, BigSpatialBundle};
pub use commands::{BigSpaceCommands, ReferenceFrameCommands, SpatialEntityCommands};
pub use floating_origins::{BigSpace, FloatingOrigin};
pub use grid_cell::GridCell;
pub use plugin::{BigSpacePlugin, FloatingOriginSet};
pub use reference_frame::ReferenceFrame;

117
src/plugin.rs Normal file
View File

@ -0,0 +1,117 @@
//! The bevy plugin for big_space.
use bevy::{prelude::*, transform::TransformSystem};
use std::marker::PhantomData;
use crate::{
precision::GridPrecision,
reference_frame::{local_origin::LocalFloatingOrigin, ReferenceFrame},
validation, BigSpace, FloatingOrigin, GridCell,
};
/// Add this plugin to your [`App`] for floating origin functionality.
pub struct BigSpacePlugin<P: GridPrecision> {
phantom: PhantomData<P>,
validate_hierarchies: bool,
}
impl<P: GridPrecision> BigSpacePlugin<P> {
/// Create a big space plugin, and specify whether hierarchy validation should be enabled.
pub fn new(validate_hierarchies: bool) -> Self {
Self {
phantom: PhantomData::<P>,
validate_hierarchies,
}
}
}
impl<P: GridPrecision> Default for BigSpacePlugin<P> {
fn default() -> Self {
#[cfg(debug_assertions)]
let validate_hierarchies = true;
#[cfg(not(debug_assertions))]
let validate_hierarchies = false;
Self {
phantom: Default::default(),
validate_hierarchies,
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum FloatingOriginSet {
RecenterLargeTransforms,
LocalFloatingOrigins,
PropagateHighPrecision,
PropagateLowPrecision,
}
impl<P: GridPrecision + Reflect + FromReflect + TypePath> Plugin for BigSpacePlugin<P> {
fn build(&self, app: &mut App) {
let system_set_config = || {
(
(
GridCell::<P>::recenter_large_transforms,
BigSpace::find_floating_origin,
)
.in_set(FloatingOriginSet::RecenterLargeTransforms),
LocalFloatingOrigin::<P>::compute_all
.in_set(FloatingOriginSet::LocalFloatingOrigins)
.after(FloatingOriginSet::RecenterLargeTransforms),
ReferenceFrame::<P>::propagate_high_precision
.in_set(FloatingOriginSet::PropagateHighPrecision)
.after(FloatingOriginSet::LocalFloatingOrigins),
ReferenceFrame::<P>::propagate_low_precision
.in_set(FloatingOriginSet::PropagateLowPrecision)
.after(FloatingOriginSet::PropagateHighPrecision),
)
.in_set(TransformSystem::TransformPropagate)
};
app.register_type::<Transform>()
.register_type::<GlobalTransform>()
.register_type::<GridCell<P>>()
.register_type::<ReferenceFrame<P>>()
.register_type::<BigSpace>()
.register_type::<FloatingOrigin>()
.add_systems(PostStartup, system_set_config())
.add_systems(PostUpdate, system_set_config())
.add_systems(
PostUpdate,
validation::validate_hierarchy::<validation::SpatialHierarchyRoot<P>>
.before(TransformSystem::TransformPropagate)
.run_if({
let run = self.validate_hierarchies;
move || run
}),
)
// These are the bevy transform propagation systems. Because these start from the root
// of the hierarchy, and BigSpace bundles (at the root) do not contain a Transform,
// these systems will not interact with any high precision entities in big space. These
// systems are added for ecosystem compatibility with bevy, although the rendered
// behavior might look strange if they share a camera with one using the floating
// origin.
//
// This is most useful for bevy_ui, which relies on the transform systems to work, or if
// you want to render a camera that only needs to render a low-precision scene.
.add_systems(
PostStartup,
(
bevy::transform::systems::sync_simple_transforms,
bevy::transform::systems::propagate_transforms,
)
.in_set(TransformSystem::TransformPropagate),
)
.add_systems(
PostUpdate,
(
bevy::transform::systems::sync_simple_transforms,
bevy::transform::systems::propagate_transforms,
)
.in_set(TransformSystem::TransformPropagate),
);
}
}

View File

@ -8,6 +8,36 @@ use bevy::reflect::Reflect;
///
/// Larger grids result in a larger useable volume, at the cost of increased memory usage. In
/// addition, some platforms may be unable to use larger numeric types (e.g. [`i128`]).
///
/// [`big_space`](crate) is generic over a few integer types to allow you to select the grid size
/// you need. Assuming you are using a grid cell edge length of 10,000 meters, and `1.0` == 1 meter,
/// these correspond to a total usable volume of a cube with the following edge lengths:
///
/// - `i8`: 2,560 km = 74% of the diameter of the Moon
/// - `i16`: 655,350 km = 85% of the diameter of the Moon's orbit around Earth
/// - `i32`: 0.0045 light years = ~4 times the width of the solar system
/// - `i64`: 19.5 million light years = ~100 times the width of the milky way galaxy
/// - `i128`: 3.6e+26 light years = ~3.9e+15 times the width of the observable universe
///
/// where `usable_edge_length = 2^(integer_bits) * cell_edge_length`, resulting in a worst case
/// precision of 0.5mm in any of these cases.
///
/// This can also be used for small scales. With a cell edge length of `1e-11`, and using `i128`,
/// there is enough precision to render objects the size of quarks anywhere in the observable
/// universe.
///
/// # Note
///
/// Be sure you are using the same grid index precision everywhere. It might be a good idea to
/// define a type alias!
///
/// ```
/// # use big_space::GridCell;
/// type GalacticGrid = GridCell<i64>;
/// ```
///
/// Additionally, consider using the provided command extensions in [`crate::commands`] to
/// completely eliminate the use of this generic, and prevent many errors.
pub trait GridPrecision:
Default
+ PartialEq

View File

@ -1,220 +0,0 @@
//! Propagates transforms through the entity hierarchy.
//!
//! This is a modified version of Bevy's own transform propagation system.
use crate::{
precision::GridPrecision,
reference_frame::{ReferenceFrame, RootReferenceFrame},
GridCell,
};
use bevy::prelude::*;
/// 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>(
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>),
>,
root_frame: Res<RootReferenceFrame<P>>,
mut root_frame_gridless_query: Query<
(
Entity,
&Children,
&Transform,
&mut GlobalTransform,
Has<IgnoreFloatingOrigin>,
),
(Without<GridCell<P>>, Without<Parent>),
>,
transform_query: Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
(
With<Parent>,
Without<GridCell<P>>,
Without<ReferenceFrame<P>>,
),
>,
parent_query: Query<(Entity, Ref<Parent>)>,
) {
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.
// - 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.
unsafe {
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 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.
///
/// # 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<P: GridPrecision>(
parent: &GlobalTransform,
transform_query: &Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
(
With<Parent>,
Without<GridCell<P>>,
Without<ReferenceFrame<P>>,
),
>,
parent_query: &Query<(Entity, Ref<Parent>)>,
entity: Entity,
) {
let (global_matrix, children) = {
let Ok((transform, mut global_transform, children)) =
// SAFETY: This call cannot create aliased mutable references.
// - The top level iteration parallelizes on the roots of the hierarchy.
// - The caller ensures that each child has one and only one unique parent throughout the entire
// hierarchy.
//
// For example, consider the following malformed hierarchy:
//
// A
// / \
// B C
// \ /
// D
//
// D has two parents, B and C. If the propagation passes through C, but the Parent component on D points to B,
// the above check will panic as the origin parent does match the recorded parent.
//
// Also consider the following case, where A and B are roots:
//
// A B
// \ /
// C D
// \ /
// E
//
// Even if these A and B start two separate tasks running in parallel, one of them will panic before attempting
// to mutably access E.
(unsafe { transform_query.get_unchecked(entity) }) else {
return;
};
*global_transform = parent.mul_transform(*transform);
(*global_transform, children)
};
let Some(children) = children else { return };
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: The caller guarantees that `transform_query` will not be fetched
// for any descendants of `entity`, so it is safe to call `propagate_recursive` for each child.
//
// 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);
}
}
}

View File

@ -1,5 +1,6 @@
//! 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.
//! frames, and used to compute the floating origin's position relative to each reference frame. See
//! [`LocalFloatingOrigin`].
use bevy::{
ecs::{
@ -15,11 +16,12 @@ use bevy::{
transform::prelude::*,
};
use super::{ReferenceFrame, RootReferenceFrame};
use crate::{FloatingOrigin, GridCell, GridPrecision};
pub use inner::LocalFloatingOrigin;
use crate::{precision::GridPrecision, BigSpace, GridCell};
use super::ReferenceFrame;
/// A module kept private to enforce use of setters and getters within the parent module.
mod inner {
use bevy::{
@ -27,7 +29,7 @@ mod inner {
reflect::prelude::*,
};
use crate::{GridCell, GridPrecision};
use crate::{precision::GridPrecision, GridCell};
/// An isometry that describes the location of the floating origin's grid cell's origin, in the
/// local reference frame.
@ -38,11 +40,11 @@ mod inner {
/// 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!
/// 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.
@ -132,159 +134,164 @@ mod inner {
}
}
#[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,
fn propagate_origin_to_parent<P: GridPrecision>(
this_frame_entity: Entity,
reference_frames: &mut ReferenceFramesMut<P>,
parent_frame_entity: Entity,
) {
let (this_frame, this_cell, this_transform) = reference_frames.get(this_frame_entity);
let (parent_frame, _parent_cell, _parent_transform) = reference_frames.get(parent_frame_entity);
// 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_reference_frame(parent_frame_entity, |parent_frame, _, _| {
parent_frame.local_floating_origin.set(
parent_origin_cell,
origin_translation_remainder,
origin_rot,
);
});
}
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);
fn propagate_origin_to_child<P: GridPrecision>(
this_frame_entity: Entity,
reference_frames: &mut ReferenceFramesMut<P>,
child_frame_entity: Entity,
) {
let (this_frame, _this_cell, _this_transform) = reference_frames.get(this_frame_entity);
let (child_frame, child_cell, child_transform) = reference_frames.get(child_frame_entity);
// 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()),
// 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_reference_frame(child_frame_entity, |child_frame, _, _| {
child_frame.local_floating_origin.set(
child_origin_cell,
child_origin_translation_float,
origin_child_rotation,
);
// 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).
/// A system param for more easily navigating a hierarchy of reference frames.
#[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>>)>,
children: Query<'w, 's, Read<Children>>,
// position: Query<'w, 's, (Read<GridCell<P>>, Read<Transform>), With<ReferenceFrame<P>>>,
frame_query: Query<'w, 's, (Entity, Read<ReferenceFrame<P>>, Option<Read<Parent>>)>,
}
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 [`ReferenceFrame`] from its `Entity`.
pub fn get(&self, frame_entity: Entity) -> &ReferenceFrame<P> {
self.frame_query
.get(frame_entity)
.map(|(_entity, frame, _parent)| frame)
.unwrap_or_else(|e| {
panic!("Reference frame entity missing ReferenceFrame component.\n\tError: {e}");
})
}
/// Get a handle to this entity's reference frame, if it exists.
/// Get the [`ReferenceFrame`] that `this` `Entity` is a child of, if it exists.
pub fn parent_frame(&self, this: Entity) -> Option<&ReferenceFrame<P>> {
self.parent_frame_entity(this)
.map(|frame_entity| self.get(frame_entity))
}
/// Get the ID of the reference frame that `this` `Entity` is a child of, if it exists.
#[inline]
pub fn get_handle(&self, this: Entity) -> Option<ReferenceFrameHandle> {
pub fn parent_frame_entity(&self, this: Entity) -> Option<Entity> {
match self.parent.get(this).map(|parent| **parent) {
Err(_) => Some(ReferenceFrameHandle::Root),
Err(_) => None,
Ok(parent) => match self.frame_query.contains(parent) {
true => Some(ReferenceFrameHandle::Node(parent)),
true => Some(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))
/// Get handles to all reference frames that are children of this reference frame. Applies a
/// filter to the returned children.
fn child_frames_filtered(
&mut self,
this: Entity,
mut filter: impl FnMut(Entity) -> bool,
) -> Vec<Entity> {
self.children
.get(this)
.iter()
.flat_map(|c| c.iter())
.filter(|entity| filter(**entity))
.filter(|child| self.frame_query.contains(**child))
.copied()
.collect()
}
/// Get IDs to all reference frames that are children of this reference frame.
pub fn child_frames(&mut self, this: Entity) -> Vec<Entity> {
self.child_frames_filtered(this, |_| true)
}
/// Get IDs to all reference frames that are siblings of this reference frame.
pub fn sibling_frames(&mut self, this_entity: Entity) -> Vec<Entity> {
if let Some(parent) = self.parent_frame_entity(this_entity) {
self.child_frames_filtered(parent, |e| e != this_entity)
} else {
Vec::new()
}
}
}
@ -294,18 +301,8 @@ impl<'w, 's, P: GridPrecision> ReferenceFrames<'w, 's, P> {
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>>,
),
>,
position: Query<'w, 's, (Read<GridCell<P>>, Read<Transform>)>,
frame_query: Query<'w, 's, (Entity, Write<ReferenceFrame<P>>, Option<Read<Parent>>)>,
}
impl<'w, 's, P: GridPrecision> ReferenceFramesMut<'w, 's, P> {
@ -314,7 +311,7 @@ impl<'w, 's, P: GridPrecision> ReferenceFramesMut<'w, 's, P> {
///
/// ## Panics
///
/// This will panic if the handle passed in is invalid.
/// This will panic if the entity passed in is invalid.
///
/// ## Why a closure?
///
@ -327,114 +324,82 @@ impl<'w, 's, P: GridPrecision> ReferenceFramesMut<'w, 's, P> {
/// 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>(
pub fn update_reference_frame<T>(
&mut self,
handle: ReferenceFrameHandle,
frame_entity: Entity,
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.
),
}
let (cell, transform) = self.position(frame_entity);
self.frame_query
.get_mut(frame_entity)
.map(|(_entity, mut frame, _parent)| func(frame.as_mut(), &cell, &transform))
.expect("The supplied reference frame handle to node is no longer valid.")
}
/// 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 the reference frame and the position of the reference frame from its `Entity`.
pub fn get(&self, frame_entity: Entity) -> (&ReferenceFrame<P>, GridCell<P>, Transform) {
let (cell, transform) = self.position(frame_entity);
self.frame_query
.get(frame_entity)
.map(|(_entity, frame, _parent)| (frame, cell, transform))
.unwrap_or_else(|e| {
panic!("Reference frame entity {frame_entity:?} missing ReferenceFrame component.\n\tError: {e}");
})
}
/// Get a handle to this entity's reference frame, if it exists.
/// Get the position of this reference frame, including its grid cell and transform, or return
/// defaults if they are missing.
///
/// Needed because the root reference frame should not have a grid cell or transform.
pub fn position(&self, frame_entity: Entity) -> (GridCell<P>, Transform) {
let (cell, transform) = (GridCell::default(), Transform::default());
let (cell, transform) = self.position.get(frame_entity).unwrap_or_else(|_| {
assert!(self.parent.get(frame_entity).is_err(), "Reference frame entity {frame_entity:?} is missing a GridCell and Transform. This is valid only if this is a root reference frame, but this is not.");
(&cell, &transform)
});
(*cell, *transform)
}
/// Get the ID of the reference frame that `this` `Entity` is a child of, if it exists.
#[inline]
fn get_handle(&self, this: Entity) -> Option<ReferenceFrameHandle> {
pub fn parent_frame(&self, this: Entity) -> Option<Entity> {
match self.parent.get(this).map(|parent| **parent) {
Err(_) => Some(ReferenceFrameHandle::Root),
Err(_) => None,
Ok(parent) => match self.frame_query.contains(parent) {
true => Some(ReferenceFrameHandle::Node(parent)),
true => Some(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(
fn child_frames_filtered(
&mut self,
this: ReferenceFrameHandle,
this: Entity,
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(),
}
) -> Vec<Entity> {
self.children
.get(this)
.iter()
.flat_map(|c| c.iter())
.filter(|entity| filter(**entity))
.filter(|child| self.frame_query.contains(**child))
.copied()
.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 IDs to all reference frames that are children of this reference frame.
pub fn child_frames(&mut self, this: Entity) -> Vec<Entity> {
self.child_frames_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(),
/// Get IDs to all reference frames that are siblings of this reference frame.
pub fn sibling_frames(&mut self, this_entity: Entity) -> Vec<Entity> {
if let Some(parent) = self.parent_frame(this_entity) {
self.child_frames_filtered(parent, |e| e != this_entity)
} else {
Vec::new()
}
}
}
@ -446,78 +411,89 @@ impl<P: GridPrecision> LocalFloatingOrigin<P> {
/// 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>>,
pub fn compute_all(
mut reference_frames: ReferenceFramesMut<P>,
mut frame_stack: Local<Vec<ReferenceFrameHandle>>,
mut frame_stack: Local<Vec<Entity>>,
cells: Query<(Entity, &GridCell<P>)>,
roots: Query<(Entity, &BigSpace)>,
parents: Query<&Parent>,
) {
/// 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;
const MAX_REFERENCE_FRAME_DEPTH: usize = 255;
let (origin_entity, origin_cell) = origin
.get_single()
.expect("There can only be one entity with the `FloatingOrigin` component.");
// TODO: because each tree under a root is disjoint, these updates can be done in parallel
// without aliasing. This will require unsafe, just like bevy's own transform propagation.
'outer: for (origin_entity, origin_cell) in roots
.iter() // TODO: If any of these checks fail, log to some diagnostic
.filter_map(|(root_entity, root)| root.validate_floating_origin(root_entity, &parents))
.filter_map(|origin| cells.get(origin).ok())
{
let Some(mut this_frame) = reference_frames.parent_frame(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` component.");
continue;
};
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_reference_frame(this_frame, |frame, _, _| {
frame
.local_floating_origin
.set(*origin_cell, Vec3::ZERO, DQuat::IDENTITY);
});
// 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);
// 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_frame(this_frame) {
propagate_origin_to_parent(this_frame, &mut reference_frames, parent_frame);
for sibling_frame in reference_frames.sibling_frames(this_frame) {
// The siblings of this frame are also the children of the parent frame.
propagate_origin_to_child(
parent_frame,
&mut reference_frames,
sibling_frame,
);
frame_stack.push(sibling_frame); // We'll recurse through children next
}
}
// 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.child_frames(this_frame) {
propagate_origin_to_child(this_frame, &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_frame(this_frame) {
Some(parent_frame) => this_frame = parent_frame,
None => continue 'outer, // We have reached the root of the tree, and can exit.
}
}
// 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.")
}
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 bevy::{ecs::system::SystemState, math::DVec3, prelude::*};
use super::*;
use crate::*;
@ -526,7 +502,7 @@ mod tests {
#[test]
fn frame_hierarchy_getters() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i32>::default());
app.add_plugins(BigSpacePlugin::<i32>::default());
let frame_bundle = (
Transform::default(),
@ -537,54 +513,59 @@ mod tests {
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();
let root = app.world.spawn(frame_bundle.clone()).id();
app.world.entity_mut(root).push_children(&[parent]);
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);
let mut ref_frames = 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));
let result = ref_frames.child_frames(root);
assert_eq!(result, vec![parent]);
let result = ref_frames.child_frames(parent);
assert!(result.contains(&child_1));
assert!(result.contains(&child_2));
let result = ref_frames.child_frames(child_1);
assert_eq!(result, Vec::new());
// Parent
let result = ref_frame.parent(ReferenceFrameHandle::Root);
let result = ref_frames.parent_frame(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)));
let result = ref_frames.parent_frame(parent);
assert_eq!(result, Some(root));
let result = ref_frames.parent_frame(child_1);
assert_eq!(result, Some(parent));
// Siblings
let result = ref_frame.siblings(ReferenceFrameHandle::Root);
let result = ref_frames.sibling_frames(root);
assert_eq!(result, vec![]);
let result = ref_frame.siblings(ReferenceFrameHandle::Node(parent));
let result = ref_frames.sibling_frames(parent);
assert_eq!(result, vec![]);
let result = ref_frame.siblings(ReferenceFrameHandle::Node(child_1));
assert_eq!(result, vec![ReferenceFrameHandle::Node(child_2)]);
let result = ref_frames.sibling_frames(child_1);
assert_eq!(result, vec![child_2]);
}
#[test]
fn child_propagation() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i32>::default());
app.add_plugins(BigSpacePlugin::<i32>::default());
let root = ReferenceFrameHandle::Root;
app.insert_resource(RootReferenceFrame(ReferenceFrame {
let root_frame = 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 root = app
.world
.spawn((Transform::default(), GridCell::<i32>::default(), root_frame))
.id();
let child = app
.world
@ -595,13 +576,14 @@ mod tests {
ReferenceFrame::<i32>::default(),
))
.id();
let child = ReferenceFrameHandle::Node(child);
app.world.entity_mut(root).push_children(&[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);
propagate_origin_to_child(root, &mut reference_frames, child);
let (child_frame, ..) = reference_frames.get(child);
@ -630,9 +612,14 @@ mod tests {
#[test]
fn parent_propagation() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i64>::default());
app.add_plugins(BigSpacePlugin::<i64>::default());
let root = ReferenceFrameHandle::Root;
let frame_bundle = (
Transform::default(),
GridCell::<i64>::default(),
ReferenceFrame::<i64>::default(),
);
let root = app.world.spawn(frame_bundle.clone()).id();
let child = app
.world
@ -650,13 +637,14 @@ mod tests {
},
))
.id();
let child = ReferenceFrameHandle::Node(child);
app.world.entity_mut(root).push_children(&[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);
propagate_origin_to_parent(child, &mut reference_frames, root);
let (root_frame, ..) = reference_frames.get(root);
@ -685,18 +673,23 @@ mod tests {
#[test]
fn origin_transform() {
let mut app = App::new();
app.add_plugins(FloatingOriginPlugin::<i32>::default());
app.add_plugins(BigSpacePlugin::<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 root = app
.world
.spawn((
Transform::default(),
GridCell::<i32>::default(),
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()
},
))
.id();
let child = app
.world
@ -708,12 +701,13 @@ mod tests {
ReferenceFrame::<i32>::default(),
))
.id();
let child = ReferenceFrameHandle::Node(child);
app.world.entity_mut(root).push_children(&[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);
propagate_origin_to_child(root, &mut reference_frames, child);
let (child_frame, ..) = reference_frames.get(child);
let child_local_point = DVec3::new(5.0, 5.0, 0.0);

View File

@ -3,37 +3,32 @@
//! orbiting a star.
use bevy::{
ecs::{component::Component, system::Resource},
ecs::prelude::*,
math::{Affine3A, DAffine3, DVec3, Vec3},
prelude::{Deref, DerefMut},
reflect::Reflect,
transform::components::{GlobalTransform, Transform},
transform::prelude::*,
};
use crate::{GridCell, GridPrecision};
use crate::{precision::GridPrecision, GridCell};
use self::local_origin::LocalFloatingOrigin;
pub mod local_origin;
pub mod propagation;
/// 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`].
/// A component that defines a reference frame for children of this entity with [`GridCell`]s. All
/// entities with a [`GridCell`] must be children of an entity with a [`ReferenceFrame`]. The
/// reference frame *defines* the grid that the `GridCell` indexes into.
///
/// ## 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.
/// velocities. All entities in the same reference frame will move together, like standard transform
/// propagation, but with much more precision. Entities in the same reference frame as the
/// [`crate::FloatingOrigin`] will be rendered with the most precision. Transforms are propagated
/// starting from the floating origin, ensuring that references frames in a similar point in the
/// hierarchy have accumulated the least error. Reference frames are transformed relative to each
/// other using 64 bit float transforms.
///
/// ## Example
///

View File

@ -0,0 +1,181 @@
//! Logic for propagating transforms through the hierarchy of reference frames.
use crate::{precision::GridPrecision, reference_frame::ReferenceFrame, GridCell};
use bevy::prelude::*;
impl<P: GridPrecision> ReferenceFrame<P> {
/// Update the `GlobalTransform` of entities with a [`GridCell`], using the [`ReferenceFrame`]
/// the entity belongs to.
pub fn propagate_high_precision(
reference_frames: Query<&ReferenceFrame<P>>,
mut entities: Query<(&GridCell<P>, &Transform, &Parent, &mut GlobalTransform)>,
) {
// Update the GlobalTransform of GridCell entities that are children of a ReferenceFrame
entities
.par_iter_mut()
.for_each(|(grid, transform, parent, mut global_transform)| {
if let Ok(frame) = reference_frames.get(parent.get()) {
*global_transform = frame.global_transform(grid, 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. This will recursively propagate entities that only have low-precision
/// [`Transform`]s, just like bevy's built in systems.
pub fn propagate_low_precision(
frames: Query<&Children, With<ReferenceFrame<P>>>,
frame_child_query: Query<(Entity, &Children, &GlobalTransform), With<GridCell<P>>>,
transform_query: Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
(
With<Parent>,
Without<GridCell<P>>,
Without<ReferenceFrame<P>>,
),
>,
parent_query: Query<(Entity, Ref<Parent>)>,
) {
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"
);
// Unlike bevy's transform propagation, change detection is much more complex, because
// it is relative to the floating origin, *and* whether entities are moving.
// - If the floating origin changes grid cells, everything needs to update
// - If the floating origin's reference frame moves (translation, rotation), every
// entity outside of the reference frame subtree that the floating origin is in must
// update.
// - All entities or reference frame subtrees that move within the same frame as the
// floating origin must be updated.
//
// Instead of adding this complexity and computation, is it much simpler to update
// everything every frame.
let changed = true;
// 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.
// - 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.
unsafe {
Self::propagate_recursive(
&global_transform,
&transform_query,
&parent_query,
child,
changed,
);
}
}
};
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)))
});
}
/// 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.
///
/// # 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(
parent: &GlobalTransform,
transform_query: &Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
(
With<Parent>,
Without<GridCell<P>>, // ***ADDED*** Only recurse low-precision entities
Without<ReferenceFrame<P>>, // ***ADDED*** Only recurse low-precision entities
),
>,
parent_query: &Query<(Entity, Ref<Parent>)>,
entity: Entity,
mut changed: bool,
) {
let (global_matrix, children) = {
let Ok((transform, mut global_transform, children)) =
// SAFETY: This call cannot create aliased mutable references.
// - The top level iteration parallelizes on the roots of the hierarchy.
// - The caller ensures that each child has one and only one unique parent throughout the entire
// hierarchy.
//
// For example, consider the following malformed hierarchy:
//
// A
// / \
// B C
// \ /
// D
//
// D has two parents, B and C. If the propagation passes through C, but the Parent component on D points to B,
// the above check will panic as the origin parent does match the recorded parent.
//
// Also consider the following case, where A and B are roots:
//
// A B
// \ /
// C D
// \ /
// E
//
// Even if these A and B start two separate tasks running in parallel, one of them will panic before attempting
// to mutably access E.
(unsafe { transform_query.get_unchecked(entity) }) else {
return;
};
changed |= transform.is_changed() || global_transform.is_added();
if changed {
*global_transform = parent.mul_transform(*transform);
}
(*global_transform, children)
};
let Some(children) = children else { return };
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: The caller guarantees that `transform_query` will not be fetched
// for any descendants of `entity`, so it is safe to call `propagate_recursive` for each child.
//
// The above assertion ensures that each child has one and only one unique parent throughout the
// entire hierarchy.
unsafe {
Self::propagate_recursive(
&global_matrix,
transform_query,
parent_query,
child,
changed || actual_parent.is_changed(),
);
}
}
}
}

96
src/tests.rs Normal file
View File

@ -0,0 +1,96 @@
use bevy::prelude::*;
use crate::{BigSpacePlugin, BigSpaceRootBundle, FloatingOrigin, GridCell};
#[test]
fn changing_floating_origin_updates_global_transform() {
let mut app = App::new();
app.add_plugins(BigSpacePlugin::<i32>::default());
let first = app
.world
.spawn((
TransformBundle::from_transform(Transform::from_translation(Vec3::new(
150.0, 0.0, 0.0,
))),
GridCell::<i32>::new(5, 0, 0),
FloatingOrigin,
))
.id();
let second = app
.world
.spawn((
TransformBundle::from_transform(Transform::from_translation(Vec3::new(
0.0, 0.0, 300.0,
))),
GridCell::<i32>::new(0, -15, 0),
))
.id();
app.world
.spawn(BigSpaceRootBundle::<i32>::default())
.push_children(&[first, second]);
app.update();
app.world.entity_mut(first).remove::<FloatingOrigin>();
app.world.entity_mut(second).insert(FloatingOrigin);
app.update();
let second_global_transform = app.world.get::<GlobalTransform>(second).unwrap();
assert_eq!(
second_global_transform.translation(),
Vec3::new(0.0, 0.0, 300.0)
);
}
#[test]
fn child_global_transforms_are_updated_when_floating_origin_changes() {
let mut app = App::new();
app.add_plugins(BigSpacePlugin::<i32>::default());
let first = app
.world
.spawn((
TransformBundle::from_transform(Transform::from_translation(Vec3::new(
150.0, 0.0, 0.0,
))),
GridCell::<i32>::new(5, 0, 0),
FloatingOrigin,
))
.id();
let second = app
.world
.spawn((
TransformBundle::from_transform(Transform::from_translation(Vec3::new(
0.0, 0.0, 300.0,
))),
GridCell::<i32>::new(0, -15, 0),
))
.with_children(|parent| {
parent.spawn((TransformBundle::from_transform(
Transform::from_translation(Vec3::new(0.0, 0.0, 300.0)),
),));
})
.id();
app.world
.spawn(BigSpaceRootBundle::<i32>::default())
.push_children(&[first, second]);
app.update();
app.world.entity_mut(first).remove::<FloatingOrigin>();
app.world.entity_mut(second).insert(FloatingOrigin);
app.update();
let child = app.world.get::<Children>(second).unwrap()[0];
let child_transform = app.world.get::<GlobalTransform>(child).unwrap();
assert_eq!(child_transform.translation(), Vec3::new(0.0, 0.0, 600.0));
}

296
src/validation.rs Normal file
View File

@ -0,0 +1,296 @@
//! Tools for validating high-precision transform hierarchies
use std::marker::PhantomData;
use bevy::prelude::*;
use bevy::utils::HashMap;
use crate::{
precision::GridPrecision, reference_frame::ReferenceFrame, BigSpace, FloatingOrigin, GridCell,
};
struct ValidationStackEntry {
parent_node: Box<dyn ValidHierarchyNode>,
children: Vec<Entity>,
}
#[derive(Default, Resource)]
struct ValidatorCaches {
query_state_cache: HashMap<&'static str, QueryState<(Entity, Option<&'static Children>)>>,
validator_cache: HashMap<&'static str, Vec<Box<dyn ValidHierarchyNode>>>,
root_query: Option<QueryState<Entity, Without<Parent>>>,
stack: Vec<ValidationStackEntry>,
}
/// Validate the entity hierarchy and report errors.
pub fn validate_hierarchy<V: 'static + ValidHierarchyNode + Default>(world: &mut World) {
world.init_resource::<ValidatorCaches>();
let mut caches = world.remove_resource::<ValidatorCaches>().unwrap();
let root_entities = caches
.root_query
.get_or_insert(world.query_filtered::<Entity, Without<Parent>>())
.iter(world)
.collect();
caches.stack.push(ValidationStackEntry {
parent_node: Box::<V>::default(),
children: root_entities,
});
while let Some(stack_entry) = caches.stack.pop() {
let mut validators_and_queries = caches
.validator_cache
.entry(stack_entry.parent_node.name())
.or_insert_with(|| stack_entry.parent_node.allowed_child_nodes())
.iter()
.map(|validator| {
let query = caches
.query_state_cache
.remove(validator.name())
.unwrap_or_else(|| {
let mut query_builder = QueryBuilder::new(world);
validator.match_self(&mut query_builder);
query_builder.build()
});
(validator, query)
})
.collect::<Vec<_>>();
for entity in stack_entry.children.iter() {
let query_result = validators_and_queries
.iter_mut()
.find_map(|(validator, query)| {
query.get(world, *entity).ok().map(|res| (validator, res.1))
});
match query_result {
Some((validator, Some(children))) => {
caches.stack.push(ValidationStackEntry {
parent_node: validator.clone(),
children: children.to_vec(),
});
}
Some(_) => (), // Matched, but no children to push on the stack
None => {
let mut possibilities = String::new();
stack_entry
.parent_node
.allowed_child_nodes()
.iter()
.for_each(|v| {
possibilities.push('\t');
possibilities.push('\t');
possibilities.push_str(v.name());
possibilities.push('\n');
});
let mut inspect = String::new();
world.inspect_entity(*entity).iter().for_each(|info| {
inspect.push('\t');
inspect.push('\t');
inspect.push_str(info.name());
inspect.push('\n');
});
error!("big_space hierarchy validation error:\n\tEntity {:#?} is a child of the node {:#?}, but the entity does not match its parent's validation criteria.\n\tBecause it is a child of a {:#?}, the entity must be one of the following kinds of nodes:\n{}\tHowever, the entity has the following components, which does not match any of the above allowed archetypes:\n{}\tCommon errors include:\n\t - Using mismatched GridPrecisions, like GridCell<i32> and GridCell<i64>\n\t - Spawning an entity with a GridCell as a child of an entity without a ReferenceFrame.\n\tIf possible, use commands.spawn_big_space(), which prevents these errors, instead of manually assembling a hierarchy.\n\tSee {} for details.", entity, stack_entry.parent_node.name(), stack_entry.parent_node.name(), possibilities, inspect, file!());
}
}
}
for (validator, query) in validators_and_queries.drain(..) {
caches.query_state_cache.insert(validator.name(), query);
}
}
world.insert_resource(caches);
}
/// Defines a valid node in the hierarchy: what components it must have, must not have, and what
/// kinds of nodes its children can be. This can be used recursively to validate an entire entity
/// hierarchy by starting from the root.
pub trait ValidHierarchyNode: sealed::CloneHierarchy + Send + Sync {
/// Add filters to a query to check if entities match this type of node
fn match_self(&self, query: &mut QueryBuilder<(Entity, Option<&Children>)>);
/// The types of nodes that can be children of this node.
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>>;
/// A unique identifier of this type
fn name(&self) -> &'static str {
std::any::type_name::<Self>()
}
}
pub(super) mod sealed {
use super::ValidHierarchyNode;
pub trait CloneHierarchy {
fn clone_box(&self) -> Box<dyn ValidHierarchyNode>;
}
impl<T: ?Sized> CloneHierarchy for T
where
T: 'static + ValidHierarchyNode + Clone,
{
fn clone_box(&self) -> Box<dyn ValidHierarchyNode> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn ValidHierarchyNode> {
fn clone(&self) -> Self {
self.clone_box()
}
}
}
/// The root hierarchy validation struct, used as a generic parameter in [`crate::validation`].
#[derive(Default, Clone)]
pub struct SpatialHierarchyRoot<P: GridPrecision>(PhantomData<P>);
impl<P: GridPrecision> ValidHierarchyNode for SpatialHierarchyRoot<P> {
fn match_self(&self, _: &mut QueryBuilder<(Entity, Option<&Children>)>) {}
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>> {
vec![
Box::<RootFrame<P>>::default(),
Box::<RootSpatialLowPrecision<P>>::default(),
Box::<AnyNonSpatial<P>>::default(),
]
}
}
#[derive(Default, Clone)]
struct AnyNonSpatial<P: GridPrecision>(PhantomData<P>);
impl<P: GridPrecision> ValidHierarchyNode for AnyNonSpatial<P> {
fn match_self(&self, query: &mut QueryBuilder<(Entity, Option<&Children>)>) {
query
.without::<GridCell<P>>()
.without::<Transform>()
.without::<GlobalTransform>()
.without::<BigSpace>()
.without::<ReferenceFrame<P>>()
.without::<FloatingOrigin>();
}
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>> {
vec![Box::<AnyNonSpatial<P>>::default()]
}
}
#[derive(Default, Clone)]
struct RootFrame<P: GridPrecision>(PhantomData<P>);
impl<P: GridPrecision> ValidHierarchyNode for RootFrame<P> {
fn match_self(&self, query: &mut QueryBuilder<(Entity, Option<&Children>)>) {
query
.with::<BigSpace>()
.with::<ReferenceFrame<P>>()
.without::<GridCell<P>>()
.without::<Transform>()
.without::<GlobalTransform>()
.without::<Parent>()
.without::<FloatingOrigin>();
}
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>> {
vec![
Box::<ChildFrame<P>>::default(),
Box::<ChildSpatialLowPrecision<P>>::default(),
Box::<ChildSpatialHighPrecision<P>>::default(),
Box::<AnyNonSpatial<P>>::default(),
]
}
}
#[derive(Default, Clone)]
struct RootSpatialLowPrecision<P: GridPrecision>(PhantomData<P>);
impl<P: GridPrecision> ValidHierarchyNode for RootSpatialLowPrecision<P> {
fn match_self(&self, query: &mut QueryBuilder<(Entity, Option<&Children>)>) {
query
.with::<Transform>()
.with::<GlobalTransform>()
.without::<GridCell<P>>()
.without::<BigSpace>()
.without::<ReferenceFrame<P>>()
.without::<Parent>()
.without::<FloatingOrigin>();
}
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>> {
vec![
Box::<ChildSpatialLowPrecision<P>>::default(),
Box::<AnyNonSpatial<P>>::default(),
]
}
}
#[derive(Default, Clone)]
struct ChildFrame<P: GridPrecision>(PhantomData<P>);
impl<P: GridPrecision> ValidHierarchyNode for ChildFrame<P> {
fn match_self(&self, query: &mut QueryBuilder<(Entity, Option<&Children>)>) {
query
.with::<ReferenceFrame<P>>()
.with::<GridCell<P>>()
.with::<Transform>()
.with::<GlobalTransform>()
.with::<Parent>()
.without::<BigSpace>();
}
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>> {
vec![
Box::<ChildFrame<P>>::default(),
Box::<ChildSpatialLowPrecision<P>>::default(),
Box::<ChildSpatialHighPrecision<P>>::default(),
Box::<AnyNonSpatial<P>>::default(),
]
}
}
#[derive(Default, Clone)]
struct ChildSpatialLowPrecision<P: GridPrecision>(PhantomData<P>);
impl<P: GridPrecision> ValidHierarchyNode for ChildSpatialLowPrecision<P> {
fn match_self(&self, query: &mut QueryBuilder<(Entity, Option<&Children>)>) {
query
.with::<Transform>()
.with::<GlobalTransform>()
.with::<Parent>()
.without::<GridCell<P>>()
.without::<BigSpace>()
.without::<ReferenceFrame<P>>()
.without::<FloatingOrigin>();
}
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>> {
vec![
Box::<ChildSpatialLowPrecision<P>>::default(),
Box::<AnyNonSpatial<P>>::default(),
]
}
}
#[derive(Default, Clone)]
struct ChildSpatialHighPrecision<P: GridPrecision>(PhantomData<P>);
impl<P: GridPrecision> ValidHierarchyNode for ChildSpatialHighPrecision<P> {
fn match_self(&self, query: &mut QueryBuilder<(Entity, Option<&Children>)>) {
query
.with::<GridCell<P>>()
.with::<Transform>()
.with::<GlobalTransform>()
.with::<Parent>()
.without::<BigSpace>()
.without::<ReferenceFrame<P>>();
}
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>> {
vec![
Box::<ChildSpatialLowPrecision<P>>::default(),
Box::<AnyNonSpatial<P>>::default(),
]
}
}