initial commit

This commit is contained in:
Aevyrie Roessler 2023-01-06 08:02:59 -08:00
commit 9f3084474d
No known key found for this signature in database
GPG Key ID: F975B68AD0BCCF40
11 changed files with 1443 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/target
/Cargo.lock
/.cargo
/.vscode
/.idea

23
Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "big_space"
version = "0.1.0"
edition = "2021"
description = "A floating origin plugin for bevy"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy = { version = "0.9", default_features = false }
bevy_rapier3d = { version = "0.19", optional = true }
bevy_polyline = "0.4"
[dev-dependencies]
bevy = { version = "0.9", default_features = false, features = [
"bevy_render",
"bevy_winit",
"x11",
] }
[features]
rapier = ["bevy_rapier3d"]

176
LICENSE-APACHE Normal file
View File

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

19
LICENSE-MIT Normal file
View File

@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

34
README.md Normal file
View File

@ -0,0 +1,34 @@
<div align="center">
# Big Space
[![crates.io](https://img.shields.io/crates/v/big_space)](https://crates.io/crates/big_space)
[![docs.rs](https://docs.rs/big_space/badge.svg)](https://docs.rs/big_space)
[![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-main-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking)
A floating origin plugin for [Bevy](https://github.com/bevyengine/bevy).
</div>
## Features
Lots of space to play in.
# Bevy Version Support
I intend to track the `main` branch of Bevy. PRs supporting this are welcome!
| bevy | big_space |
| ---- | --------- |
| 0.9 | 0.1 |
# License
This project is dual licensed:
* MIT License ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
## Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Binary file not shown.

135
examples/demo.rs Normal file
View File

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

163
examples/error.rs Normal file
View File

@ -0,0 +1,163 @@
//! This example demonstrates what floating point error in rendering looks like. You can press
//! spacebar to smoothly switch between enabling and disabling the floating origin.
//!
//! Instead of disabling the plugin outright, this example simply moves the floating origin
//! independently from the camera, which is equivalent to what would happen when moving far from the
//! origin when not using this plugin.
use bevy::prelude::*;
use big_space::{FloatingOrigin, FloatingSpatialBundle, GridCell};
fn main() {
App::new()
.add_plugins(DefaultPlugins.build().disable::<TransformPlugin>())
.add_plugin(big_space::FloatingOriginPlugin::<i64>::default())
.add_startup_system(setup_scene)
.add_startup_system(setup_ui)
.add_system(rotator_system)
.add_system(toggle_plugin)
.run()
}
/// You can put things really, really far away from the origin. The distance we use here is actually
/// quite small, because we want the cubes to still be visible when the floating origin is far from
/// the camera. If you go much further than this, the cubes will simply disappear in a *POOF* of
/// floating point error.
///
/// This plugin can function much further from the origin without any issues. Try setting this to:
/// 10_000_000_000_000_000_000_000_000_000_000_000_000
const DISTANCE: f32 = 20_000_000.0;
/// 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<Input<KeyCode>>,
mut text: Query<&mut Text>,
mut state: Local<bool>,
mut floating_origin: Query<&mut GridCell<i64>, With<FloatingOrigin>>,
) {
if input.just_pressed(KeyCode::Space) {
*state = !*state;
}
let mut cell = floating_origin.single_mut();
let cell_max = (DISTANCE / 10_000f32) as i64;
let i = cell_max / 200;
let msg = if *state {
if 0 <= cell.x - i {
cell.x = 0.max(cell.x - i);
cell.y = 0.max(cell.y - i);
cell.z = 0.max(cell.z - i);
"Disabling..."
} else {
"Floating Origin Disabled"
}
} else {
if cell_max >= cell.x + i {
cell.x = i64::min(cell_max, cell.x + i);
cell.y = i64::min(cell_max, cell.y + i);
cell.z = i64::min(cell_max, cell.z + i);
"Enabling..."
} else {
"Floating Origin Enabled"
}
};
let dist = (cell_max - cell.x) * 10_000;
text.single_mut().sections[0].value =
format!("Press Spacebar to toggle: {msg}\nCamera distance to floating origin: {dist}")
}
#[derive(Component)]
struct Rotator;
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Rotator>>) {
for mut transform in &mut query {
transform.rotate_x(3.0 * time.delta_seconds());
}
}
fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/FiraMono-Medium.ttf");
commands.spawn(TextBundle {
style: Style {
align_self: AlignSelf::FlexStart,
flex_direction: FlexDirection::Column,
..Default::default()
},
text: Text {
sections: vec![TextSection {
value: "hello: ".to_string(),
style: TextStyle {
font: font.clone(),
font_size: 30.0,
color: Color::WHITE,
},
}],
..Default::default()
},
..Default::default()
});
}
/// set up a simple scene with a "parent" cube and a "child" cube
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 2.0 }));
let cube_material_handle = materials.add(StandardMaterial {
base_color: Color::rgb(0.8, 0.7, 0.6),
..default()
});
// 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((
FloatingSpatialBundle::<i64> {
transform: Transform::from_translation(Vec3::splat(DISTANCE)),
..default()
},
FloatingOrigin,
));
// parent cube
commands
.spawn(PbrBundle {
mesh: cube_handle.clone(),
material: cube_material_handle.clone(),
transform: Transform::from_translation(Vec3::splat(DISTANCE)),
..default()
})
.insert(GridCell::<i64>::default())
.insert(Rotator)
.with_children(|parent| {
// child cube
parent.spawn(PbrBundle {
mesh: cube_handle,
material: cube_material_handle,
transform: Transform::from_xyz(0.0, 0.0, 3.0),
..default()
});
});
// light
commands
.spawn(PointLightBundle {
transform: Transform::from_xyz(DISTANCE + 4.0, DISTANCE - 10.0, DISTANCE - 4.0),
..default()
})
.insert(GridCell::<i64>::default());
// camera
commands
.spawn(Camera3dBundle {
transform: Transform::from_xyz(DISTANCE + 8.0, DISTANCE - 8.0, DISTANCE)
.looking_at(Vec3::splat(DISTANCE), Vec3::Y),
..default()
})
.insert(GridCell::<i64>::default());
}

