use std::{f32::consts::FRAC_PI_2, iter};
use avian_pickup::prelude::*;
use avian3d::prelude::*;
use bevy::{
core_pipeline::{Skybox, bloom::Bloom, tonemapping::Tonemapping},
pbr::NotShadowCaster,
prelude::*,
render::{
camera::Exposure,
view::{NoFrustumCulling, RenderLayers},
},
scene::SceneInstanceReady,
window::CursorGrabMode,
};
#[cfg(feature = "native")]
use bevy::{
core_pipeline::{experimental::taa::TemporalAntiAliasing, prepass::NormalPrepass},
pbr::{ScreenSpaceAmbientOcclusion, ShadowFilteringMethod},
};
use bevy_enhanced_input::prelude::*;
#[cfg(feature = "hot_patch")]
use bevy_simple_subsecond_system::hot;
use crate::{
CameraOrder, PostPhysicsAppSystems, RenderLayer,
gameplay::{
animation::{AnimationPlayerAncestor, AnimationPlayerOf, AnimationPlayers},
level::LevelAssets,
},
screens::{Screen, loading::LoadingScreen},
third_party::{avian3d::CollisionLayer, bevy_trenchbroom::LoadTrenchbroomModel as _},
};
use super::{PLAYER_FLOAT_HEIGHT, Player, default_input::Rotate};
pub(super) fn plugin(app: &mut App) {
app.add_observer(spawn_view_model);
app.add_observer(add_render_layers_to_point_light);
app.add_observer(add_render_layers_to_spot_light);
app.add_observer(add_render_layers_to_directional_light);
app.add_observer(rotate_camera_yaw_and_pitch);
app.add_systems(
Update,
sync_camera_translation_with_player
.run_if(in_state(Screen::Gameplay))
.in_set(PostPhysicsAppSystems::Update),
);
app.register_type::<PlayerCamera>();
}
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
#[require(Transform, Visibility)]
pub(crate) struct PlayerCamera;
#[cfg_attr(feature = "hot_patch", hot)]
fn spawn_view_model(
trigger: Trigger<OnAdd, Player>,
player_transform: Query<&Transform>,
mut commands: Commands,
assets: Res<AssetServer>,
level_assets: Res<LevelAssets>,
) {
let player_transform = player_transform.get(trigger.target()).unwrap();
let env_map = EnvironmentMapLight {
diffuse_map: level_assets.env_map_diffuse.clone(),
specular_map: level_assets.env_map_specular.clone(),
intensity: 300.0,
..default()
};
commands
.spawn((
Name::new("Player Camera Parent"),
PlayerCamera,
*player_transform,
StateScoped(Screen::Gameplay),
StateScoped(LoadingScreen::Shaders),
AvianPickupActor {
prop_filter: SpatialQueryFilter::from_mask(CollisionLayer::Prop),
obstacle_filter: SpatialQueryFilter::from_mask(CollisionLayer::Default),
actor_filter: SpatialQueryFilter::from_mask(CollisionLayer::Character),
interaction_distance: 2.0,
pull: AvianPickupActorPullConfig {
impulse: 20.0,
max_prop_mass: 10_000.0,
},
hold: AvianPickupActorHoldConfig {
distance_to_allow_holding: 2.0,
linear_velocity_easing: 0.7,
..default()
},
..default()
},
AnimationPlayerAncestor,
SpatialListener::new(0.4),
))
.with_children(|parent| {
parent.spawn((
Name::new("World Model Camera"),
Camera3d::default(),
Camera {
order: CameraOrder::World.into(),
hdr: true,
clear_color: Color::srgb_u8(15, 9, 20).into(),
..default()
},
Projection::from(PerspectiveProjection {
fov: 75.0_f32.to_radians(),
..default()
}),
RenderLayers::from(
RenderLayer::DEFAULT | RenderLayer::PARTICLES | RenderLayer::GIZMO3,
),
Exposure::INDOOR,
Tonemapping::TonyMcMapface,
Bloom::NATURAL,
Skybox {
image: level_assets.env_map_specular.clone(),
brightness: 8.0,
..default()
},
env_map.clone(),
#[cfg(feature = "native")]
(
Msaa::Off,
ScreenSpaceAmbientOcclusion::default(),
TemporalAntiAliasing::default(),
NormalPrepass,
ShadowFilteringMethod::Temporal,
),
));
parent.spawn((
Name::new("View Model Camera"),
Camera3d::default(),
Camera {
order: CameraOrder::ViewModel.into(),
hdr: true,
..default()
},
Projection::from(PerspectiveProjection {
fov: 62.0_f32.to_radians(),
..default()
}),
RenderLayers::from(RenderLayer::VIEW_MODEL),
Exposure::INDOOR,
Tonemapping::TonyMcMapface,
env_map,
));
parent
.spawn((
Name::new("View Model"),
SceneRoot(assets.load_trenchbroom_model::<Player>()),
))
.observe(configure_player_view_model);
})
.observe(move_anim_players_relationship_to_player);
}
#[cfg_attr(feature = "hot_patch", hot)]
fn move_anim_players_relationship_to_player(
trigger: Trigger<OnAdd, AnimationPlayers>,
q_anim_player: Query<&AnimationPlayers>,
player: Single<Entity, With<Player>>,
mut commands: Commands,
) {
let anim_players = q_anim_player.get(trigger.target()).unwrap();
for anim_player in anim_players.iter() {
commands
.entity(anim_player)
.insert(AnimationPlayerOf(*player));
}
}
#[cfg_attr(feature = "hot_patch", hot)]
fn configure_player_view_model(
trigger: Trigger<SceneInstanceReady>,
mut commands: Commands,
q_children: Query<&Children>,
q_mesh: Query<(), With<Mesh3d>>,
) {
let view_model = trigger.target();
for child in iter::once(view_model)
.chain(q_children.iter_descendants(view_model))
.filter(|e| q_mesh.contains(*e))
{
commands.entity(child).insert((
RenderLayers::from(RenderLayer::VIEW_MODEL),
NotShadowCaster,
NoFrustumCulling,
));
}
}
#[cfg_attr(feature = "hot_patch", hot)]
fn rotate_camera_yaw_and_pitch(
trigger: Trigger<Fired<Rotate>>,
mut transform: Single<&mut Transform, With<PlayerCamera>>,
window: Single<&Window>,
) {
if window.cursor_options.grab_mode == CursorGrabMode::None {
return;
}
let delta = trigger.value;
if delta != Vec2::ZERO {
let delta_yaw = delta.x;
let delta_pitch = delta.y;
let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);
let yaw = yaw + delta_yaw;
const PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
}
}
#[cfg_attr(feature = "hot_patch", hot)]
fn sync_camera_translation_with_player(
mut player_camera_parent: Single<&mut Transform, With<PlayerCamera>>,
player: Single<&Transform, (With<Player>, Without<PlayerCamera>)>,
) {
let camera_height = 1.84;
player_camera_parent.translation =
player.translation + Vec3::Y * (camera_height - PLAYER_FLOAT_HEIGHT);
}
#[cfg_attr(feature = "hot_patch", hot)]
fn add_render_layers_to_point_light(trigger: Trigger<OnAdd, PointLight>, mut commands: Commands) {
let entity = trigger.target();
commands.entity(entity).insert(RenderLayers::from(
RenderLayer::DEFAULT | RenderLayer::VIEW_MODEL,
));
}
#[cfg_attr(feature = "hot_patch", hot)]
fn add_render_layers_to_spot_light(trigger: Trigger<OnAdd, SpotLight>, mut commands: Commands) {
let entity = trigger.target();
commands.entity(entity).insert(RenderLayers::from(
RenderLayer::DEFAULT | RenderLayer::VIEW_MODEL,
));
}
#[cfg_attr(feature = "hot_patch", hot)]
fn add_render_layers_to_directional_light(
trigger: Trigger<OnAdd, DirectionalLight>,
mut commands: Commands,
) {
let entity = trigger.target();
commands.entity(entity).insert(RenderLayers::from(
RenderLayer::DEFAULT | RenderLayer::VIEW_MODEL,
));
}