rustial-renderer-bevy 0.0.1

Bevy Engine renderer for the rustial 2.5D map engine
//! Pitch-adaptive horizon fade system.
//!
//! When the camera is pitched toward the horizon, distant tiles fade
//! smoothly per-pixel -- matching the WGPU renderer's tile.wgsl fog.
//!
//! This system updates the fog uniform parameters on
//! [`TileFogMaterial`] and [`HillshadeMaterial`] instances only when
//! the underlying camera or hillshade state has changed, rather than
//! unconditionally every frame.  The actual per-fragment distance fade
//! is computed in the GPU shader, eliminating the visible per-tile
//! boundaries that occur with uniform-alpha approaches.

use bevy::prelude::*;

use crate::hillshade_material::HillshadeMaterial;
use crate::plugin::MapStateResource;
use crate::systems::frame_change_detection::{frame_unchanged, FrameChangeDetection};
use crate::tile_fog_material::TileFogMaterial;

/// Tracks the last fog parameters written to materials so the system
/// can skip redundant writes when the camera and hillshade state are
/// unchanged.
#[derive(Resource, Default)]
pub struct FogDirtyState {
    last_eye_pos: [i32; 3],
    last_fog_params: [i32; 4],
    last_fog_color: [i32; 4],
    last_hillshade_highlight: [i32; 4],
    last_hillshade_shadow: [i32; 4],
    last_hillshade_accent: [i32; 4],
    last_hillshade_light: [i32; 4],
    /// Number of materials present last time we wrote.  When new
    /// materials are added (entity spawn) we must write even if the
    /// camera hasn't moved.
    last_tile_material_count: usize,
    last_hillshade_material_count: usize,
}

impl FogDirtyState {
    fn quantise_vec4(v: Vec4) -> [i32; 4] {
        [
            (v.x * 1000.0) as i32,
            (v.y * 1000.0) as i32,
            (v.z * 1000.0) as i32,
            (v.w * 1000.0) as i32,
        ]
    }

    fn quantise_vec3(x: f32, y: f32, z: f32) -> [i32; 3] {
        [
            (x * 1000.0) as i32,
            (y * 1000.0) as i32,
            (z * 1000.0) as i32,
        ]
    }
}

/// Update fog parameters on all tile materials when camera or
/// hillshade state has changed.
///
/// Runs each frame after texture upload. Computes the same pitch-based
/// fog ramp used in the WGPU renderer and writes `eye_pos`, `fog_start`,
/// `fog_end`, and `density` into each [`TileFogMaterial`] uniform.
/// The fragment shader then uses these to fade pixels smoothly based on
/// their ground-plane distance to the camera eye.
///
/// Skips the write pass entirely when fog parameters are unchanged from
/// the previous frame AND no new materials have been added (avoiding
/// O(n_materials) iteration on steady-state frames).
pub(crate) fn sync_horizon_fade(
    state: Res<MapStateResource>,
    mut materials: ResMut<Assets<TileFogMaterial>>,
    mut hillshade_materials: ResMut<Assets<HillshadeMaterial>>,
    mut dirty: ResMut<FogDirtyState>,
    detection: Res<FrameChangeDetection>,
) {
    // Whole-frame skip: if nothing in the engine changed AND no new
    // materials have been added, fog writes are unnecessary.
    // Note: we still need to check material counts because new entities
    // may have been spawned by other systems on the same frame.
    let tile_count = materials.len();
    let hillshade_count = hillshade_materials.len();
    if frame_unchanged(&detection, &state.0)
        && tile_count == dirty.last_tile_material_count
        && hillshade_count == dirty.last_hillshade_material_count
    {
        return;
    }

    let cam = state.0.camera();
    let fog = state.0.computed_fog();
    let hillshade = state.0.hillshade().unwrap_or_default();

    let eye = cam.eye_offset();

    let eye_pos = Vec4::new(eye.x as f32, eye.y as f32, eye.z as f32, 0.0);
    let fog_params = Vec4::new(fog.fog_start, fog.fog_end, fog.fog_density, 0.0);
    let fog_color = Vec4::new(
        fog.fog_color[0],
        fog.fog_color[1],
        fog.fog_color[2],
        fog.fog_color[3],
    );
    let hs_highlight = Vec4::from_array(hillshade.highlight_color);
    let hs_shadow = Vec4::from_array(hillshade.shadow_color);
    let hs_accent = Vec4::from_array(hillshade.accent_color);
    let hs_light = Vec4::new(
        hillshade.illumination_direction,
        hillshade.illumination_altitude,
        hillshade.exaggeration,
        hillshade.opacity,
    );

    // Check whether any parameter has changed since the last write.
    let q_eye = FogDirtyState::quantise_vec3(eye.x as f32, eye.y as f32, eye.z as f32);
    let q_fog = FogDirtyState::quantise_vec4(fog_params);
    let q_color = FogDirtyState::quantise_vec4(fog_color);
    let q_highlight = FogDirtyState::quantise_vec4(hs_highlight);
    let q_shadow = FogDirtyState::quantise_vec4(hs_shadow);
    let q_accent = FogDirtyState::quantise_vec4(hs_accent);
    let q_light = FogDirtyState::quantise_vec4(hs_light);

    let params_changed = q_eye != dirty.last_eye_pos
        || q_fog != dirty.last_fog_params
        || q_color != dirty.last_fog_color
        || q_highlight != dirty.last_hillshade_highlight
        || q_shadow != dirty.last_hillshade_shadow
        || q_accent != dirty.last_hillshade_accent
        || q_light != dirty.last_hillshade_light;

    let tile_materials_added = tile_count != dirty.last_tile_material_count;
    let hillshade_materials_added = hillshade_count != dirty.last_hillshade_material_count;

    if !params_changed && !tile_materials_added && !hillshade_materials_added {
        return;
    }

    if params_changed || tile_materials_added {
        for (_id, mat) in materials.iter_mut() {
            mat.fog.eye_pos = eye_pos;
            mat.fog.fog_params = fog_params;
            mat.fog.fog_color = fog_color;
            mat.fog.hillshade_highlight = hs_highlight;
            mat.fog.hillshade_shadow = hs_shadow;
            mat.fog.hillshade_accent = hs_accent;
            mat.fog.hillshade_light = hs_light;
        }
    }

    if params_changed || hillshade_materials_added {
        for (_id, mat) in hillshade_materials.iter_mut() {
            mat.fog.eye_pos = eye_pos;
            mat.fog.fog_params = fog_params;
            mat.fog.fog_color = fog_color;
            mat.fog.hillshade_highlight = hs_highlight;
            mat.fog.hillshade_shadow = hs_shadow;
            mat.fog.hillshade_accent = hs_accent;
            mat.fog.hillshade_light = hs_light;
        }
    }

    dirty.last_eye_pos = q_eye;
    dirty.last_fog_params = q_fog;
    dirty.last_fog_color = q_color;
    dirty.last_hillshade_highlight = q_highlight;
    dirty.last_hillshade_shadow = q_shadow;
    dirty.last_hillshade_accent = q_accent;
    dirty.last_hillshade_light = q_light;
    dirty.last_tile_material_count = tile_count;
    dirty.last_hillshade_material_count = hillshade_count;
}