186
src/debug.rs Normal file
View File

@ -0,0 +1,186 @@
//! Contains tools for debugging the floating origin.
use std::marker::PhantomData;
use bevy::{prelude::*, utils::HashMap};
use bevy_polyline::prelude::*;
use crate::{precision::GridPrecision, FloatingOrigin, FloatingOriginSettings, GridCell};
/// This plugin will render the bounds of occupied grid cells.
#[derive(Default)]
pub struct FloatingOriginDebugPlugin<P: GridPrecision>(PhantomData<P>);
impl<P: GridPrecision> Plugin for FloatingOriginDebugPlugin<P> {
fn build(&self, app: &mut App) {
app.add_plugin(bevy_polyline::PolylinePlugin)
.add_system_to_stage(CoreStage::Update, build_cube)
.add_system_to_stage(
CoreStage::PostUpdate,
update_debug_bounds::<P>
.after(crate::recenter_transform_on_grid::<P>)
.before(crate::update_global_from_grid::<P>),
);
}
}
/// Marks entities that are used to render grid cell bounds.
#[derive(Component, Reflect)]
pub struct DebugBounds;
/// A resource that holds the handles to use for the debug bound polylines.
#[derive(Resource, Reflect)]
pub struct CubePolyline {
polyline: Handle<Polyline>,
material: Handle<PolylineMaterial>,
origin_matl: Handle<PolylineMaterial>,
}
/// Update the rendered debug bounds to only highlight occupied [`GridCell`]s. [`DebugBounds`] are
/// spawned or hidden as needed.
pub fn update_debug_bounds<P: GridPrecision>(
mut commands: Commands,
cube_polyline: Res<CubePolyline>,
occupied_cells: Query<(&GridCell<P>, Option<&FloatingOrigin>), Without<DebugBounds>>,
mut debug_bounds: Query<
(
&mut GridCell<P>,
&mut Handle<Polyline>,
&mut Handle<PolylineMaterial>,
&mut Visibility,
),
With<DebugBounds>,
>,
) {
let mut occupied_cells = HashMap::from_iter(occupied_cells.iter()).into_iter();
for (mut cell, mut polyline, mut matl, mut visibility) in &mut debug_bounds {
if cube_polyline.is_changed() {
*polyline = cube_polyline.polyline.clone();
}
if let Some((occupied_cell, has_origin)) = occupied_cells.next() {
visibility.is_visible = true;
*cell = *occupied_cell;
if has_origin.is_some() {
*matl = cube_polyline.origin_matl.clone();
} else {
*matl = cube_polyline.material.clone();
}
} else {
// If there are more debug bounds than occupied cells, hide the extras.
visibility.is_visible = false;
}
}
// If there are still occupied cells but no more debug bounds, we need to spawn more.
for (occupied_cell, has_origin) in occupied_cells {
let material = if has_origin.is_some() {
cube_polyline.origin_matl.clone()
} else {
cube_polyline.material.clone()
};
commands.spawn((
SpatialBundle::default(),
cube_polyline.polyline.clone(),
material,
occupied_cell.to_owned(),
DebugBounds,
));
}
}
/// Construct a polyline to match the [`FloatingOriginSettings`].
pub fn build_cube(
settings: Res<FloatingOriginSettings>,
mut commands: Commands,
mut polyline_materials: ResMut<Assets<PolylineMaterial>>,
mut polylines: ResMut<Assets<Polyline>>,
) {
if !settings.is_changed() {
return;
}
let s = settings.grid_edge_length / 2.001;
/*
(2)-----(3) Y
| \ | \ |
| (1)-----(0) MAX o---X
| | | | \
MIN (6)--|--(7) | Z
\ | \ |
(5)-----(4)
*/
let indices = [
0, 1, 1, 2, 2, 3, 3, 0, // Top ring
4, 5, 5, 6, 6, 7, 7, 4, // Bottom ring
0, 4, 8, 1, 5, 8, 2, 6, 8, 3, 7, // Verticals (8's are NaNs)
];
let vertices = [
Vec3::new(s, s, s),
Vec3::new(-s, s, s),
Vec3::new(-s, s, -s),
Vec3::new(s, s, -s),
Vec3::new(s, -s, s),
Vec3::new(-s, -s, s),
Vec3::new(-s, -s, -s),
Vec3::new(s, -s, -s),
Vec3::NAN,
];
let vertices = [
vertices[indices[0]],
vertices[indices[1]],
vertices[indices[2]],
vertices[indices[3]],
vertices[indices[4]],
vertices[indices[5]],
vertices[indices[6]],
vertices[indices[7]],
vertices[indices[8]],
vertices[indices[9]],
vertices[indices[10]],
vertices[indices[11]],
vertices[indices[12]],
vertices[indices[13]],
vertices[indices[14]],
vertices[indices[15]],
vertices[indices[16]],
vertices[indices[17]],
vertices[indices[18]],
vertices[indices[19]],
vertices[indices[20]],
vertices[indices[21]],
vertices[indices[22]],
vertices[indices[23]],
vertices[indices[24]],
vertices[indices[25]],
vertices[indices[26]],
];
let polyline = polylines.add(Polyline {
vertices: vertices.into(),
..Default::default()
});
let material = polyline_materials.add(PolylineMaterial {
width: 1.5,
color: Color::rgb(2.0, 0.0, 0.0),
perspective: false,
..Default::default()
});
let origin_matl = polyline_materials.add(PolylineMaterial {
width: 1.5,
color: Color::rgb(0.0, 0.0, 2.0),
perspective: false,
..Default::default()
});
commands.insert_resource(CubePolyline {
polyline,
material,
origin_matl,
})
}

