big_space 0.12.0

A floating origin plugin for bevy
Documentation
//! A practical example of a spare ship on a planet, in a solar system, surrounded by stars.
extern crate alloc;

use alloc::collections::VecDeque;

use bevy::core_pipeline::Skybox;
use bevy::render::view::Hdr;
use bevy::window::CursorOptions;
use bevy::{
    camera::Exposure,
    color::palettes,
    light::{CascadeShadowConfigBuilder, NotShadowCaster},
    math::DVec3,
    post_process::bloom::Bloom,
    prelude::*,
    transform::TransformSystems,
};
use big_space::prelude::*;
use big_space::validation::BigSpaceValidationPlugin;
use turborand::{rng::Rng, TurboRand};

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins.build().disable::<TransformPlugin>(),
            BigSpaceDefaultPlugins
                .build()
                .enable::<BigSpaceValidationPlugin>(),
        ))
        .insert_resource(GlobalAmbientLight {
            color: Color::WHITE,
            brightness: 200.0,
            ..Default::default()
        })
        .add_systems(Startup, spawn_solar_system)
        .add_systems(Update, configure_skybox_image)
        .add_systems(
            PostUpdate,
            (
                rotate,
                lighting
                    .in_set(TransformSystems::Propagate)
                    .after(BigSpaceSystems::PropagateLowPrecision),
                cursor_grab_system,
                springy_ship
                    .after(big_space::camera::default_camera_inputs)
                    .before(big_space::camera::camera_controller),
            ),
        )
        .register_type::<Sun>()
        .register_type::<Rotates>()
        .run();
}

const EARTH_ORBIT_RADIUS_M: f64 = 149.60e9;
const EARTH_RADIUS_M: f64 = 6.371e6;
const SUN_RADIUS_M: f64 = 695_508_000_f64;
const MOON_RADIUS_M: f64 = 1.7375e6;

#[derive(Component, Reflect)]
struct Sun;

#[derive(Component, Reflect)]
struct PrimaryLight;

#[derive(Component, Reflect)]
struct Spaceship;

#[derive(Component, Reflect)]
struct Rotates(f32);

fn rotate(mut rotate_query: Query<(&mut Transform, &Rotates)>) {
    for (mut transform, rotates) in rotate_query.iter_mut() {
        transform.rotate_local_y(rotates.0);
    }
}

fn lighting(
    mut light: Query<(&mut Transform, &mut GlobalTransform), With<PrimaryLight>>,
    sun: Query<&GlobalTransform, (With<Sun>, Without<PrimaryLight>)>,
) -> Result {
    let sun_pos = sun.single()?.translation();
    let (mut light_tr, mut light_gt) = light.single_mut()?;
    light_tr.look_at(-sun_pos, Vec3::Y);
    *light_gt = (*light_tr).into();
    Ok(())
}

fn springy_ship(
    cam_input: Res<big_space::camera::BigSpaceCameraInput>,
    mut ship: Query<&mut Transform, With<Spaceship>>,
    mut desired_dir: Local<(Vec3, Quat)>,
    mut smoothed_rot: Local<VecDeque<Vec3>>,
) -> Result {
    desired_dir.0 = DVec3::new(cam_input.right, cam_input.up, -cam_input.forward).as_vec3()
        * (1.0 + cam_input.boost as u8 as f32);

    smoothed_rot.truncate(15);
    smoothed_rot.push_front(DVec3::new(cam_input.pitch, cam_input.yaw, cam_input.roll).as_vec3());
    let avg_rot = smoothed_rot.iter().sum::<Vec3>() / smoothed_rot.len() as f32;

    use core::f32::consts::*;
    desired_dir.1 = Quat::IDENTITY.slerp(
        Quat::from_euler(
            EulerRot::XYZ,
            avg_rot.x.clamp(-FRAC_PI_4, FRAC_PI_4),
            avg_rot.y.clamp(-FRAC_PI_4, FRAC_PI_4),
            avg_rot.z.clamp(-FRAC_PI_4, FRAC_PI_4),
        ),
        0.2,
    ) * Quat::from_rotation_y(PI);

    ship.single_mut()?.translation = ship
        .single_mut()?
        .translation
        .lerp(desired_dir.0 * Vec3::new(0.5, 0.5, -2.0), 0.02);

    ship.single_mut()?.rotation = ship.single_mut()?.rotation.slerp(desired_dir.1, 0.02);

    Ok(())
}

