use bevy::app::PluginGroupBuilder;
use bevy::camera::Hdr;
use bevy::core_pipeline::prepass::DepthPrepass;
use bevy::ecs::system::SystemParam;
use bevy::light::atmosphere::ScatteringMedium;
use bevy::light::{Atmosphere, AtmosphereEnvironmentMapLight, VolumetricFog};
use bevy::pbr::{AtmosphereSettings, DistanceFog};
use bevy::prelude::*;
use bevy::render::view::Msaa;
use bevy::shader::load_shader_library;
use crate::celestial::{CelestialPlugin, SunState, clock_plugin};
use crate::climate::{ClimatePlugin, Weather};
use crate::clouds::CloudPlugin;
use crate::cover::GroundCoverPlugin;
use crate::dust::DustPlugin;
use crate::fog::FogPlugin;
use crate::gpu::readback::ReadbackPlugin;
use crate::hydrology::{HydrologyPlugin, WaterPlugin};
use crate::lightning::LightningPlugin;
use crate::paths::PathsPlugin;
use crate::precipitation::PrecipitationPlugin;
use crate::query::EnvironmentSnapshot;
use crate::rocks::RocksPlugin;
use crate::terrain::TerrainPlugin;
use crate::trails::TrailsPlugin;
use crate::waves::WavesPlugin;
use crate::weathered::WeatheredPlugin;
use crate::wind::{WindPlugin, WindState};
pub struct ElementsPlugins;
impl PluginGroup for ElementsPlugins {
fn build(self) -> PluginGroupBuilder {
let group = PluginGroupBuilder::start::<Self>()
.add(ClimatePlugin)
.add(CelestialPlugin)
.add(WindPlugin)
.add(TerrainPlugin)
.add(HydrologyPlugin)
.add(WaterPlugin)
.add(GroundCoverPlugin)
.add(DustPlugin)
.add(TrailsPlugin)
.add(PathsPlugin)
.add(WavesPlugin)
.add(FogPlugin)
.add(CloudPlugin)
.add(LightningPlugin)
.add(PrecipitationPlugin)
.add(WeatheredPlugin)
.add(RocksPlugin);
#[cfg(feature = "isomesh")]
let group = group.add(crate::voxel::VoxelPlugin);
group
}
}
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ElementsSystems {
Clock,
Weather,
Celestial,
Simulation,
Readback,
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect)]
#[require(Hdr)]
pub struct ElementsCamera {
pub auto_exposure: bool,
pub atmosphere: bool,
pub distance_fog: bool,
pub volumetric_fog: bool,
pub ambient: bool,
pub msaa_off: bool,
}
impl Default for ElementsCamera {
fn default() -> Self {
Self {
auto_exposure: true,
atmosphere: true,
distance_fog: true,
volumetric_fog: true,
ambient: true,
msaa_off: true,
}
}
}
impl ElementsCamera {
pub fn bare() -> Self {
Self {
auto_exposure: false,
atmosphere: false,
distance_fog: false,
volumetric_fog: false,
ambient: false,
msaa_off: false,
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
pub enum ElementsFocus {
Entity(Entity),
Point(Vec3),
}
#[derive(Resource, Default, Clone, Copy, Debug, PartialEq, Eq, Hash, Reflect)]
pub enum ElementsQuality {
Low,
Medium,
#[default]
High,
Ultra,
}
impl ElementsQuality {
pub fn wind_grid(self) -> UVec3 {
match self {
Self::Low => UVec3::new(32, 8, 32),
Self::Medium => UVec3::new(48, 12, 48),
Self::High => UVec3::new(64, 16, 64),
Self::Ultra => UVec3::new(96, 24, 96),
}
}
pub fn wind_pressure_iterations(self) -> u32 {
match self {
Self::Low | Self::Medium => 10,
Self::High => 12,
Self::Ultra => 40,
}
}
pub fn terrain_max_resolution(self) -> u32 {
match self {
Self::Low => 256,
Self::Medium | Self::High => 512,
Self::Ultra => 2048,
}
}
pub fn hydrology_substeps(self) -> u32 {
match self {
Self::Low => 2,
Self::Medium | Self::High => 4,
Self::Ultra => 16,
}
}
pub fn fog_steps(self) -> u32 {
match self {
Self::Low => 32,
Self::Medium => 48,
Self::High => 64,
Self::Ultra => 96,
}
}
pub fn precipitation_particles(self) -> u32 {
match self {
Self::Low => 20_000,
Self::Medium => 60_000,
Self::High => 150_000,
Self::Ultra => 300_000,
}
}
pub fn trail_window(self) -> (u32, f32) {
match self {
Self::Low => (512, 0.08),
Self::Medium => (1024, 0.06),
Self::High => (1024, 0.05),
Self::Ultra => (2048, 0.03),
}
}
pub fn wave_window(self) -> (u32, f32) {
match self {
Self::Low => (256, 0.08),
Self::Medium => (512, 0.06),
Self::High => (512, 0.05),
Self::Ultra => (1024, 0.04),
}
}
pub fn rock_range(self) -> f32 {
match self {
Self::Low => 60.0,
Self::Medium => 80.0,
Self::High => 100.0,
Self::Ultra => 140.0,
}
}
}
#[derive(Resource, Clone, Copy, Debug, PartialEq, Reflect)]
pub struct ElementsUnits {
pub units_per_metre: f32,
}
impl Default for ElementsUnits {
fn default() -> Self {
Self {
units_per_metre: 1.0,
}
}
}
impl ElementsUnits {
pub fn per_metre(&self) -> f32 {
let u = self.units_per_metre;
if u.is_finite() && u > 0.0 { u } else { 1.0 }
}
pub fn world(&self, metres: f32) -> f32 {
metres * self.per_metre()
}
pub fn metres(&self, world: f32) -> f32 {
world / self.per_metre()
}
pub fn point_to_metres(&self, world: Vec3) -> Vec3 {
world / self.per_metre()
}
pub fn point_to_world(&self, metres: Vec3) -> Vec3 {
metres * self.per_metre()
}
pub fn to_metres(&self, transform: &GlobalTransform) -> GlobalTransform {
GlobalTransform::from_scale(Vec3::splat(1.0 / self.per_metre())) * *transform
}
}
fn apply_units(
units: Res<ElementsUnits>,
mut atmospheres: Query<(&mut Transform, &Atmosphere), With<ElementsAtmosphere>>,
mut warned: Local<bool>,
) {
if !units.is_changed() {
return;
}
let u = units.units_per_metre;
if !(u.is_finite() && u > 0.0) && !*warned {
warn!("ElementsUnits::units_per_metre is {u}: reading it as 1.0");
*warned = true;
}
for (mut transform, atmosphere) in &mut atmospheres {
*transform = planet_transform(atmosphere, units.per_metre());
}
}
fn planet_transform(atmosphere: &Atmosphere, units: f32) -> Transform {
Transform::from_translation(Vec3::NEG_Y * (atmosphere.inner_radius * units))
.with_scale(Vec3::splat(units))
}
#[derive(Component, Clone, Copy, Debug)]
pub(crate) struct ElementsAtmosphere;
#[derive(Resource, Default, Clone, Copy, Debug)]
pub(crate) struct ElementsView {
pub entity: Option<Entity>,
pub position: Vec3,
pub eye: Vec3,
pub velocity: Vec3,
}
pub(crate) struct ElementsCorePlugin;
impl Plugin for ElementsCorePlugin {
fn build(&self, app: &mut App) {
clock_plugin(app);
app.configure_sets(
Update,
(
ElementsSystems::Clock,
ElementsSystems::Weather,
ElementsSystems::Celestial,
ElementsSystems::Simulation,
ElementsSystems::Readback,
)
.chain(),
)
.init_resource::<ElementsQuality>()
.init_resource::<ElementsUnits>()
.init_resource::<ElementsView>()
.init_resource::<EnvironmentSnapshot>()
.init_resource::<Weather>()
.init_resource::<SunState>()
.init_resource::<WindState>()
.add_systems(
Update,
(apply_units, follow_camera)
.chain()
.in_set(ElementsSystems::Clock),
);
}
}
pub(crate) struct ElementsGpuCorePlugin;
impl Plugin for ElementsGpuCorePlugin {
fn build(&self, app: &mut App) {
ensure_core(app);
load_shader_library!(app, "noise.wesl");
load_shader_library!(app, "grid.wesl");
load_shader_library!(app, "wind.wesl");
load_shader_library!(app, "surface_cover.wesl");
if let Some(mut buffers) = app
.world_mut()
.get_resource_mut::<Assets<bevy::render::storage::ShaderBuffer>>()
{
crate::terrain::insert_placeholder(&mut buffers);
}
load_shader_library!(app, "climate/snow.wesl");
load_shader_library!(app, "terrain/common.wesl");
load_shader_library!(app, "trails/surface.wesl");
load_shader_library!(app, "paths.wesl");
app.init_resource::<CameraNeeds>()
.add_plugins(ReadbackPlugin)
.add_observer(dress_camera)
.add_systems(Update, redress_cameras.in_set(ElementsSystems::Clock));
}
}
pub(crate) fn ensure_core(app: &mut App) {
if !app.is_plugin_added::<ElementsCorePlugin>() {
app.add_plugins(ElementsCorePlugin);
}
}
pub(crate) fn ensure_gpu_core(app: &mut App) {
if !app.is_plugin_added::<ElementsGpuCorePlugin>() {
app.add_plugins(ElementsGpuCorePlugin);
}
}
#[derive(Resource, Default, Clone, Copy, Debug)]
pub(crate) struct CameraNeeds {
pub depth_prepass: bool,
atmosphere_spawned: bool,
}
pub(crate) fn needs_depth_prepass(app: &mut App) {
app.world_mut()
.get_resource_or_init::<CameraNeeds>()
.depth_prepass = true;
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct ElementsDressing {
atmosphere: bool,
sky_background: bool,
distance_fog: bool,
volumetric_fog: bool,
ambient: bool,
msaa: bool,
depth_prepass: bool,
}
type DressParts = (
&'static ElementsCamera,
Option<&'static mut ElementsDressing>,
&'static mut Camera,
Has<AtmosphereSettings>,
Has<DistanceFog>,
Has<VolumetricFog>,
Has<AmbientLight>,
Has<DepthPrepass>,
);
#[derive(SystemParam)]
struct Dresser<'w, 's> {
commands: Commands<'w, 's>,
quality: Res<'w, ElementsQuality>,
units: Res<'w, ElementsUnits>,
needs: ResMut<'w, CameraNeeds>,
atmospheres: Query<'w, 's, (), With<Atmosphere>>,
media: ResMut<'w, Assets<ScatteringMedium>>,
cameras: Query<'w, 's, DressParts>,
}
fn toggle<B: Bundle>(
entity: &mut EntityCommands,
want: bool,
present: bool,
ours: &mut bool,
make: impl FnOnce() -> B,
) {
if want && !present {
entity.insert(make());
*ours = true;
} else if !want && *ours {
entity.remove::<B>();
*ours = false;
}
}
impl Dresser<'_, '_> {
fn dress(&mut self, entity: Entity) {
let fog_steps = self.quality.fog_steps();
let depth_prepass = self.needs.depth_prepass;
let Ok((want, done, mut camera, atmosphere, fog, volumetric, ambient, prepass)) =
self.cameras.get_mut(entity)
else {
return;
};
let want = *want;
let mut ours = done.as_deref().copied().unwrap_or_default();
let mut e = self.commands.entity(entity);
toggle(
&mut e,
want.atmosphere,
atmosphere,
&mut ours.atmosphere,
|| {
(
AtmosphereSettings::default(),
AtmosphereEnvironmentMapLight::default(),
)
},
);
toggle(
&mut e,
depth_prepass,
prepass,
&mut ours.depth_prepass,
|| DepthPrepass,
);
toggle(
&mut e,
want.volumetric_fog,
volumetric,
&mut ours.volumetric_fog,
|| VolumetricFog {
ambient_intensity: 0.0,
step_count: fog_steps,
..default()
},
);
toggle(
&mut e,
want.distance_fog,
fog,
&mut ours.distance_fog,
DistanceFog::default,
);
toggle(&mut e, want.ambient, ambient, &mut ours.ambient, || {
AmbientLight {
brightness: 0.0,
..default()
}
});
if want.msaa_off {
e.insert(Msaa::Off);
ours.msaa = true;
} else if ours.msaa {
e.insert(Msaa::default());
ours.msaa = false;
}
if want.atmosphere && matches!(camera.clear_color, ClearColorConfig::Default) {
camera.clear_color = ClearColorConfig::Custom(Color::BLACK);
e.insert(SkyBackground);
ours.sky_background = true;
} else if !want.atmosphere && ours.sky_background {
camera.clear_color = ClearColorConfig::Default;
e.remove::<SkyBackground>();
ours.sky_background = false;
}
match done {
Some(mut done) => {
if *done != ours {
*done = ours;
}
}
None => {
e.insert(ours);
}
}
if want.atmosphere && self.atmospheres.is_empty() && !self.needs.atmosphere_spawned {
let medium = self.media.add(ScatteringMedium::default());
let atmosphere = Atmosphere::earth(medium);
let transform = planet_transform(&atmosphere, self.units.per_metre());
self.commands.spawn((
Name::new("Elements atmosphere"),
atmosphere,
ElementsAtmosphere,
transform,
));
self.needs.atmosphere_spawned = true;
}
}
}
#[derive(Component, Clone, Copy, Debug, Default)]
pub(crate) struct SkyBackground;
fn dress_camera(add: On<Add<ElementsCamera>>, mut dresser: Dresser) {
dresser.dress(add.entity);
}
fn redress_cameras(changed: Query<Entity, Changed<ElementsCamera>>, mut dresser: Dresser) {
for entity in &changed {
dresser.dress(entity);
}
}
fn follow_camera(
time: Res<Time<Virtual>>,
cameras: Query<
(Entity, &Camera, &GlobalTransform, Option<&ElementsFocus>),
With<ElementsCamera>,
>,
transforms: Query<&GlobalTransform>,
units: Res<ElementsUnits>,
mut view: ResMut<ElementsView>,
mut warned: Local<bool>,
) {
let mut count = 0usize;
let mut best: Option<(Entity, isize, Vec3, Option<ElementsFocus>)> = None;
for (entity, camera, transform, focus) in &cameras {
if !camera.is_active {
continue;
}
count += 1;
if best.is_none_or(|(_, order, _, _)| camera.order < order) {
best = Some((
entity,
camera.order,
units.point_to_metres(transform.translation()),
focus.copied(),
));
}
}
if count > 1 && !*warned {
warn!(
"{count} active entities carry ElementsCamera; the environment follows the one with the lowest Camera::order"
);
*warned = true;
}
match best {
Some((entity, _, eye, focus)) => {
let dt = time.delta_secs();
view.velocity = if view.entity == Some(entity) && dt > 0.0 {
(eye - view.eye) / dt
} else {
Vec3::ZERO
};
view.entity = Some(entity);
view.eye = eye;
view.position = match focus {
Some(ElementsFocus::Point(p)) => units.point_to_metres(p),
Some(ElementsFocus::Entity(e)) => transforms
.get(e)
.map_or(eye, |t| units.point_to_metres(t.translation())),
None => eye,
};
}
None => {
view.entity = None;
view.velocity = Vec3::ZERO;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_inactive_camera_is_not_followed() {
let mut app = App::new();
app.add_plugins((MinimalPlugins, ElementsCorePlugin));
app.world_mut().spawn((
Camera {
order: -1,
is_active: false,
..default()
},
ElementsCamera::default(),
GlobalTransform::from_xyz(100.0, 0.0, 0.0),
));
app.world_mut().spawn((
Camera::default(),
ElementsCamera::default(),
GlobalTransform::from_xyz(5.0, 2.0, 0.0),
));
app.update();
assert_eq!(
app.world().resource::<ElementsView>().position,
Vec3::new(5.0, 2.0, 0.0)
);
}
#[test]
fn the_view_centres_on_the_focus() {
let mut app = App::new();
app.add_plugins((MinimalPlugins, ElementsCorePlugin));
let eye = Vec3::new(0.0, 860.0, 1229.0);
let player = app
.world_mut()
.spawn(GlobalTransform::from_xyz(3.0, 0.5, -4.0))
.id();
let bare = app.world_mut().spawn_empty().id();
let camera = app
.world_mut()
.spawn((
Camera::default(),
ElementsCamera::default(),
GlobalTransform::from_translation(eye),
ElementsFocus::Point(Vec3::new(1.0, 0.0, 2.0)),
))
.id();
for (focus, expected) in [
(
ElementsFocus::Point(Vec3::new(1.0, 0.0, 2.0)),
Vec3::new(1.0, 0.0, 2.0),
),
(ElementsFocus::Entity(player), Vec3::new(3.0, 0.5, -4.0)),
(ElementsFocus::Entity(bare), eye),
] {
app.world_mut().entity_mut(camera).insert(focus);
app.update();
let view = app.world().resource::<ElementsView>();
assert_eq!((view.position, view.eye), (expected, eye), "{focus:?}");
}
app.world_mut().entity_mut(camera).remove::<ElementsFocus>();
app.update();
assert_eq!(app.world().resource::<ElementsView>().position, eye);
}
#[test]
fn the_crate_atmosphere_centres_the_planet_below_the_origin() {
let mut app = App::new();
app.add_plugins((
MinimalPlugins,
bevy::transform::TransformPlugin,
ElementsCorePlugin,
))
.init_resource::<CameraNeeds>()
.insert_resource(Assets::<ScatteringMedium>::default())
.add_observer(dress_camera);
app.world_mut()
.spawn((Camera3d::default(), ElementsCamera::default()));
let centre = |app: &mut App| {
let mut atmospheres = app
.world_mut()
.query_filtered::<(&GlobalTransform, &Atmosphere), With<ElementsAtmosphere>>();
let (global, atmosphere) = atmospheres.single(app.world()).map_or_else(
|e| panic!("one crate atmosphere: {e}"),
|(g, a)| (*g, a.inner_radius),
);
let (scale, _, translation) = global.to_scale_rotation_translation();
(translation, scale, atmosphere)
};
for u in [1.0_f32, 3.2808] {
app.insert_resource(ElementsUnits { units_per_metre: u });
for _ in 0..3 {
app.update();
}
let (translation, scale, radius) = centre(&mut app);
assert_eq!(translation, Vec3::NEG_Y * (radius * u), "units {u}");
assert_eq!(scale, Vec3::splat(u), "units {u}");
}
}
}