big_space

repository·main·Indexed 18 days ago

https://github.com/aevyrie/big_space

A floating origin plugin for Bevy designed for massive scales, from protons to the observable universe. It uses nestable integer grids (i8 to i128) and absolute coordinates to prevent floating-point precision loss and coordinate drift. Key features include spatial hashing, partitioning, and a built-in fly-cam camera controller (BigSpaceCameraControllerPlugin) that supports high-precision movement and speed scaling near objects.

Tokens
12.2K
Snippets
38
Records
49
Agent score
63%

What's inside big_space

  1. Overview of Big Space

    main

    Big Space is a high-performance spatial partitioning and coordinate system designed for massive scales in Bevy. It enables rendering objects ranging from proton-sized meshes to the scale of the observable universe without precision loss or coordinate drift.

    Key features include:

    • Nestable Integer Grids: Chunks the world into grids using integer coordinates from i8 up to i128.
    • Absolute Coordinates: Provides absolute coordinates without the drift associated with camera-relative or periodic recentering solutions.
    • Ecosystem Compatibility: Uses Bevy's Transform, ensuring compatibility with most Bevy plugins and tools.
    • Spatial Hashing & Partitioning: Implements spatial hashing for fast grid cell lookups and neighbor searches, and spatial partitioning to group connected cells.
    • Zero Dependencies: Built with no added dependencies for a lightweight footprint.
  2. How spatial hashing and CellLookup work together

    main

    Spatial hashing in big_space is managed by the CellHashingPlugin. It uses CellId components to identify which grid cell an entity belongs to and a CellLookup<F> resource to provide fast access to entities within those cells.

    Key Components:

    • CellId: A component attached to entities representing their current cell.
    • CellLookup<F>: A resource that maps CellIds to the entities residing in those cells.
    • SpatialHashFilter: A trait used to restrict which entities are hashed. If you use a filter F in the plugin, you must use the corresponding CellLookup<F> resource to access the map.

    Important Ordering Note: Because spatial hashing involves deferred commands and multiple update stages, any user system that queries CellLookup must be ordered after SpatialHashSystems::UpdateCellLookup to ensure the map is populated for the current frame.

  3. How Big Space handles precision and movement

    main

    big_space uses an integer grid system to extend Bevy's Transform component with up to 128 bits of added precision.

    The Mental Model

    • Grid: A large integer grid of cells. The root BigSpace also has a Grid component.
    • CellCoord: An entity's index within its parent's grid.
    • Transform: Positions the entity relative to the center of its current GridCell.
    • Floating Origin: An entity marked with FloatingOrigin determines the 32-bit rendering origin for its BigSpace. All GlobalTransforms in that space are computed relative to this entity's grid cell.

    Moving Entities: Best Practices

    To avoid fighting the plugin's automatic re-centering logic and to maintain precision, avoid setting positions absolutely. Instead, apply relative deltas.

    ❌ Avoid:

    transform.translation = a_huge_imprecise_position;

    ✅ Prefer:

    let delta = new_pos - old_pos;
    transform.translation += delta;

    If you must use absolute positions (e.g., for a stable orbital path), calculate the position in high precision first, then use Grid::translation_to_grid to compute the correct CellCoord and Transform.

  4. Use PartitionLookup to find connected groups of cells

    main

    The PartitionLookup<F> resource is used to quickly find connected groups of occupied grid cells, known as Partitions. Partitions divide space into independent groups.

    Key Mental Model:

    • Partitions track cell occupancy, not entity occupancy. To find which entities are in a partition, you must look up the partition's CellIds in the CellLookup resource.
    • PartitionLookup is built on top of a CellLookup resource using the same SpatialHashFilter (F).
    • It provides a way to map a specific CellId to a PartitionId and vice versa.
    // Assuming PartitionLookup<F> is already added as a resource in your Bevy app
    // F is your SpatialHashFilter
    
    // 1. Find which partition a specific cell belongs to
    if let Some(partition_id) = partition_lookup.get(&some_cell_id) {
        // 2. Resolve the partition details using the ID
        if let Some(partition) = partition_lookup.resolve(partition_id) {
            // Do something with the partition
        }
    }
    
    // 3. Iterate over all existing partitions
    for (id, partition) in partition_lookup.iter() {
        // Process each partition
    }
  5. How Stationary and CellCoord synchronization works

    main

    The Stationary component works in tandem with CellCoord and CellId to maintain spatial consistency.

    • CellCoord: Represents the logical grid coordinates of the entity.
    • CellId: A component added by the system that represents the current cell the entity occupies. It is used for efficient spatial lookups.
    • Stationary: A marker component that tells the system to optimize this entity. While Stationary is present, the system assumes the entity's CellCoord is stable.

    Important Behavior: Wake-up and Movement If you need to move a Stationary entity to a new cell, you should remove the Stationary component in the same operation (or frame) that you update its CellCoord. Once Stationary is removed, the system's CellId::update logic (which runs Without<Stationary>) will detect the change in CellCoord and update the CellId accordingly on the next frame. This ensures the entity is correctly re-indexed in the CellLookup resource at its new location.

    // Simulate wake-up: remove Stationary and change CellCoord in the same operation.
    app.world_mut().entity_mut(entity).remove::<Stationary>();
    app.world_mut()
        .entity_mut(entity)
        .get_mut::<CellCoord>()
        .unwrap()
        .x = 5;
    
    // After app.update(), CellId will reflect the new CellCoord (5, 0, 0)
    // and the entity will be tracked in CellLookup at the new cell.
  6. Use CellCoord for high-precision spatial positioning

    main

    In a high-precision BigSpace, an entity's position is defined by two components: a Transform and a CellCoord.

    • CellCoord acts as the integer index of a cubic cell within its parent Grid.
    • Transform represents the floating-point position of the entity relative to the center of that specific cell.

    This dual-component approach allows for high precision even at massive scales by keeping the Transform values small. All entities with a CellCoord must be children of an entity with a Grid component. If an entity's Transform translation exceeds the maximum_distance_from_origin defined in its Grid, the system will automatically recompute the CellCoord and reset the Transform to keep the translation within bounds.

    // An entity's position is the combination of its cell index and its local transform
    // within that cell.
    // Entity Hierarchy: Grid (Parent) -> Child (with CellCoord and Transform)
  7. How `Stationary` and `StationaryInitialized` work together

    main

    The lifecycle of a stationary entity is managed via two components to ensure all initial computations (like spatial hashing) occur before the entity is optimized into a 'sleeping' state:

    1. Stationary added: The entity is marked as potentially stationary. During this frame, it is still processed by standard propagation and spatial hashing systems.
    2. One-frame delay: The plugin waits for one full frame and (if present) one FixedUpdate tick to ensure the entity is correctly registered in the world.
    3. StationaryInitialized inserted: The plugin automatically inserts this marker. Once present, the entity is considered 'sleeping'. Propagation skips recomputing its GlobalTransform (unless the floating origin moves), and CellHashingPlugin skips recomputing its CellId.
    4. Stationary removed: If the Stationary component is removed, the plugin automatically removes StationaryInitialized, returning the entity to the normal update path.
  8. Optimize non-moving entities with the `Stationary` component

    main

    The Stationary component optimizes entities that do not move by skipping per-frame computations like grid recentering and spatial hashing updates.

    Important Usage Rules

    • Do not move stationary entities by mutating their Transform or CellCoord. The plugin will not detect these changes.
    • To relocate a stationary entity: Remove the Stationary component, move the entity, and then re-add Stationary.
    • Initialization Delay: Stationary takes effect one full frame after insertion. During this first frame, the entity has Stationary but not yet StationaryInitialized.
    • Systems that must run before the entity 'sleeps': Query `(With<Stationary>, Without<StationaryInitialized>)
    • Systems that should skip sleeping entities: Query With<StationaryInitialized>
    // To optimize an entity
    commands.entity(entity).insert(Stationary);
    
    // To move a stationary entity
    commands.entity(entity).remove::<Stationary>();
    // ... move entity ...
    commands.entity(entity).insert(Stationary);
  9. How the camera controller handles large-scale movement

    main

    The BigSpaceCameraController is designed for large-scale worlds and integrates with the floating origin system.

    1. High Precision: It uses DVec3 and DQuat (double precision) for velocity calculations to prevent jitter at extreme distances.
    2. Grid Integration: It converts high-precision translation into grid cell offsets using the Grid resource, ensuring the camera's CellCoord stays synchronized with the world partitions.
    3. Speed Scaling: If slow_near_objects is enabled, the camera's speed is automatically scaled based on the distance to the nearest_object. This prevents the user from 'crashing' into large entities at high speeds.
    4. Framerate Independence: Smoothing (lerping) is calculated using an exponential decay formula 1.0 - smoothness.powf(dt * 60.0), ensuring the 'feel' of the camera remains consistent across different framerates.
  10. How hierarchical Grids work for spatial partitioning

    main

    A Grid is a component used to group entities that move through space together (e.g., entities on a planet orbiting a star). Grids are hierarchical: entities in a child grid move relative to their parent grid. This allows for high precision by using 64-bit float transforms for grid-to-grid relationships and keeping local transforms small.

    Key Rules:

    • All entities with a CellCoord component must be children of an entity with a Grid component.
    • Grids allow for more precision for objects with similar relative velocities.
    • Entities in the same grid as the FloatingOrigin receive the highest rendering precision.
    • Transform propagation starts from the floating origin to minimize accumulated error.
    // Example of the hierarchical relationship:
    // Entity (Grid) -> Child (CellCoord + Transform)
  11. Manage stationary entities with the Stationary component

    main

    To optimize stationary entities in a large-scale spatial grid, attach the Stationary component to an entity. This signals to the system that the entity's position is relatively stable and can be managed via grid-based optimizations.

    When an entity is marked as Stationary, the system automatically manages its CellId and tracks it in the CellLookup resource. To "wake up" an entity (making it dynamic again), simply remove the Stationary component. This removal triggers a chain that cleans up internal tracking components like StationaryInitialized and allows the spatial hash to detect changes to the entity's CellCoord on the next frame.

    Lifecycle of a Stationary entity:

    1. Spawn: Add Stationary and CellCoord to an entity.
    2. Stabilization: After one or two app.update() cycles, the system adds CellId (to track its current cell) and StationaryInitialized (to mark it as fully registered).
    3. Wake-up: Remove Stationary. The entity will then be treated as a dynamic entity, and its CellId will update automatically whenever its CellCoord changes.
    // Spawn entity with Stationary
    let entity = app
        .world_mut()
        .spawn((
            Transform::from_translation(Vec3::ZERO),
            CellCoord::new(1, 0, 0),
            Stationary,
        ))
        .set_parent_in_place(grid_entity)
        .id();
    
    // To wake up the entity (make it dynamic):
    app.world_mut().entity_mut(entity).remove::<Stationary>();
  12. Quickstart: Using Big Space in Bevy

    main

    To use big_space for high-precision spatial hierarchies in Bevy, follow these three steps:

    1. Add the plugin: Add BigSpaceDefaultPlugins to your App.
    2. Spawn a BigSpace: Use spawn_big_space (via BigSpaceCommands) to create the root of a high-precision hierarchy.
    3. Set the Floating Origin: Add the FloatingOrigin component to your active camera within that BigSpace. This camera will act as the 32-bit rendering origin, ensuring smooth movement and high precision for all entities in that space.

    Note: Grids can be nested like Transforms to create moving high-precision hierarchies (e.g., a planet's surface grid orbiting a star).

    // Example setup logic (conceptual based on documentation)
    app.add_plugins(BigSpaceDefaultPlugins)
       .add_systems(Startup, setup_space);
    
    fn setup_space(mut commands: Commands) {
        // 1. Spawn the root BigSpace
        commands.spawn_big_space();
    
        // 2. Spawn a camera and mark it as the FloatingOrigin
        commands.spawn(( 
            Camera3dBundle::default(), 
            FloatingOrigin 
        ));
    }