1mod pan_orbit_camera_custom_input_plugin;
11
12use std::time::Duration;
13
14use bevy::camera::Hdr;
15use bevy::camera_controller::pan_orbit_camera::{
16 extensions::{dolly_zoom::DollyZoomTrigger, look_to::LookToTrigger},
17 prelude::*,
18};
19use bevy::math::DVec3;
20use bevy::{
21 anti_alias::smaa::Smaa, camera::primitives::Aabb, core_pipeline::tonemapping::Tonemapping,
22 pbr::ScreenSpaceAmbientOcclusion, platform::time::Instant, post_process::bloom::Bloom,
23 prelude::*, window::RequestRedraw,
24};
25
26use crate::pan_orbit_camera_custom_input_plugin::CustomInputPlugin;
27fn main() {
28 App::new()
29 .add_plugins((
30 DefaultPlugins,
31 MeshPickingPlugin,
32 DefaultPanOrbitCameraPlugins,
33 CustomInputPlugin,
35 ))
36 .insert_resource(GlobalAmbientLight::NONE)
39 .add_systems(Startup, setup)
40 .add_systems(
41 Update,
42 (
43 toggle_projection,
44 toggle_constraint,
45 explode,
46 switch_direction,
47 toggle_zoom,
48 )
49 .chain(),
50 )
51 .run();
52}
53
54fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
55 let diffuse_map = asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2");
61 let specular_map = asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2");
62 let intensity = 5000.0;
63
64 let scene = asset_server.load(get_web_asset_url("PlaneEngine/scene.gltf#Scene0"));
65
66 commands.spawn((
67 WorldAssetRoot(scene),
68 Transform::from_scale(Vec3::splat(2.0)),
69 ));
70
71 let cam_trans = Transform::from_xyz(2.0, 2.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y);
72 let camera = commands
73 .spawn((
74 Camera3d::default(),
75 Hdr,
76 Camera::default(),
77 cam_trans,
78 Tonemapping::AcesFitted,
79 Bloom::default(),
80 EnvironmentMapLight {
81 intensity,
82 diffuse_map: diffuse_map.clone(),
83 specular_map: specular_map.clone(),
84 ..Default::default()
85 },
86 PanOrbitCamera {
87 orbit_constraint: OrbitConstraint::Free,
88 last_anchor_depth: -cam_trans.translation.length() as f64,
89 orthographic: projections::OrthographicSettings {
90 scale_to_near_clip: 1_000_f32, ..Default::default()
92 },
93 ..Default::default()
94 },
95 ScreenSpaceAmbientOcclusion::default(),
96 Smaa::default(),
97 Msaa::Off,
98 ))
99 .id();
100
101 setup_ui(commands, camera);
102}
103
104fn toggle_projection(
105 keys: Res<ButtonInput<KeyCode>>,
106 mut dolly: MessageWriter<DollyZoomTrigger>,
107 cam: Query<Entity, With<PanOrbitCamera>>,
108 mut toggled: Local<bool>,
109) {
110 if keys.just_pressed(KeyCode::KeyP) {
111 *toggled = !*toggled;
112 let target_projection = if *toggled {
113 Projection::Orthographic(OrthographicProjection::default_3d())
114 } else {
115 Projection::Perspective(PerspectiveProjection::default())
116 };
117 dolly.write(DollyZoomTrigger {
118 target_projection,
119 camera: cam.single().unwrap(),
120 });
121 }
122}
123
124#[derive(Component)]
125struct ZoomStatusIndicator;
126
127fn zoom_status(zoom_through: bool) -> String {
128 format!("Zoom Through: {zoom_through}")
129}
130
131fn toggle_zoom(
132 keys: Res<ButtonInput<KeyCode>>,
133 mut cam: Query<&mut PanOrbitCamera>,
134 mut text: Query<&mut Text, With<ZoomStatusIndicator>>,
135) {
136 if keys.just_pressed(KeyCode::KeyZ) {
137 let mut editor = cam.single_mut().unwrap();
138 editor.zoom_limits.zoom_through_objects = !editor.zoom_limits.zoom_through_objects;
139 text.single_mut().unwrap().0 = zoom_status(editor.zoom_limits.zoom_through_objects);
140 }
141}
142
143fn toggle_constraint(
144 keys: Res<ButtonInput<KeyCode>>,
145 mut cam: Query<(Entity, &Transform, &mut PanOrbitCamera)>,
146 mut look_to: MessageWriter<LookToTrigger>,
147) {
148 if keys.just_pressed(KeyCode::KeyC) {
149 let (entity, transform, mut editor) = cam.single_mut().unwrap();
150 match editor.orbit_constraint {
151 OrbitConstraint::Fixed { .. } => editor.orbit_constraint = OrbitConstraint::Free,
152 OrbitConstraint::Free => {
153 editor.orbit_constraint = OrbitConstraint::Fixed {
154 up: DVec3::Y,
155 can_pass_tdc: false,
156 };
157
158 look_to.write(LookToTrigger::auto_snap_up_direction(
159 transform.forward().as_dvec3(),
160 entity,
161 &transform.rotation.as_dquat(),
162 editor.as_ref(),
163 ));
164 }
165 };
166 }
167}
168
169fn switch_direction(
170 keys: Res<ButtonInput<KeyCode>>,
171 mut look_to: MessageWriter<LookToTrigger>,
172 cam: Query<(Entity, &Transform, &PanOrbitCamera)>,
173) {
174 let (camera, transform, editor) = cam.single().unwrap();
175 if keys.just_pressed(KeyCode::Digit1) {
176 look_to.write(LookToTrigger::auto_snap_up_direction(
177 DVec3::X,
178 camera,
179 &transform.rotation.as_dquat(),
180 editor,
181 ));
182 }
183 if keys.just_pressed(KeyCode::Digit2) {
184 look_to.write(LookToTrigger::auto_snap_up_direction(
185 DVec3::Z,
186 camera,
187 &transform.rotation.as_dquat(),
188 editor,
189 ));
190 }
191 if keys.just_pressed(KeyCode::Digit3) {
192 look_to.write(LookToTrigger::auto_snap_up_direction(
193 DVec3::NEG_X,
194 camera,
195 &transform.rotation.as_dquat(),
196 editor,
197 ));
198 }
199 if keys.just_pressed(KeyCode::Digit4) {
200 look_to.write(LookToTrigger::auto_snap_up_direction(
201 DVec3::NEG_Z,
202 camera,
203 &transform.rotation.as_dquat(),
204 editor,
205 ));
206 }
207 if keys.just_pressed(KeyCode::Digit5) {
208 look_to.write(LookToTrigger::auto_snap_up_direction(
209 DVec3::Y,
210 camera,
211 &transform.rotation.as_dquat(),
212 editor,
213 ));
214 }
215 if keys.just_pressed(KeyCode::Digit6) {
216 look_to.write(LookToTrigger::auto_snap_up_direction(
217 DVec3::NEG_Y,
218 camera,
219 &transform.rotation.as_dquat(),
220 editor,
221 ));
222 }
223}
224
225fn setup_ui(mut commands: Commands, camera: Entity) {
226 let text = "\
227Left Mouse - Pan
228Right Mouse - Orbit
229Scroll - Zoom
230Z - Toggle passing thought surface on minimal zoom
231P - Toggle projection
232C - Toggle orbit constraint
233E - Toggle explode
2341-6 - Switch direction";
235 commands.spawn((
236 children![
237 Text::new(text),
238 (Text::new(zoom_status(false)), ZoomStatusIndicator),
239 ],
240 TextFont {
241 font_size: FontSize::Px(20.0),
242 ..default()
243 },
244 Node {
245 margin: UiRect::all(Val::Px(20.0)),
246 flex_direction: FlexDirection::Column,
247 ..Default::default()
248 },
249 Pickable::IGNORE,
250 UiTargetCamera(camera),
251 ));
252}
253
254#[derive(Component)]
255struct StartPos(f32);
256
257fn explode(
258 mut commands: Commands,
259 keys: Res<ButtonInput<KeyCode>>,
260 mut toggle: Local<Option<(bool, Instant, f32)>>,
261 mut explode_amount: Local<f32>,
262 mut redraw: MessageWriter<RequestRedraw>,
263 mut parts: Query<(Entity, &mut Transform, &Aabb, Option<&StartPos>), With<Mesh3d>>,
264 mut matls: ResMut<Assets<StandardMaterial>>,
265) {
266 let animation = Duration::from_millis(2000);
267 if keys.just_pressed(KeyCode::KeyE) {
268 let new = if let Some((last, ..)) = *toggle {
269 !last
270 } else {
271 true
272 };
273 *toggle = Some((new, Instant::now(), *explode_amount));
274 }
275 if let Some((toggled, start, start_amount)) = *toggle {
276 let goal_amount = toggled as usize as f32;
277 let t = (start.elapsed().as_secs_f32() / animation.as_secs_f32()).clamp(0.0, 1.0);
278 let progress = CubicSegment::new_bezier_easing((0.25, 0.1), (0.25, 1.0)).ease(t);
279 *explode_amount = start_amount + (goal_amount - start_amount) * progress;
280 for (part, mut transform, aabb, start) in &mut parts {
281 let start = if let Some(start) = start {
282 start.0
283 } else {
284 let start = aabb.max().y;
285 commands.entity(part).insert(StartPos(start));
286 start
287 };
288 transform.translation.y = *explode_amount * (start) * 2.0;
289 }
290 if t < 1.0 {
291 redraw.write(RequestRedraw);
292 }
293 }
294 for (_, matl) in matls.iter_mut() {
295 matl.perceptual_roughness = matl.perceptual_roughness.clamp(0.3, 1.0);
296 }
297}
298
299fn get_web_asset_url(name: &str) -> String {
305 format!(
306 "https://raw.githubusercontent.com/bevyengine/bevy_asset_files/refs/heads/main/{}",
307 name
308 )
309}