| .vscode | ||
| examples | ||
| finite_light_bevy | ||
| finite_light_gpu | ||
| finite_light_gpu_common | ||
| finite_light_gpu_shaders | ||
| finite_light_math | ||
| scripts | ||
| .gitignore | ||
| .gitlab-ci.yml | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| impl_guide.pdf | ||
| impl_guide.typ | ||
| LICENSE-APACHE | ||
| LICENSE-MIT | ||
| README.md | ||
| rust-toolchain.toml | ||
| rustfmt.toml | ||
Finite Light Project
Tools for enabling relativistic rendering for games and visualizations in Rust. Currently comprises
a plugin for the Bevy game engine, finite_light_bevy, along with other
supporting crates.
When enabled, objects appear as they actually would to an observer in a universe with a finite speed of light: light-travel delay, Lorentz contraction, relativistic aberration, and Penrose-Terrell rotation are all computed per-vertex on the GPU.
Check out the web demo here.
Can I add relativistic rendering to my Bevy game/app?
Yes! The biggest hurdle might be velocity tracking: once enabled, the [RelativisticPlugin] must be
continually informed about the correct velocity of every moving object in a scene, and velocities
can never exceed the configured speed of light (otherwise there can be visual glitches). See below.
Quick start
use bevy::prelude::*;
use finite_light_bevy::{RelativisticPlugin, Relativistic, RelativisticMetric};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(RelativisticPlugin::with_speed_of_light(30.))
.add_systems(Startup, set_up)
.add_systems(Update, move_cube)
.run();
}
#[derive(Component)]
struct MovingCube;
fn set_up(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
metric: Res<RelativisticMetric>,
) {
// Manually enroll a cube in relativistic rendering and give it a constant initial
// velocity. `Transform` can be used to seed initial position/pose, but subsequent
// changes must use the `PoincareTransform` contained in the `Relativistic` component
// instead.
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.2, 0.2))),
Transform::from_xyz(50., 0., 0.),
Relativistic::default().with_velocity(**metric, Vec3::new(-10., 0., 0.)),
MovingCube,
));
// Add a stationary cube. `Relativistic` will be added automatically by the plugin.
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.8, 0.8))),
Transform::from_xyz(-50., 0., 0.),
));
// A camera. Non-mesh entities need explicit `Relativistic::default()`.
commands.spawn((
Camera3d::default(),
Relativistic::default(),
));
}
/// Advance the cube's position each frame according to its constant velocity.
fn move_cube(
mut query: Query<&mut Relativistic, With<MovingCube>>,
metric: Res<RelativisticMetric>,
time: Res<Time>,
) {
let Ok(mut relativistic) = query.single_mut() else { return };
relativistic.update_position(**metric, time.delta_secs());
}
How it works
Each frame, the plugin records each entity's spacetime pose as a keyframe on its world line. A
compute shader then solves, for every mesh vertex, the intersection of the camera's past light cone
with the object's world line. (See impl_guide.typ.) Each vertex is then placed at the retarded
position -- where it was when the light now reaching the camera was emitted -- and the
displacement is Lorentz-transformed into the camera's rest frame to produce correct relativistic
aberration. Normals are rotated to match the object's orientation at retarded time.
The result is written directly into Bevy's mesh buffers, so standard materials and lighting work without modification.
What it doesn't do
- Lighting is not treated relativistically: relativistic Doppler shifts and beaming are currently not included, nor are time retardation effects for lighting (e.g. a light turns on at t = 0, object becomes illuminated later when light first hits it).
- Relativistic dynamics are not automatically enabled, e.g. relativistic collisions. This needs to be accounted for in game systems manually if desired.
Examples
See examples/! Run e.g. the Mars demo with cargo run --bin mars.
Caveats
Each [Relativistic] entity with a Mesh3d must have a unique Mesh3d. The compute shader
writes transformed vertices directly into the vertex buffer keyed by mesh asset ID, so entities
sharing a handle would overwrite each other. Call meshes.add(...) per entity rather than cloning a
single handle.
Core concepts
PoincareTransform
An inhomogeneous Lorentz transformation -- the
relativistic equivalent of Bevy's Transform. It combines a spatial rotation, a Lorentz boost
(velocity), and a spacetime translation (position in space and time). This is the fundamental
state for each relativistic entity, which the plugin tracks.
Relativistic
Tracks an entity's spacetime state and history. The plugin automatically adds this to every
[Mesh3d] entity it discovers, seeding the [PoincareTransform] from the entity's [Transform].
For non-mesh entities (cameras, player controllers), add Relativistic::default() yourself; the
Poincare is seeded from the [Transform] on the first frame.
Access the [PoincareTransform] via transform()/transform_mut(). Convenience methods
set_velocity(), with_velocity(), and update_position() cover the common case of setting a
velocity and advancing position each frame.
NonRelativistic
Opt-out marker. Add this to an entity to prevent the plugin from auto-adding [Relativistic] to it
or any of its [Mesh3d] descendants. Useful for UI elements or other geometry that should bypass
the relativistic pipeline.
RelativisticChild
Added automatically by init_relativistic for [Mesh3d] descendants of a [Relativistic] entity.
Each frame, the entity's [PoincareTransform] is composed from the source entity's transform plus a
stored local offset (position and rotation). Can also be added manually for custom follow behavior.
ProperTime
Accumulated proper time along the camera's world line. Each frame, delta_secs() is set to
dt_game / gamma (where gamma is the camera's Lorentz factor) and added to the running total.
To make the game tick at proper-time rate, call Time<Virtual>::set_relative_speed(gamma). Game
systems that apply forces or consume resources should read Res<Time<Real>> to avoid a positive
feedback loop where acceleration scales with gamma.
The metric
RelativisticPlugin takes a math::Metric that sets the speed of light. The metric is available as
the RelativisticMetric resource.
RelativisticPlugin::with_speed_of_light(30.)
Debug mode
Chain .with_debug(true) on the plugin to visualize entity world lines with gizmos. Add
HideWorldLine to any entity you want to exclude from the visualization.
Public API/ontology
| Item | Kind | Description |
|---|---|---|
RelativisticPlugin |
Plugin | Registers all systems and the GPU pipeline. |
Relativistic |
Component | Tracks spacetime state and world-line history. |
NonRelativistic |
Component | Opt-out: suppresses auto-enrollment of meshes. |
RelativisticChild |
Component | Links an entity's Poincare to a source + offset. |
RelativisticSkybox |
Component | Cubemap skybox rendered as a mesh with aberration. |
HideWorldLine |
Component | Excludes an entity from debug world-line drawing. |
RelativisticMetric |
Resource | Wraps math::Metric; derefs to &Metric. |
ProperTime |
Resource | Accumulated proper time along the camera world line. |
math |
Re-export | The underlying spacetime math library. |
Key types re-exported through math:
PoincareTransform: Inhomogeneous Lorentz transform (rotation + boost + spacetime translation).Boost: Lorentz boost parameterized by rapidity vector. Collinear boosts compose by adding rapidities.SpacetimeEvent: A point in Minkowski spacetime(x, y, z, t).Metric: Minkowski metric with configurable speed of light.WorldLine: Chronologically ordered history of spacetime poses.
System schedule
All systems run in PostUpdate, chained after TransformSystems::Propagate:
init_relativistic: For eachMesh3dwithoutRelativistic, walk up the hierarchy looking for an existingRelativisticancestor (wireRelativisticChild) or aNonRelativisticancestor (skip). If neither is found, addRelativisticto the hierarchy root (or directly to a standalone mesh).update_poincare: Seed newly-added entities' Poincare from theirTransform, then stamp the current wall time on all entities.advance_proper_time: IncrementProperTimebydt / gammausing the camera's Lorentz factor.sync_children: Compose source Poincare + local offset forRelativisticChildentities.record_and_gc: Push a keyframe onto each entity's world line and discard keyframes that can no longer affect any vertex.sync_transforms: ResetTransformandGlobalTransformto identity for GPU-transformed entities; syncPoincareTransformtoTransformfor others.init_mesh_data: Lazily prepare vertex data for new entities (bakes scale into vertices). Runs last because it addsGpuTransformedvia deferred commands.