508
src/lib.rs Normal file
View File

@ -0,0 +1,508 @@
//! This [`bevy`] plugin makes it easy to build high-precision worlds that exceed the size of the
//! observable universe, with no added dependencies, while remaining largely compatible with the
//! rest of the Bevy ecosystem.
//!
//! ## Problem
//!
//! Objects far from the origin suffer from reduced precision, causing rendered meshes to jitter and
//! jiggle, and transformation calculations to encounter catastrophic cancellation.
//!
//! ## Solution
//!
//! While using the [`FloatingOriginPlugin`], entities are placed into a large fixed precision grid,
//! and their [`Transform`]s are recomputed to be relative to their current grid cell. If an entity
//! moves into a neighboring cell, its transform will be recomputed. This prevents `Transforms` from
//! ever becoming larger than a single grid cell.
//!
//! # 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:
//!
//! **Avoid setting position absolutely, instead prefer applying a relative delta**
//!
//! Instead of:
//!
//! ```no_run
//! transform.translation = a_huge_imprecise_position;
//! ```
//!
//! do:
//!
//! ```no_run
//! let delta = new_pos - old_pos;
//! transform.translation += delta;
//! ```
//!
//! ## Absolute Position
//!
//! If you are updating the position of an entity with absolute positions, and the position exceeds
//! the bounds of the entity's grid cell, the floating origin plugin will recenter that entity into
//! its new cell. Every time you update that entity, you will be fighting with the floating origin
//! plugin as it constantly recenters your entity. This can especially cause problems with camera
//! controllers which may not expect the large discontinuity in position as an entity moves between
//! cells.
//!
//! The other reason to avoid this is you will likely run into precision issues! This plugin exists
//! because single precision is limited, and the larger the position coordinates get, the less
//! precision you have.
//!
//! However, if you have something that cannot accumulate error, like the orbit of a planet, you can
//! instead do the orbital calculation (position as a function of time) to compute the absolute
//! position of the planet, then directly compute the [`GridCell`] and [`Transform`] of that entity
//! using [`FloatingOriginSettings::translation_to_grid`]. If the star this planet is orbiting
//! around is also moving through space, note that you can add/subtract grid cells. This means you
//! can do each calculation in the reference frame of the moving body, and sum up the computed
//! translations and grid cell offsets to get a more precise result.
#![deny(missing_docs)]
use bevy::{math::DVec3, prelude::*, transform::TransformSystem};
use std::marker::PhantomData;
pub mod debug;
pub mod precision;
use precision::*;
/// Add this plugin to your [`App`] to for floating origin functionality.
#[derive(Default)]
pub struct FloatingOriginPlugin<P: GridPrecision> {
/// Initial floating origin settings.
pub settings: FloatingOriginSettings,
phantom: PhantomData<P>,
}
impl<P: GridPrecision> FloatingOriginPlugin<P> {
/// # `switching_threshold`:
///
/// 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 fn new(grid_edge_length: f32, switching_threshold: f32) -> Self {
FloatingOriginPlugin {
settings: FloatingOriginSettings::new(grid_edge_length, switching_threshold),
..Default::default()
}
}
}
impl<P: GridPrecision> Plugin for FloatingOriginPlugin<P> {
fn build(&self, app: &mut App) {
app.insert_resource(self.settings.clone())
.register_type::<Transform>()
.register_type::<GlobalTransform>()
.register_type::<GridCell<P>>()
.add_plugin(ValidParentCheckPlugin::<GlobalTransform>::default())
// add transform systems to startup so the first update is "correct"
.add_startup_system_to_stage(
StartupStage::PostStartup,
recenter_transform_on_grid::<P>
.label(TransformSystem::TransformPropagate)
.before(update_global_from_grid::<P>),
)
.add_startup_system_to_stage(
StartupStage::PostStartup,
update_global_from_grid::<P>
.label(TransformSystem::TransformPropagate)
.before(transform_propagate_system::<P>),
)
.add_startup_system_to_stage(
StartupStage::PostStartup,
transform_propagate_system::<P>.label(TransformSystem::TransformPropagate),
)
.add_system_to_stage(
CoreStage::PostUpdate,
recenter_transform_on_grid::<P>
.label(TransformSystem::TransformPropagate)
.before(update_global_from_grid::<P>),
)
.add_system_to_stage(
CoreStage::PostUpdate,
update_global_from_grid::<P>
.label(TransformSystem::TransformPropagate)
.before(transform_propagate_system::<P>),
)
.add_system_to_stage(
CoreStage::PostUpdate,
transform_propagate_system::<P>.label(TransformSystem::TransformPropagate),
);
}
}
/// Configuration settings for the floating origin plugin.
#[derive(Reflect, Clone, Resource)]
pub struct FloatingOriginSettings {
grid_edge_length: f32,
maximum_distance_from_origin: f32,
}
impl FloatingOriginSettings {
fn new(grid_edge_length: f32, switching_threshold: f32) -> Self {
Self {
grid_edge_length,
maximum_distance_from_origin: grid_edge_length / 2.0 + switching_threshold,
}
}
/// Compute the double precision position of an entity's [`Transform`] with respect to the given
/// [`GridCell`].
pub fn grid_position_double<P: GridPrecision>(
&self,
pos: &GridCell<P>,
transform: &Transform,
) -> DVec3 {
DVec3 {
x: pos.x.as_f64() * self.grid_edge_length as f64 + transform.translation.x as f64,
y: pos.y.as_f64() * self.grid_edge_length as f64 + transform.translation.y as f64,
z: pos.z.as_f64() * self.grid_edge_length as f64 + transform.translation.z as f64,
}
}
/// Compute the single precision position of an entity's [`Transform`] with respect to the given
/// [`GridCell`].
pub fn grid_position<P: GridPrecision>(
&self,
pos: &GridCell<P>,
transform: &Transform,
) -> Vec3 {
Vec3 {
x: pos.x.as_f64() as f32 * self.grid_edge_length + transform.translation.x,
y: pos.y.as_f64() as f32 * self.grid_edge_length + transform.translation.y,
z: pos.z.as_f64() as f32 * self.grid_edge_length + transform.translation.z,
}
}
/// Convert a large translation into a small translation relative to a grid cell.
pub fn translation_to_grid<P: GridPrecision>(&self, input: Vec3) -> (GridCell<P>, Vec3) {
let l = self.grid_edge_length;
let Vec3 { x, y, z } = input;
if input.abs().max_element() < self.maximum_distance_from_origin {
return (GridCell::default(), input);
}
let x_r = (x / l).round();
let y_r = (y / l).round();
let z_r = (z / l).round();
let t_x = x - x_r * l;
let t_y = y - y_r * l;
let t_z = z - z_r * l;
(
GridCell {
x: P::from_f32(x_r),
y: P::from_f32(y_r),
z: P::from_f32(z_r),
},
Vec3::new(t_x, t_y, t_z),
)
}
}
impl Default for FloatingOriginSettings {
fn default() -> Self {
Self::new(10_000f32, 100f32)
}
}
/// 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.
pub visibility: Visibility,
/// The computed visibility of the entity.
pub computed: ComputedVisibility,
/// 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>,
}
/// 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 crate::GridCell;
/// type GalacticGrid = GridCell<i64>;
/// ```
///
#[derive(Component, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Reflect)]
#[reflect(Component, Default, PartialEq)]
pub struct GridCell<P: GridPrecision> {
/// The x-index of the cell.
pub x: P,
/// The y-index of the cell.
pub y: P,
/// The z-index of the cell.
pub z: P,
}
impl<P: GridPrecision> GridCell<P> {
/// Construct a new [`GridCell`].
pub fn new(x: P, y: P, z: P) -> Self {
Self { x, y, z }
}
/// The origin [`GridCell`].
pub const ZERO: Self = GridCell {
x: P::ZERO,
y: P::ZERO,
z: P::ZERO,
};
/// A unit value [`GridCell`]. Useful for offsets.
pub const ONE: Self = GridCell {
x: P::ONE,
y: P::ONE,
z: P::ONE,
};
}
impl<P: GridPrecision> std::ops::Add for GridCell<P> {
type Output = GridCell<P>;
fn add(self, rhs: Self) -> Self::Output {
GridCell {
x: self.x.wrapping_add(rhs.x),
y: self.y.wrapping_add(rhs.y),
z: self.z.wrapping_add(rhs.z),
}
}
}
impl<P: GridPrecision> std::ops::Sub for GridCell<P> {
type Output = GridCell<P>;
fn sub(self, rhs: Self) -> Self::Output {
GridCell {
x: self.x.wrapping_sub(rhs.x),
y: self.y.wrapping_sub(rhs.y),
z: self.z.wrapping_sub(rhs.z),
}
}
}
impl<P: GridPrecision> std::ops::Add for &GridCell<P> {
type Output = GridCell<P>;
fn add(self, rhs: Self) -> Self::Output {
(*self).add(*rhs)
}
}
impl<P: GridPrecision> std::ops::Sub for &GridCell<P> {
type Output = GridCell<P>;
fn sub(self, rhs: Self) -> Self::Output {
(*self).sub(*rhs)
}
}
/// 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>(
settings: Res<FloatingOriginSettings>,
mut query: Query<(&mut GridCell<P>, &mut Transform), (Changed<Transform>, Without<Parent>)>,
) {
query.par_for_each_mut(1024, |(mut grid_pos, mut transform)| {
if transform.as_ref().translation.abs().max_element()
> settings.maximum_distance_from_origin
{
let (grid_cell_delta, translation) =
settings.translation_to_grid(transform.as_ref().translation);
*grid_pos = *grid_pos + grid_cell_delta;
transform.translation = translation;
}
});
}
/// Compute the `GlobalTransform` relative to the floating origin.
pub fn update_global_from_grid<P: GridPrecision>(
settings: Res<FloatingOriginSettings>,
origin: Query<(&GridCell<P>, Changed<GridCell<P>>), With<FloatingOrigin>>,
mut entities: ParamSet<(
Query<
(&Transform, &mut GlobalTransform, &GridCell<P>),
Or<(Changed<GridCell<P>>, Changed<Transform>)>,
>,
Query<(&Transform, &mut GlobalTransform, &GridCell<P>)>,
)>,
) {
let (origin_cell, origin_grid_pos_changed) = origin.single();
if origin_grid_pos_changed {
let mut all_entities = entities.p1();
all_entities.par_for_each_mut(1024, |(local, global, entity_cell)| {
update_global_from_cell_local(&settings, entity_cell, origin_cell, local, global);
});
} else {
let mut moved_cell_entities = entities.p0();
moved_cell_entities.par_for_each_mut(1024, |(local, global, entity_cell)| {
update_global_from_cell_local(&settings, entity_cell, origin_cell, local, global);
});
}
}
fn update_global_from_cell_local<P: GridPrecision>(
settings: &FloatingOriginSettings,
entity_cell: &GridCell<P>,
origin_cell: &GridCell<P>,
local: &Transform,
mut global: Mut<GlobalTransform>,
) {
let grid_cell_delta = entity_cell - origin_cell;
*global = local
.clone()
.with_translation(settings.grid_position(&grid_cell_delta, local))
.into();
}
/// Update [`GlobalTransform`] component of entities based on entity hierarchy and
/// [`Transform`] component.
pub fn transform_propagate_system<P: GridPrecision>(
origin_moved: Query<(), (Changed<GridCell<P>>, With<FloatingOrigin>)>,
mut root_query_no_grid: Query<
(
Option<(&Children, Changed<Children>)>,
&Transform,
Changed<Transform>,
&mut GlobalTransform,
Entity,
),
(Without<GridCell<P>>, Without<Parent>),
>,
mut root_query_grid: Query<
(
Option<(&Children, Changed<Children>)>,
Changed<Transform>,
Changed<GridCell<P>>,
&GlobalTransform,
Entity,
),
(With<GridCell<P>>, Without<Parent>),
>,
mut transform_query: Query<(
&Transform,
Changed<Transform>,
&mut GlobalTransform,
&Parent,
)>,
children_query: Query<(&Children, Changed<Children>), (With<Parent>, With<GlobalTransform>)>,
) {
let origin_cell_changed = !origin_moved.is_empty();
for (children, transform, transform_changed, mut global_transform, entity) in
root_query_no_grid.iter_mut()
{
let mut changed = transform_changed || origin_cell_changed;
if transform_changed {
*global_transform = GlobalTransform::from(*transform);
}
if let Some((children, changed_children)) = children {
// If our `Children` has changed, we need to recalculate everything below us
changed |= changed_children;
for child in children {
let _ = propagate_recursive(
&global_transform,
&mut transform_query,
&children_query,
*child,
entity,
changed,
);
}
}
}
for (children, cell_changed, transform_changed, global_transform, entity) in
root_query_grid.iter_mut()
{
let mut changed = transform_changed || cell_changed || origin_cell_changed;
if let Some((children, changed_children)) = children {
// If our `Children` has changed, we need to recalculate everything below us
changed |= changed_children;
for child in children {
let _ = propagate_recursive(
&global_transform,
&mut transform_query,
&children_query,
*child,
entity,
changed,
);
}
}
}
}
fn propagate_recursive(
parent: &GlobalTransform,
transform_query: &mut Query<(
&Transform,
Changed<Transform>,
&mut GlobalTransform,
&Parent,
)>,
children_query: &Query<(&Children, Changed<Children>), (With<Parent>, With<GlobalTransform>)>,
entity: Entity,
expected_parent: Entity,
mut changed: bool,
// We use a result here to use the `?` operator. Ideally we'd use a try block instead
) -> Result<(), ()> {
let global_matrix = {
let (transform, transform_changed, mut global_transform, child_parent) =
transform_query.get_mut(entity).map_err(drop)?;
// Note that for parallelising, this check cannot occur here, since there is an `&mut GlobalTransform` (in global_transform)
assert_eq!(
child_parent.get(), expected_parent,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
changed |= transform_changed;
if changed {
*global_transform = parent.mul_transform(*transform);
}
*global_transform
};
let (children, changed_children) = children_query.get(entity).map_err(drop)?;
// If our `Children` has changed, we need to recalculate everything below us
changed |= changed_children;
for child in children {
let _ = propagate_recursive(
&global_matrix,
transform_query,
children_query,
*child,
entity,
changed,
);
}
Ok(())
}

194
src/precision.rs Normal file
View File

@ -0,0 +1,194 @@
//! Contains the [`GridPrecision`] trait and its implementations.
use std::{hash::Hash, ops::Add};
use bevy::reflect::Reflect;
/// Used to make the floating origin plugin generic over many grid sizes.
///
/// 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`]).
pub trait GridPrecision:
Default
+ PartialEq
+ Eq
+ PartialOrd
+ Ord
+ Hash
+ Copy
+ Clone
+ Send
+ Sync
+ Reflect
+ Add
+ std::fmt::Debug
+ std::fmt::Display
+ 'static
{
/// The zero value for this type.
const ZERO: Self;
/// The value of `1` for this type.
const ONE: Self;
/// Adds `rhs` to `self`, wrapping when overflow would occur.
fn wrapping_add(self, rhs: Self) -> Self;
/// Subtracts `rhs` from `self`, wrapping when overflow would occur.
fn wrapping_sub(self, rhs: Self) -> Self;
/// Multiplies `self` by `rhs`.
fn mul(self, rhs: Self) -> Self;
/// Casts `self` as a double precision float.
fn as_f64(self) -> f64;
/// Casts a double precision float into `Self`.
fn from_f64(input: f64) -> Self;
/// Casts a single precision float into `Self`.
fn from_f32(input: f32) -> Self;
}
impl GridPrecision for i8 {
const ZERO: Self = 0;
const ONE: Self = 1;
#[inline]
fn wrapping_add(self, rhs: Self) -> Self {
Self::wrapping_add(self, rhs)
}
#[inline]
fn wrapping_sub(self, rhs: Self) -> Self {
Self::wrapping_sub(self, rhs)
}
#[inline]
fn mul(self, rhs: Self) -> Self {
self * rhs
}
#[inline]
fn as_f64(self) -> f64 {
self as f64
}
#[inline]
fn from_f64(input: f64) -> Self {
input as Self
}
#[inline]
fn from_f32(input: f32) -> Self {
input as Self
}
}
impl GridPrecision for i16 {
const ZERO: Self = 0;
const ONE: Self = 1;
#[inline]
fn wrapping_add(self, rhs: Self) -> Self {
Self::wrapping_add(self, rhs)
}
#[inline]
fn wrapping_sub(self, rhs: Self) -> Self {
Self::wrapping_sub(self, rhs)
}
#[inline]
fn mul(self, rhs: Self) -> Self {
self * rhs
}
#[inline]
fn as_f64(self) -> f64 {
self as f64
}
#[inline]
fn from_f64(input: f64) -> Self {
input as Self
}
#[inline]
fn from_f32(input: f32) -> Self {
input as Self
}
}
impl GridPrecision for i32 {
const ZERO: Self = 0;
const ONE: Self = 1;
#[inline]
fn wrapping_add(self, rhs: Self) -> Self {
Self::wrapping_add(self, rhs)
}
#[inline]
fn wrapping_sub(self, rhs: Self) -> Self {
Self::wrapping_sub(self, rhs)
}
#[inline]
fn mul(self, rhs: Self) -> Self {
self * rhs
}
#[inline]
fn as_f64(self) -> f64 {
self as f64
}
#[inline]
fn from_f64(input: f64) -> Self {
input as Self
}
#[inline]
fn from_f32(input: f32) -> Self {
input as Self
}
}
impl GridPrecision for i64 {
const ZERO: Self = 0;
const ONE: Self = 1;
#[inline]
fn wrapping_add(self, rhs: Self) -> Self {
Self::wrapping_add(self, rhs)
}
#[inline]
fn wrapping_sub(self, rhs: Self) -> Self {
Self::wrapping_sub(self, rhs)
}
#[inline]
fn mul(self, rhs: Self) -> Self {
self * rhs
}
#[inline]
fn as_f64(self) -> f64 {
self as f64
}
#[inline]
fn from_f64(input: f64) -> Self {
input as Self
}
#[inline]
fn from_f32(input: f32) -> Self {
input as Self
}
}
impl GridPrecision for i128 {
const ZERO: Self = 0;
const ONE: Self = 1;
#[inline]
fn wrapping_add(self, rhs: Self) -> Self {
Self::wrapping_add(self, rhs)
}
#[inline]
fn wrapping_sub(self, rhs: Self) -> Self {
Self::wrapping_sub(self, rhs)
}
#[inline]
fn mul(self, rhs: Self) -> Self {
self * rhs
}
#[inline]
fn as_f64(self) -> f64 {
self as f64
}
#[inline]
fn from_f64(input: f64) -> Self {
input as Self
}
#[inline]
fn from_f32(input: f32) -> Self {
input as Self
}
}