fn spawn_solar_system(
    asset_server: Res<AssetServer>,
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let sun_mesh_handle = meshes.add(Sphere::new(SUN_RADIUS_M as f32).mesh().ico(6).unwrap());
    let earth_mesh_handle = meshes.add(Sphere::new(1.0).mesh().ico(35).unwrap());
    let moon_mesh_handle = meshes.add(Sphere::new(MOON_RADIUS_M as f32).mesh().ico(15).unwrap());
    let ball_mesh_handle = meshes.add(Sphere::new(5.0).mesh().ico(5).unwrap());
    let plane_mesh_handle = meshes.add(Plane3d::new(Vec3::X, Vec2::splat(0.5)));

    commands.spawn((
        PrimaryLight,
        DirectionalLight {
            color: Color::WHITE,
            illuminance: 120_000.,
            shadows_enabled: true,
            ..default()
        },
        CascadeShadowConfigBuilder {
            num_cascades: 4,
            minimum_distance: 0.1,
            maximum_distance: 10_000.0,
            first_cascade_far_bound: 100.0,
            overlap_proportion: 0.2,
        }
        .build(),
    ));

    commands.spawn_big_space_default(|root_grid| {
        root_grid.with_grid_default(|sun| {
            sun.insert((Sun, Name::new("Sun")));
            sun.spawn_spatial((
                Mesh3d(sun_mesh_handle),
                MeshMaterial3d(materials.add(StandardMaterial {
                    base_color: Color::WHITE,
                    emissive: LinearRgba::rgb(10000., 10000., 10000.),
                    ..default()
                })),
                NotShadowCaster,
            ));

            let earth_pos = DVec3::Z * EARTH_ORBIT_RADIUS_M;
            let (earth_cell, earth_pos) = sun.grid().translation_to_grid(earth_pos);
            sun.with_grid_default(|earth| {
                earth.insert((
                    Name::new("Earth"),
                    earth_cell,
                    Mesh3d(earth_mesh_handle),
                    MeshMaterial3d(materials.add(StandardMaterial {
                        base_color: Color::Srgba(palettes::css::BLUE),
                        perceptual_roughness: 0.8,
                        reflectance: 1.0,
                        ..default()
                    })),
                    Transform::from_translation(earth_pos)
                        .with_scale(Vec3::splat(EARTH_RADIUS_M as f32)),
                    Rotates(0.000001),
                ));

                let moon_orbit_radius_m = 385e6;
                let moon_pos = DVec3::NEG_Z * moon_orbit_radius_m;
                let (moon_cell, moon_pos) = earth.grid().translation_to_grid(moon_pos);
                earth.spawn_spatial((
                    Name::new("Moon"),
                    Mesh3d(moon_mesh_handle),
                    MeshMaterial3d(materials.add(StandardMaterial {
                        base_color: Color::Srgba(palettes::css::GRAY),
                        perceptual_roughness: 1.0,
                        reflectance: 0.0,
                        ..default()
                    })),
                    Transform::from_translation(moon_pos),
                    moon_cell,
                ));

                let ball_pos =
                    DVec3::X * (EARTH_RADIUS_M + 1.0) + DVec3::NEG_Z * 30.0 + DVec3::Y * 10.0;
                let (ball_cell, ball_pos) = earth.grid().translation_to_grid(ball_pos);
                earth
                    .spawn_spatial((ball_cell, Transform::from_translation(ball_pos)))
                    .with_children(|children| {
                        children.spawn((
                            Mesh3d(ball_mesh_handle),
                            MeshMaterial3d(materials.add(StandardMaterial {
                                base_color: Color::WHITE,
                                ..default()
                            })),
                        ));

                        children.spawn((
                            Mesh3d(plane_mesh_handle),
                            MeshMaterial3d(materials.add(StandardMaterial {
                                base_color: Color::Srgba(palettes::css::DARK_GREEN),
                                perceptual_roughness: 1.0,
                                reflectance: 0.0,
                                ..default()
                            })),
                            Transform::from_scale(Vec3::splat(100.0))
                                .with_translation(Vec3::X * -5.0),
                        ));
                    });

                let cam_pos = DVec3::X * (EARTH_RADIUS_M + 1.0);
                let (cam_cell, cam_pos) = earth.grid().translation_to_grid(cam_pos);
                earth.with_grid_default(|floating_origin| {
                    floating_origin.insert((
                        FloatingOrigin,
                        Transform::from_translation(cam_pos).looking_to(Vec3::NEG_Z, Vec3::X),
                        BigSpaceCameraController::default() // Built-in camera controller
                            .with_speed_bounds([0.1, 10e35])
                            .with_smoothness(0.98, 0.98)
                            .with_speed(1.0),
                        cam_cell,
                    ));

                    floating_origin.spawn_spatial((
                        Camera3d::default(),
                        Transform::from_xyz(0.0, 4.0, 22.0),
                        Hdr,
                        Camera {
                            clear_color: ClearColorConfig::None,
                            ..default()
                        },
                        Exposure::SUNLIGHT,
                        Bloom::ANAMORPHIC,
                        bevy::post_process::effect_stack::ChromaticAberration {
                            intensity: 0.01,
                            ..Default::default()
                        },
                        bevy::post_process::motion_blur::MotionBlur {
                            shutter_angle: 1.0,
                            samples: 8,
                        },
                        Msaa::Off,
                    ));

                    floating_origin.with_spatial(|ship_scene| {
                        ship_scene.insert((
                            Spaceship,
                            SceneRoot(
                                asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0"),
                            ),
                            Transform::from_rotation(Quat::from_rotation_y(core::f32::consts::PI)),
                        ));
                    });
                });
            });
        });

        let star_mat = materials.add(StandardMaterial {
            base_color: Color::WHITE,
            emissive: LinearRgba::rgb(2., 2., 2.),
            ..default()
        });
        let star_mesh_handle = meshes.add(Sphere::new(1e10).mesh().ico(5).unwrap());
        let rng = Rng::new();
        (0..1000).for_each(|_| {
            root_grid.spawn_spatial((
                Mesh3d(star_mesh_handle.clone()),
                MeshMaterial3d(star_mat.clone()),
                Transform::from_xyz(
                    (rng.f32() - 0.5) * 1e14,
                    (rng.f32() - 0.5) * 1e14,
                    (rng.f32() - 0.5) * 1e14,
                ),
            ));
        });
    });
}

