Special relativistic rendering for games
Find a file
2026-03-09 18:57:20 -07:00
.vscode Fix Clippy & Wasm build 2026-03-03 09:29:29 -08:00
examples Improve README and add convenience methods 2026-03-09 09:11:45 -07:00
finite_light_bevy Fix handling of README.md 2026-03-09 18:57:20 -07:00
finite_light_gpu README/Cargo.toml updates 2026-03-09 18:49:36 -07:00
finite_light_gpu_common README/Cargo.toml updates 2026-03-09 18:49:36 -07:00
finite_light_gpu_shaders Bump version 2026-03-09 18:50:43 -07:00
finite_light_math README/Cargo.toml updates 2026-03-09 18:49:36 -07:00
scripts Fix publish script 2026-03-09 15:52:41 -07:00
.gitignore Prepare for publishing to crates.io 2026-03-02 17:50:49 -08:00
.gitlab-ci.yml README/Cargo.toml updates 2026-03-09 18:49:36 -07:00
Cargo.lock Bump version 2026-03-09 18:50:43 -07:00
Cargo.toml Bump version 2026-03-09 18:50:43 -07:00
CLAUDE.md Improve README and add convenience methods 2026-03-09 09:11:45 -07:00
impl_guide.pdf Fix skybox orientation and sampling 2026-03-03 20:00:28 -08:00
impl_guide.typ Fix skybox orientation and sampling 2026-03-03 20:00:28 -08:00
LICENSE-APACHE Prepare for publishing to crates.io 2026-03-02 17:50:49 -08:00
LICENSE-MIT Prepare for publishing to crates.io 2026-03-02 17:50:49 -08:00
README.md README/Cargo.toml updates 2026-03-09 18:49:36 -07:00
rust-toolchain.toml Prepare for publishing to crates.io 2026-03-02 17:50:49 -08:00
rustfmt.toml Improve README and add convenience methods 2026-03-09 09:11:45 -07:00

Finite Light Project

Pipeline status crates.io docs.rs License: MIT or Apache 2.0 Bevy 0.18 Web Demo

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:

  1. init_relativistic: For each Mesh3d without Relativistic, walk up the hierarchy looking for an existing Relativistic ancestor (wire RelativisticChild) or a NonRelativistic ancestor (skip). If neither is found, add Relativistic to the hierarchy root (or directly to a standalone mesh).
  2. update_poincare: Seed newly-added entities' Poincare from their Transform, then stamp the current wall time on all entities.
  3. advance_proper_time: Increment ProperTime by dt / gamma using the camera's Lorentz factor.
  4. sync_children: Compose source Poincare + local offset for RelativisticChild entities.
  5. record_and_gc: Push a keyframe onto each entity's world line and discard keyframes that can no longer affect any vertex.
  6. sync_transforms: Reset Transform and GlobalTransform to identity for GPU-transformed entities; sync PoincareTransform to Transform for others.
  7. init_mesh_data: Lazily prepare vertex data for new entities (bakes scale into vertices). Runs last because it adds GpuTransformed via deferred commands.