#![doc = include_str!("../README.md")]
#![expect(
clippy::too_many_arguments,
clippy::type_complexity,
reason = "Bevy systems commonly have many parameters and complex query types."
)]
mod render;
mod skybox;
use std::ops::Deref;
use bevy::{
camera::visibility::NoFrustumCulling, prelude::*, render::sync_world::SyncToRenderWorld,
};
pub use finite_light_math as math;
use finite_light_math::{PoincareTransform, SpacetimeEvent, Vec3, Vec4};
pub use skybox::RelativisticSkybox;
#[derive(Component)]
pub struct HideWorldLine;
#[derive(Resource, Default)]
pub struct ProperTime {
elapsed: f64,
delta: f64,
}
impl ProperTime {
pub fn elapsed_secs(&self) -> f32 {
self.elapsed as f32
}
pub fn delta_secs(&self) -> f32 {
self.delta as f32
}
}
#[derive(Resource)]
pub struct RelativisticMetric(pub finite_light_math::Metric);
impl Deref for RelativisticMetric {
type Target = finite_light_math::Metric;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Component, Default)]
#[require(Transform)]
pub struct Relativistic {
poincare: PoincareTransform,
world_line: finite_light_math::WorldLine,
bounding_radius: Option<f32>,
seeded: bool,
last_synced: Option<(bevy::math::Vec3, Quat)>,
}
#[derive(Component)]
pub struct NonRelativistic;
impl Relativistic {
pub fn transform(&self) -> &PoincareTransform {
&self.poincare
}
pub fn transform_mut(&mut self) -> &mut PoincareTransform {
&mut self.poincare
}
pub fn with_velocity(mut self, metric: finite_light_math::Metric, v: Vec3) -> Self {
self.set_velocity(metric, v);
self
}
pub fn set_velocity(&mut self, metric: finite_light_math::Metric, v: Vec3) {
self.poincare.lorentz.boost = finite_light_math::Boost::from_velocity(metric, v);
}
pub fn update_position(&mut self, metric: finite_light_math::Metric, dt: f32) {
let v = self.poincare.lorentz.boost.velocity(metric);
self.poincare
.translation
.set_spatial(self.poincare.translation.spatial() + v * dt);
}
pub fn world_line(&self) -> &finite_light_math::WorldLine {
&self.world_line
}
}
#[derive(Component)]
pub(crate) struct PendingMeshData {
pub(crate) vertices: Vec<Vec4>,
pub(crate) normals: Vec<Vec4>,
}
#[derive(Component)]
pub struct RelativisticChild {
pub source: Entity,
pub offset: Vec3,
pub rotation: Quat,
}
#[derive(Component)]
pub(crate) struct GpuTransformed;
pub struct RelativisticPlugin {
pub metric: finite_light_math::Metric,
pub debug: bool,
}
impl RelativisticPlugin {
pub fn with_speed_of_light(speed_of_light: f32) -> Self {
Self {
metric: finite_light_math::Metric { speed_of_light },
debug: false,
}
}
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug = debug;
self
}
}
impl Plugin for RelativisticPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(RelativisticMetric(self.metric))
.init_resource::<ProperTime>()
.add_plugins(render::RelativisticRenderPlugin)
.add_systems(Update, skybox::assemble_skybox)
.add_systems(
PostUpdate,
(
init_relativistic,
ApplyDeferred,
update_poincare,
advance_proper_time,
sync_children,
record_and_gc,
sync_transforms,
init_mesh_data,
)
.chain()
.after(TransformSystems::Propagate),
);
if self.debug {
app.add_systems(Update, draw_world_lines).add_systems(
Startup,
|mut config_store: ResMut<GizmoConfigStore>| {
config_store
.config_mut::<DefaultGizmoConfigGroup>()
.0
.depth_bias = -1.;
},
);
}
}
}
fn init_relativistic(
mut commands: Commands,
parent_query: Query<&ChildOf>,
relativistic_query: Query<&GlobalTransform, With<Relativistic>>,
non_rel_query: Query<(), With<NonRelativistic>>,
mesh_query: Query<
(Entity, &GlobalTransform),
(
With<Mesh3d>,
Without<Relativistic>,
Without<NonRelativistic>,
),
>,
) {
let mut roots_handled = std::collections::HashSet::new();
for (mesh_entity, mesh_global) in &mesh_query {
match walk_ancestors(
mesh_entity,
&parent_query,
&relativistic_query,
&non_rel_query,
) {
AncestorResult::Relativistic(source, source_global) => {
let (_, mesh_rot, mesh_trans) = mesh_global.to_scale_rotation_translation();
let mut relativistic = Relativistic::default();
relativistic.poincare.translation = SpacetimeEvent::new(mesh_trans, 0.);
relativistic.poincare.lorentz.rotation = mesh_rot;
relativistic.seeded = true;
let (_, source_rot, source_trans) = source_global.to_scale_rotation_translation();
let inv_source_rot = source_rot.inverse();
commands.entity(mesh_entity).insert((
relativistic,
RelativisticChild {
source,
offset: inv_source_rot * (mesh_trans - source_trans),
rotation: inv_source_rot * mesh_rot,
},
));
}
AncestorResult::NonRelativistic => {}
AncestorResult::None => {
let root = find_root(mesh_entity, &parent_query);
if root == mesh_entity {
let (_, rot, trans) = mesh_global.to_scale_rotation_translation();
let mut relativistic = Relativistic::default();
relativistic.poincare.translation = SpacetimeEvent::new(trans, 0.);
relativistic.poincare.lorentz.rotation = rot;
relativistic.seeded = true;
commands.entity(mesh_entity).insert(relativistic);
} else if roots_handled.insert(root) {
commands.entity(root).insert(Relativistic::default());
}
}
}
}
}
enum AncestorResult {
Relativistic(Entity, GlobalTransform),
NonRelativistic,
None,
}
fn walk_ancestors(
start: Entity,
parent_query: &Query<&ChildOf>,
relativistic_query: &Query<&GlobalTransform, With<Relativistic>>,
non_rel_query: &Query<(), With<NonRelativistic>>,
) -> AncestorResult {
let mut current = start;
while let Ok(child_of) = parent_query.get(current) {
let parent = child_of.0;
if let Ok(global) = relativistic_query.get(parent) {
return AncestorResult::Relativistic(parent, *global);
}
if non_rel_query.get(parent).is_ok() {
return AncestorResult::NonRelativistic;
}
current = parent;
}
AncestorResult::None
}
fn find_root(start: Entity, parent_query: &Query<&ChildOf>) -> Entity {
let mut current = start;
while let Ok(child_of) = parent_query.get(current) {
current = child_of.0;
}
current
}
fn update_poincare(
time: Res<Time>,
mut non_child: Query<(&mut Relativistic, &Transform), Without<RelativisticChild>>,
mut children: Query<&mut Relativistic, With<RelativisticChild>>,
) {
let t = time.elapsed_secs();
for (mut relativistic, transform) in &mut non_child {
if !relativistic.seeded {
relativistic
.poincare
.translation
.set_spatial(transform.translation);
relativistic.poincare.lorentz.rotation = transform.rotation;
relativistic.seeded = true;
}
relativistic.poincare.translation.set_t(t);
}
for mut relativistic in &mut children {
relativistic.poincare.translation.set_t(t);
}
}
fn advance_proper_time(
time: Res<Time>,
camera: Query<&Relativistic, With<Camera3d>>,
mut proper_time: ResMut<ProperTime>,
) {
let gamma = camera
.single()
.map_or(1., |rel| rel.transform().lorentz.boost.gamma() as f64);
let delta = time.delta_secs_f64() / gamma;
proper_time.delta = delta;
proper_time.elapsed += delta;
}
fn sync_children(
source_query: Query<&Relativistic, Without<RelativisticChild>>,
mut follower_query: Query<(&mut Relativistic, &RelativisticChild)>,
) {
for (mut follower, follow) in &mut follower_query {
let Ok(source) = source_query.get(follow.source) else {
continue;
};
let parent = source.transform();
let world_offset = parent.lorentz.rotation * follow.offset;
follower
.poincare
.translation
.set_spatial(parent.translation.spatial() + world_offset);
follower.poincare.lorentz.rotation = parent.lorentz.rotation * follow.rotation;
follower.poincare.lorentz.boost = parent.lorentz.boost;
}
}
fn record_and_gc(
metric: Res<RelativisticMetric>,
camera_query: Query<&Relativistic, With<Camera3d>>,
mut source_query: Query<
(Entity, &mut Relativistic),
(Without<Camera3d>, Without<RelativisticChild>),
>,
child_query: Query<(&Relativistic, &RelativisticChild), Without<Camera3d>>,
mut child_extents: Local<std::collections::HashMap<Entity, f32>>,
) {
let camera_position = camera_query
.single()
.ok()
.map(|camera| camera.poincare.translation);
child_extents.clear();
for (child, follow) in &child_query {
let extent = follow.offset.length() + child.bounding_radius.unwrap_or(0.);
let entry = child_extents.entry(follow.source).or_insert(0.);
*entry = entry.max(extent);
}
for (entity, mut relativistic) in &mut source_query {
let poincare = relativistic.poincare;
relativistic.world_line.push(poincare);
if let Some(cam_pos) = camera_position {
let own_radius = relativistic.bounding_radius.unwrap_or(0.);
let child_radius = child_extents.get(&entity).copied().unwrap_or(0.);
relativistic
.world_line
.gc(metric.0, cam_pos, own_radius.max(child_radius));
}
}
}
fn draw_world_lines(
query: Query<&Relativistic, (Without<HideWorldLine>, Without<RelativisticChild>)>,
mut gizmos: Gizmos,
) {
for relativistic in &query {
let keyframes = relativistic.world_line.keyframes();
for window in keyframes.iter().collect::<Vec<_>>().windows(2) {
let a = window[0].translation.spatial();
let b = window[1].translation.spatial();
gizmos.line(a, b, Color::srgb(0., 1., 0.));
}
}
}
fn sync_transforms(
mut gpu: Query<(&mut Transform, &mut GlobalTransform), With<GpuTransformed>>,
mut non_gpu: Query<
(&mut Relativistic, &mut Transform, &mut GlobalTransform),
(Without<GpuTransformed>, Without<RelativisticChild>),
>,
) {
for (mut transform, mut global_transform) in &mut gpu {
if *transform != Transform::IDENTITY {
*transform = Transform::IDENTITY;
}
if *global_transform != GlobalTransform::IDENTITY {
*global_transform = GlobalTransform::default();
}
}
for (mut relativistic, mut transform, mut global_transform) in &mut non_gpu {
if let Some((last_t, last_r)) = relativistic.last_synced {
assert!(
transform.translation == last_t && transform.rotation == last_r,
"Transform on a `Relativistic` entity was modified externally. Use \
`Relativistic::transform_mut()` to change the spacetime pose.",
);
}
transform.translation = relativistic.poincare.translation.spatial();
transform.rotation = relativistic.poincare.lorentz.rotation;
relativistic.last_synced = Some((transform.translation, transform.rotation));
*global_transform = GlobalTransform::from(*transform);
}
}
fn init_mesh_data(
mut commands: Commands,
meshes: Res<Assets<Mesh>>,
pending_query: Query<Entity, With<PendingMeshData>>,
existing_query: Query<&Mesh3d, With<GpuTransformed>>,
mut query: Query<
(Entity, &mut Relativistic, &Mesh3d, &GlobalTransform),
Without<GpuTransformed>,
>,
) {
for entity in &pending_query {
commands.entity(entity).remove::<PendingMeshData>();
}
let mut claimed_meshes = std::collections::HashSet::new();
for mesh_handle in &existing_query {
claimed_meshes.insert(mesh_handle.id());
}
for (entity, mut relativistic, mesh_handle, global_transform) in &mut query {
assert!(
claimed_meshes.insert(mesh_handle.id()),
"Multiple Relativistic entities share mesh asset {:?}. Each entity needs a unique \
mesh handle because the compute shader writes directly into the vertex buffer keyed \
by asset ID.",
mesh_handle.id(),
);
let Some(mesh) = meshes.get(mesh_handle) else {
continue;
};
let Some(positions) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) else {
continue;
};
let (scale, _, _) = global_transform.to_scale_rotation_translation();
let vertices: Vec<Vec4> = positions
.as_float3()
.unwrap()
.iter()
.map(|p| Vec4::new(p[0] * scale.x, p[1] * scale.y, p[2] * scale.z, 1.))
.collect();
let bounding_radius = vertices
.iter()
.map(|p| p.truncate().length())
.fold(0f32, f32::max);
relativistic.bounding_radius = Some(bounding_radius);
let inv_scale = Vec3::new(1. / scale.x, 1. / scale.y, 1. / scale.z);
let normals: Vec<Vec4> = mesh
.attribute(Mesh::ATTRIBUTE_NORMAL)
.and_then(|n| n.as_float3())
.map(|slice| {
slice
.iter()
.map(|n| {
let normal =
Vec3::new(n[0] * inv_scale.x, n[1] * inv_scale.y, n[2] * inv_scale.z)
.normalize();
Vec4::new(normal.x, normal.y, normal.z, 0.)
})
.collect()
})
.unwrap_or_else(|| vec![Vec4::new(0., 1., 0., 0.); vertices.len()]);
commands.entity(entity).insert((
PendingMeshData { vertices, normals },
GpuTransformed,
SyncToRenderWorld,
NoFrustumCulling,
));
}
}