fn cursor_grab_system(
    mut windows: Query<&mut CursorOptions, With<bevy::window::PrimaryWindow>>,
    mut cam: ResMut<big_space::camera::BigSpaceCameraInput>,
    btn: Res<ButtonInput<MouseButton>>,
    key: Res<ButtonInput<KeyCode>>,
    mut triggered: Local<bool>,
) -> Result<()> {
    let mut cursor_options = windows.single_mut()?;

    if btn.just_pressed(MouseButton::Right) || !*triggered {
        cursor_options.grab_mode = bevy::window::CursorGrabMode::Locked;
        cursor_options.visible = false;
        // window.mode = WindowMode::BorderlessFullscreen;
        cam.defaults_disabled = false;
        *triggered = true;
    }

    if key.just_pressed(KeyCode::Escape) {
        cursor_options.grab_mode = bevy::window::CursorGrabMode::None;
        cursor_options.visible = true;
        // window.mode = WindowMode::Windowed;
        cam.defaults_disabled = true;
    }

    Ok(())
}

#[derive(Resource)]
struct Cubemap(Handle<Image>, bool);

fn configure_skybox_image(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    cubemap: Option<ResMut<Cubemap>>,
    cameras: Query<Entity, With<Camera>>,
    mut images: ResMut<Assets<Image>>,
) {
    let Some(mut cubemap) = cubemap else {
        commands.insert_resource(Cubemap(asset_server.load("images/cubemap.png"), false));
        return;
    };

    if cubemap.1 {
        return;
    }

    if asset_server.load_state(&cubemap.0).is_loaded() {
        let image = images.get_mut(&cubemap.0).unwrap();
        // NOTE: PNGs do not have any metadata that could indicate they contain a cubemap texture,
        // so they appear as one texture. The following code reconfigures the texture as necessary.
        if image.texture_descriptor.array_layer_count() == 1 {
            image
                .reinterpret_stacked_2d_as_array(image.height() / image.width())
                .unwrap();
            image.texture_view_descriptor =
                Some(bevy::render::render_resource::TextureViewDescriptor {
                    dimension: Some(bevy::render::render_resource::TextureViewDimension::Cube),
                    ..default()
                });
        }
        let camera = cameras.single().unwrap();
        commands.entity(camera).insert(Skybox {
            image: cubemap.0.clone(),
            ..Default::default()
        });
        cubemap.1 = true;
    }
}