benthic_ui 0.3.2

Bevy UI plugin for open metaverse clients
use std::f32::consts::{FRAC_1_SQRT_2, PI};

use benthic_protocol::messages::ui::skybox_update::SkyboxUpdate;
use bevy::core_pipeline::Skybox;
use bevy::core_pipeline::prepass::DeferredPrepass;
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::light::atmosphere::ScatteringMedium;
use bevy::light::light_consts::lux;
use bevy::light::{
    Atmosphere, AtmosphereEnvironmentMapLight, CascadeShadowConfigBuilder, SunDisk, VolumetricFog,
};
use bevy::math::cubic_splines::LinearSpline;
use bevy::pbr::{AtmosphereSettings, DefaultOpaqueRendererMethod, ScreenSpaceReflections};
use bevy::prelude::*;
use bevy_post_process::auto_exposure::{
    AutoExposure, AutoExposureCompensationCurve, AutoExposurePlugin,
};
use bevy_post_process::bloom::Bloom;

use crate::render::MainCamera;

#[derive(Message)]
pub struct SkyboxUpdateEvent {
    pub value: SkyboxUpdate,
}

#[derive(Component)]
pub struct SunLight;

#[derive(Component)]
pub struct MoonLight;

#[derive(Resource)]
pub struct SunState {
    pub current_phase: f32,
    pub target_phase: f32,
}

pub struct SkyPlugin;

impl Plugin for SkyPlugin {
    fn build(&self, app: &mut App) {
        app.add_message::<SkyboxUpdateEvent>()
            .insert_resource(DefaultOpaqueRendererMethod::deferred())
            .add_plugins(AutoExposurePlugin)
            .insert_resource(SunState {
                current_phase: 0.0,
                target_phase: 0.0,
            })
            .add_systems(Startup, setup_skybox)
            .add_systems(Update, (handle_skybox_update, update_sun))
            .insert_resource(SunState {
                current_phase: 0.0,
                target_phase: 0.0,
            });
    }
}

fn setup_skybox(
    mut commands: Commands,
    mut scattering_mediums: ResMut<Assets<ScatteringMedium>>,
    mut compensation_curves: ResMut<Assets<AutoExposureCompensationCurve>>,
    camera: Single<Entity, With<MainCamera>>,
) {
    let under_expose_curve = compensation_curves.add(
        AutoExposureCompensationCurve::from_curve(LinearSpline::new([
            vec2(-4.0, 1.8 * 1.67),
            vec2(-2.0, 0.0 * 1.33),
            vec2(0.0, -2.0),
            vec2(2.0, -2.0 * 0.67),
            vec2(4.0, -2.0 * 0.33),
        ]))
        .expect("Failed to create compensation curve"),
    );

    let medium = scattering_mediums.add(ScatteringMedium::default());

    let cascade_shadow_config = CascadeShadowConfigBuilder {
        first_cascade_far_bound: 0.3,
        maximum_distance: 15.0,
        ..default()
    }
    .build();

    commands.spawn((
        DirectionalLight {
            illuminance: lux::RAW_SUNLIGHT,
            ..default()
        },
        SunDisk::default(),
        Transform::from_xyz(0.0, 1.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
        SunLight,
        cascade_shadow_config,
    ));

    commands.spawn((
        DirectionalLight {
            illuminance: 0.065,
            color: Color::srgb(0.65, 0.7, 1.0),
            ..default()
        },
        Transform::from_xyz(0.0, -1.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
        MoonLight,
    ));

    commands.entity(*camera).insert((
        Atmosphere::earth(medium),
        AtmosphereSettings::default(),
        Skybox {
            brightness: 0.3,
            ..default()
        },
        AtmosphereEnvironmentMapLight::default(),
        Tonemapping::AgX,
        Bloom::NATURAL,
        VolumetricFog {
            ambient_intensity: 0.0,
            ..default()
        },
        AutoExposure {
            compensation_curve: under_expose_curve,
            speed_darken: 8.0,
            ..default()
        },
        DeferredPrepass,
        Msaa::Off,
        ScreenSpaceReflections::default(),
    ));
}

fn handle_skybox_update(mut events: MessageReader<SkyboxUpdateEvent>, mut sun: ResMut<SunState>) {
    for update in events.read() {
        sun.target_phase = update.value.sun_phase;
    }
}

fn update_sun(
    time: Res<Time>,
    mut sun: ResMut<SunState>,
    mut sun_query: Query<&mut Transform, With<SunLight>>,
    mut moon_query: Query<&mut Transform, (With<MoonLight>, Without<SunLight>)>,
) {
    let dt = time.delta_secs();
    let speed = 1.0;

    sun.current_phase = sun
        .current_phase
        .lerp(sun.target_phase, 1.0 - (-speed * dt).exp());

    let adjusted_phase = ((sun.current_phase / (2.0 * PI) + 0.25) % 1.0) * 24.0;

    let azimuth = (adjusted_phase / 24.0) * 2.0 * PI - PI / 2.0;

    let elevation = ((adjusted_phase - 6.0) / 12.0) * PI;

    let mut dir = Vec3::new(
        azimuth.cos() * elevation.cos(),
        azimuth.sin() * elevation.cos(),
        elevation.sin(),
    )
    .normalize();

    if dir.z > 0.0 {
        let sun_dot = dir.z * dir.z;

        let adjusted_dir = (dir + Vec3::new(0.0, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)) * 0.5;

        dir = (adjusted_dir * sun_dot + dir * (1.0 - sun_dot)).normalize();
    }

    for mut transform in &mut sun_query {
        transform.rotation = Quat::from_rotation_arc(Vec3::NEG_Y, dir);
    }

    let moon_dir = -dir;

    for mut transform in &mut moon_query {
        transform.rotation = Quat::from_rotation_arc(Vec3::NEG_Y, moon_dir);
    }
}