diff --git a/Cargo.lock b/Cargo.lock index 8bc67a8..5bfa7b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -713,6 +713,22 @@ dependencies = [ "wgpu-types", ] +[[package]] +name = "bevy_camera_controller" +version = "0.19.0-dev" +source = "git+https://github.com/bevyengine/bevy?branch=main#26ee545b87cdfa36aeb4ca83738240063e691a07" +dependencies = [ + "bevy_app", + "bevy_camera", + "bevy_ecs", + "bevy_input", + "bevy_log", + "bevy_math", + "bevy_time", + "bevy_transform", + "bevy_window", +] + [[package]] name = "bevy_color" version = "0.19.0-dev" @@ -1067,6 +1083,7 @@ dependencies = [ "bevy_asset", "bevy_audio", "bevy_camera", + "bevy_camera_controller", "bevy_color", "bevy_core_pipeline", "bevy_derive", diff --git a/Cargo.toml b/Cargo.toml index 257bbee..94ff5c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ type_complexity = "allow" too_many_arguments = "allow" [workspace.dependencies] -bevy = { git = "https://github.com/bevyengine/bevy", branch = "main", features = ["file_watcher", "shader_format_wesl"] } +bevy = { git = "https://github.com/bevyengine/bevy", branch = "main", features = ["file_watcher", "shader_format_wesl", "free_camera", "pan_camera"] } bevy_naga_reflect = { git = "https://github.com/tychedelia/bevy_naga_reflect" } naga = { version = "28", features = ["wgsl-in"] } wesl = { version = "0.3", default-features = false } @@ -148,6 +148,10 @@ path = "examples/shapes.rs" name = "blend_modes" path = "examples/blend_modes.rs" +[[example]] +name = "camera_controllers" +path = "examples/camera_controllers.rs" + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/crates/processing_glfw/src/lib.rs b/crates/processing_glfw/src/lib.rs index 635a1d3..740762b 100644 --- a/crates/processing_glfw/src/lib.rs +++ b/crates/processing_glfw/src/lib.rs @@ -4,8 +4,9 @@ use bevy::prelude::Entity; use glfw::{Action, Glfw, GlfwReceiver, PWindow, WindowEvent, WindowMode}; use processing_core::error::Result; use processing_input::{ - input_flush, input_set_char, input_set_cursor_enter, input_set_cursor_leave, input_set_focus, - input_set_key, input_set_mouse_button, input_set_mouse_move, input_set_scroll, + input_cursor_grab_mode, input_cursor_visible, input_flush, input_set_char, + input_set_cursor_enter, input_set_cursor_leave, input_set_focus, input_set_key, + input_set_mouse_button, input_set_mouse_move, input_set_scroll, }; pub struct GlfwContext { @@ -173,10 +174,30 @@ impl GlfwContext { return false; } - input_flush().unwrap(); + let Ok(_) = input_flush() else { + return false; + }; + self.sync_cursor(surface); true } + + fn sync_cursor(&mut self, surface: Entity) { + use bevy::window::CursorGrabMode; + + let grab = input_cursor_grab_mode(surface).unwrap_or(CursorGrabMode::None); + let visible = input_cursor_visible(surface).unwrap_or(true); + + let mode = match grab { + CursorGrabMode::Locked | CursorGrabMode::Confined => glfw::CursorMode::Disabled, + CursorGrabMode::None if !visible => glfw::CursorMode::Hidden, + CursorGrabMode::None => glfw::CursorMode::Normal, + }; + + if self.window.get_cursor_mode() != mode { + self.window.set_cursor_mode(mode); + } + } } fn glfw_button_to_bevy(button: glfw::MouseButton) -> Option { diff --git a/crates/processing_input/src/lib.rs b/crates/processing_input/src/lib.rs index 997a483..38f74e8 100644 --- a/crates/processing_input/src/lib.rs +++ b/crates/processing_input/src/lib.rs @@ -152,9 +152,38 @@ pub fn input_set_focus(surface: Entity, focused: bool) -> error::Result<()> { }) } +pub fn input_cursor_grab_mode(surface: Entity) -> error::Result { + app_mut(|app| { + let cursor = app + .world() + .get::(surface) + .map(|c| c.grab_mode) + .unwrap_or(bevy::window::CursorGrabMode::None); + Ok(cursor) + }) +} + +pub fn input_cursor_visible(surface: Entity) -> error::Result { + app_mut(|app| { + let visible = app + .world() + .get::(surface) + .map(|c| c.visible) + .unwrap_or(true); + Ok(visible) + }) +} + +/// Flushes the input state by running the relevant schedules. This is required to ensure that +/// Bevy's bookkeeping of input state is up to date after manually sending input events. +/// It should be called after sending any input events and before querying input state +/// to ensure that the state reflects the events that were sent. pub fn input_flush() -> error::Result<()> { app_mut(|app| { - app.world_mut().run_schedule(PreUpdate); + let world = app.world_mut(); + world.run_schedule(First); + world.run_schedule(PreUpdate); + world.run_schedule(RunFixedMainLoop); Ok(()) }) } diff --git a/crates/processing_pyo3/assets b/crates/processing_pyo3/assets new file mode 120000 index 0000000..41aef43 --- /dev/null +++ b/crates/processing_pyo3/assets @@ -0,0 +1 @@ +../../assets \ No newline at end of file diff --git a/crates/processing_pyo3/examples/assets b/crates/processing_pyo3/examples/assets deleted file mode 120000 index 2978ef3..0000000 --- a/crates/processing_pyo3/examples/assets +++ /dev/null @@ -1 +0,0 @@ -../../../assets \ No newline at end of file diff --git a/crates/processing_pyo3/examples/camera_controllers.py b/crates/processing_pyo3/examples/camera_controllers.py new file mode 100644 index 0000000..7feae0f --- /dev/null +++ b/crates/processing_pyo3/examples/camera_controllers.py @@ -0,0 +1,46 @@ +from mewnala import * + +angle = 0.0 +mode = 0 + +def setup(): + size(800, 600) + mode_3d() + orbit_camera() + + dir_light = create_directional_light((1.0, 0.98, 0.95), 1500.0) + dir_light.position(300.0, 400.0, 300.0) + dir_light.look_at(0.0, 0.0, 0.0) + +def draw(): + global angle, mode + + if key_just_pressed(KEY_1): + mode_3d() + orbit_camera() + mode = 0 + if key_just_pressed(KEY_2): + mode_3d() + free_camera() + mode = 1 + if key_just_pressed(KEY_3): + mode_2d() + pan_camera() + mode = 2 + + background(13, 13, 18) + if mode < 2: + fill(255, 217, 145) + roughness(0.3) + metallic(0.8) + push_matrix() + rotate(angle) + box(100.0, 100.0, 100.0) + pop_matrix() + else: + fill(204, 77, 51) + rect(300.0, 200.0, 200.0, 200.0) + + angle += 0.02 + +run() diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index dfa8753..1107fde 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -1038,6 +1038,52 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + pub fn orbit_camera(&self) -> PyResult<()> { + graphics_orbit_camera(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn free_camera(&self) -> PyResult<()> { + graphics_free_camera(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn pan_camera(&self) -> PyResult<()> { + graphics_pan_camera(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn disable_camera(&self) -> PyResult<()> { + graphics_disable_camera_controller(self.entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn camera_distance(&self, distance: f32) -> PyResult<()> { + camera_set_distance(self.entity, distance) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + #[pyo3(signature = (*args))] + pub fn camera_center(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + let v = extract_vec3(args)?; + camera_set_center(self.entity, v).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn camera_min_distance(&self, min: f32) -> PyResult<()> { + camera_set_min_distance(self.entity, min) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn camera_max_distance(&self, max: f32) -> PyResult<()> { + camera_set_max_distance(self.entity, max) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn camera_speed(&self, speed: f32) -> PyResult<()> { + camera_set_speed(self.entity, speed).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn camera_reset(&self) -> PyResult<()> { + camera_reset(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn light_directional( &self, color: crate::color::ColorLike, diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 82eefaf..ffd56bb 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -739,6 +739,12 @@ mod mewnala { graphics!(module).mode_3d() } + #[pyfunction] + #[pyo3(pass_module)] + fn mode_2d(module: &Bound<'_, PyModule>) -> PyResult<()> { + graphics!(module).mode_2d() + } + #[pyfunction] #[pyo3(pass_module, signature = (*args))] fn camera_position(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { @@ -751,6 +757,66 @@ mod mewnala { graphics!(module).camera_look_at(args) } + #[pyfunction] + #[pyo3(pass_module)] + fn orbit_camera(module: &Bound<'_, PyModule>) -> PyResult<()> { + graphics!(module).orbit_camera() + } + + #[pyfunction] + #[pyo3(pass_module)] + fn free_camera(module: &Bound<'_, PyModule>) -> PyResult<()> { + graphics!(module).free_camera() + } + + #[pyfunction] + #[pyo3(pass_module)] + fn pan_camera(module: &Bound<'_, PyModule>) -> PyResult<()> { + graphics!(module).pan_camera() + } + + #[pyfunction] + #[pyo3(pass_module)] + fn disable_camera(module: &Bound<'_, PyModule>) -> PyResult<()> { + graphics!(module).disable_camera() + } + + #[pyfunction] + #[pyo3(pass_module)] + fn camera_distance(module: &Bound<'_, PyModule>, distance: f32) -> PyResult<()> { + graphics!(module).camera_distance(distance) + } + + #[pyfunction] + #[pyo3(pass_module, signature = (*args))] + fn camera_center(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { + graphics!(module).camera_center(args) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn camera_min_distance(module: &Bound<'_, PyModule>, min: f32) -> PyResult<()> { + graphics!(module).camera_min_distance(min) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn camera_max_distance(module: &Bound<'_, PyModule>, max: f32) -> PyResult<()> { + graphics!(module).camera_max_distance(max) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn camera_speed(module: &Bound<'_, PyModule>, speed: f32) -> PyResult<()> { + graphics!(module).camera_speed(speed) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn camera_reset(module: &Bound<'_, PyModule>) -> PyResult<()> { + graphics!(module).camera_reset() + } + #[pyfunction] #[pyo3(pass_module)] fn push_matrix(module: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/crates/processing_render/src/camera.rs b/crates/processing_render/src/camera.rs new file mode 100644 index 0000000..34e589f --- /dev/null +++ b/crates/processing_render/src/camera.rs @@ -0,0 +1,298 @@ +//! Supplies the following camera controllers: +//! - Orbit camera: left mouse button to orbit, middle mouse button to pan, right mouse button or scroll wheel to zoom. Inspired by [PeasyCam](https://github.com/jdf/peasycam) by Jonathan Feinberg. +//! - Free camera: WASD to move, mouse to look around. +//! - Pan camera: middle mouse button to pan, scroll wheel to zoom. +use std::f32::consts::FRAC_PI_2; + +use bevy::camera_controller::free_camera::{FreeCamera, FreeCameraState}; +use bevy::camera_controller::pan_camera::PanCamera; +use bevy::input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButton}; +use bevy::prelude::*; + +#[derive(Clone, Copy, Default)] +pub enum RotationMode { + #[default] + SuppressRoll, + YawOnly, +} + +#[derive(Component)] +pub struct OrbitCamera { + pub center: Vec3, + pub distance: f32, + pub yaw: f32, + pub pitch: f32, + pub min_distance: f32, + pub max_distance: f32, + pub orbit_sensitivity: f32, + pub pan_sensitivity: f32, + pub zoom_sensitivity: f32, + pub rotation_mode: RotationMode, + pub initial_center: Vec3, + pub initial_distance: f32, + pub initial_yaw: f32, + pub initial_pitch: f32, +} + +impl OrbitCamera { + pub fn new(center: Vec3, distance: f32) -> Self { + Self { + center, + distance, + yaw: 0.0, + pitch: 0.0, + min_distance: 1.0, + max_distance: f32::MAX, + orbit_sensitivity: 0.005, + pan_sensitivity: 1.0, + zoom_sensitivity: 0.1, + rotation_mode: RotationMode::default(), + initial_center: center, + initial_distance: distance, + initial_yaw: 0.0, + initial_pitch: 0.0, + } + } + + fn rotation(&self) -> Quat { + Quat::from_rotation_y(-self.yaw) * Quat::from_rotation_x(-self.pitch) + } + + fn apply_to_transform(&self, transform: &mut Transform) { + let rotation = self.rotation(); + transform.translation = self.center + rotation * Vec3::new(0.0, 0.0, self.distance); + transform.look_at(self.center, Vec3::Y); + } + + pub fn reset(&mut self) { + self.center = self.initial_center; + self.distance = self.initial_distance; + self.yaw = self.initial_yaw; + self.pitch = self.initial_pitch; + } +} + +pub struct OrbitCameraPlugin; + +impl Plugin for OrbitCameraPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + RunFixedMainLoop, + update_orbit_camera.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop), + ); + } +} + +fn update_orbit_camera( + mouse_motion: Res, + mouse_scroll: Res, + mouse_buttons: Res>, + mut query: Query<(&mut Transform, &mut OrbitCamera)>, +) { + let left = mouse_buttons.pressed(MouseButton::Left); + let middle = mouse_buttons.pressed(MouseButton::Middle); + let right = mouse_buttons.pressed(MouseButton::Right); + let delta = mouse_motion.delta; + let scroll = mouse_scroll.delta.y; + + for (mut transform, mut orbit) in query.iter_mut() { + let mut changed = false; + + if left && delta != Vec2::ZERO { + orbit.yaw += delta.x * orbit.orbit_sensitivity; + if matches!(orbit.rotation_mode, RotationMode::SuppressRoll) { + let limit = FRAC_PI_2 - 0.01; + orbit.pitch = + (orbit.pitch + delta.y * orbit.orbit_sensitivity).clamp(-limit, limit); + } + changed = true; + } + + if middle && delta != Vec2::ZERO { + let pan_speed = orbit.pan_sensitivity * orbit.distance * 0.001; + orbit.center -= transform.right() * delta.x * pan_speed; + orbit.center += transform.up() * delta.y * pan_speed; + changed = true; + } + + if right && delta != Vec2::ZERO { + orbit.distance *= 1.0 + delta.y * orbit.zoom_sensitivity; + orbit.distance = orbit.distance.clamp(orbit.min_distance, orbit.max_distance); + changed = true; + } + + if scroll != 0.0 { + orbit.distance *= 1.0 - scroll * orbit.zoom_sensitivity; + orbit.distance = orbit.distance.clamp(orbit.min_distance, orbit.max_distance); + changed = true; + } + + if changed { + orbit.apply_to_transform(&mut transform); + } + } +} + +/// Enables an orbit camera on the specified entity. +pub fn enable_orbit_camera( + In(entity): In, + mut commands: Commands, + transforms: Query<&Transform>, + sizes: Query<&crate::graphics::SurfaceSize>, +) -> crate::error::Result<()> { + let center = Vec3::ZERO; + let distance = if let Ok(transform) = transforms.get(entity) { + transform.translation.distance(center) + } else if let Ok(crate::graphics::SurfaceSize(_, height)) = sizes.get(entity) { + let fov = std::f32::consts::PI / 3.0; + (*height as f32 / 2.0) / (fov / 2.0).tan() + } else { + 400.0 + }; + + commands + .entity(entity) + .remove::() + .remove::() + .insert(OrbitCamera::new(center, distance)); + Ok(()) +} + +/// Enables a free camera on the specified entity. +pub fn enable_free_camera( + In(entity): In, + mut commands: Commands, + sizes: Query<&crate::graphics::SurfaceSize>, +) -> crate::error::Result<()> { + // processing uses pixel-scale coordinates + let speed = if let Ok(crate::graphics::SurfaceSize(_, height)) = sizes.get(entity) { + *height as f32 + } else { + 400.0 + }; + + commands + .entity(entity) + .remove::() + .remove::() + .insert(FreeCamera { + walk_speed: speed, + run_speed: speed * 3.0, + ..default() + }); + Ok(()) +} + +/// Enables a pan camera on the specified entity. +pub fn enable_pan_camera( + In(entity): In, + mut commands: Commands, +) -> crate::error::Result<()> { + commands + .entity(entity) + .remove::() + .remove::() + .insert(PanCamera::default()); + Ok(()) +} + +/// Disables all camera controllers on the specified entity. +pub fn disable_camera_controller( + In(entity): In, + mut commands: Commands, +) -> crate::error::Result<()> { + commands + .entity(entity) + .remove::() + .remove::() + .remove::(); + Ok(()) +} + +pub fn set_distance( + In((entity, distance)): In<(Entity, f32)>, + mut orbits: Query<(&mut Transform, &mut OrbitCamera)>, + mut pans: Query<&mut PanCamera>, +) -> crate::error::Result<()> { + if let Ok((mut transform, mut orbit)) = orbits.get_mut(entity) { + orbit.distance = distance.clamp(orbit.min_distance, orbit.max_distance); + orbit.apply_to_transform(&mut transform); + } else if let Ok(mut pan) = pans.get_mut(entity) { + pan.zoom_factor = distance.clamp(pan.min_zoom, pan.max_zoom); + } + Ok(()) +} + +pub fn set_center( + In((entity, center)): In<(Entity, Vec3)>, + mut query: Query<(&mut Transform, &mut OrbitCamera)>, +) -> crate::error::Result<()> { + if let Ok((mut transform, mut orbit)) = query.get_mut(entity) { + orbit.center = center; + orbit.apply_to_transform(&mut transform); + } + Ok(()) +} + +pub fn set_min_distance( + In((entity, min)): In<(Entity, f32)>, + mut orbits: Query<&mut OrbitCamera>, + mut pans: Query<&mut PanCamera>, +) -> crate::error::Result<()> { + if let Ok(mut orbit) = orbits.get_mut(entity) { + orbit.min_distance = min; + } else if let Ok(mut pan) = pans.get_mut(entity) { + pan.min_zoom = min; + } + Ok(()) +} + +pub fn set_max_distance( + In((entity, max)): In<(Entity, f32)>, + mut orbits: Query<&mut OrbitCamera>, + mut pans: Query<&mut PanCamera>, +) -> crate::error::Result<()> { + if let Ok(mut orbit) = orbits.get_mut(entity) { + orbit.max_distance = max; + } else if let Ok(mut pan) = pans.get_mut(entity) { + pan.max_zoom = max; + } + Ok(()) +} + +pub fn set_speed( + In((entity, speed)): In<(Entity, f32)>, + mut orbits: Query<&mut OrbitCamera>, + mut frees: Query<&mut FreeCamera>, + mut pans: Query<&mut PanCamera>, +) -> crate::error::Result<()> { + if let Ok(mut orbit) = orbits.get_mut(entity) { + orbit.orbit_sensitivity = speed; + } else if let Ok(mut free) = frees.get_mut(entity) { + free.walk_speed = speed; + free.run_speed = speed * 3.0; + } else if let Ok(mut pan) = pans.get_mut(entity) { + pan.pan_speed = speed; + } + Ok(()) +} + +pub fn reset_camera( + In(entity): In, + mut orbits: Query<(&mut Transform, &mut OrbitCamera)>, + mut free_states: Query<&mut FreeCameraState>, + mut pans: Query<&mut PanCamera>, +) -> crate::error::Result<()> { + if let Ok((mut transform, mut orbit)) = orbits.get_mut(entity) { + orbit.reset(); + orbit.apply_to_transform(&mut transform); + } else if let Ok(mut state) = free_states.get_mut(entity) { + state.pitch = 0.0; + state.yaw = 0.0; + state.velocity = Vec3::ZERO; + state.speed_multiplier = 1.0; + } else if let Ok(mut pan) = pans.get_mut(entity) { + pan.zoom_factor = 1.0; + } + Ok(()) +} diff --git a/crates/processing_render/src/color.rs b/crates/processing_render/src/color.rs index d912c78..932fc8d 100644 --- a/crates/processing_render/src/color.rs +++ b/crates/processing_render/src/color.rs @@ -112,7 +112,6 @@ impl ColorMode { #[cfg(test)] mod tests { use super::*; - use bevy::color::ColorToComponents; #[test] fn test_srgb_color() { diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 1908bcd..11ceecd 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1,5 +1,6 @@ #![allow(clippy::module_inception)] +pub mod camera; pub mod color; pub mod geometry; pub mod gltf; @@ -69,6 +70,9 @@ impl Plugin for ProcessingRenderPlugin { material::ProcessingMaterialPlugin, bevy::pbr::wireframe::WireframePlugin::default(), material::custom::CustomMaterialPlugin, + camera::OrbitCameraPlugin, + bevy::camera_controller::free_camera::FreeCameraPlugin, + bevy::camera_controller::pan_camera::PanCameraPlugin, )); app.add_systems(First, (clear_transient_meshes, activate_cameras)) @@ -426,6 +430,86 @@ pub fn graphics_mode_2d(graphics_entity: Entity) -> error::Result<()> { }) } +pub fn graphics_orbit_camera(graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::enable_orbit_camera, graphics_entity) + .unwrap() + }) +} + +pub fn graphics_free_camera(graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::enable_free_camera, graphics_entity) + .unwrap() + }) +} + +pub fn graphics_pan_camera(graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::enable_pan_camera, graphics_entity) + .unwrap() + }) +} + +pub fn graphics_disable_camera_controller(graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::disable_camera_controller, graphics_entity) + .unwrap() + }) +} + +pub fn camera_set_distance(entity: Entity, distance: f32) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::set_distance, (entity, distance)) + .unwrap() + }) +} + +pub fn camera_set_center(entity: Entity, center: Vec3) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::set_center, (entity, center)) + .unwrap() + }) +} + +pub fn camera_set_min_distance(entity: Entity, min: f32) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::set_min_distance, (entity, min)) + .unwrap() + }) +} + +pub fn camera_set_max_distance(entity: Entity, max: f32) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::set_max_distance, (entity, max)) + .unwrap() + }) +} + +pub fn camera_set_speed(entity: Entity, speed: f32) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::set_speed, (entity, speed)) + .unwrap() + }) +} + +pub fn camera_reset(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(camera::reset_camera, entity) + .unwrap() + }) +} + pub fn graphics_perspective( graphics_entity: Entity, fov: f32, diff --git a/crates/processing_render/src/material/mod.rs b/crates/processing_render/src/material/mod.rs index abd9f76..be48836 100644 --- a/crates/processing_render/src/material/mod.rs +++ b/crates/processing_render/src/material/mod.rs @@ -63,9 +63,7 @@ pub fn create_pbr( cull_mode: None, ..default() }, - extension: ProcessingMaterial { - blend_state: None, - }, + extension: ProcessingMaterial { blend_state: None }, }); commands.spawn(UntypedMaterial(handle.untyped())).id() } diff --git a/examples/camera_controllers.rs b/examples/camera_controllers.rs new file mode 100644 index 0000000..69a2c69 --- /dev/null +++ b/examples/camera_controllers.rs @@ -0,0 +1,97 @@ +use processing_glfw::GlfwContext; + +use bevy::math::Vec3; +use processing::prelude::*; +use processing_render::render::command::DrawCommand; + +fn main() { + match sketch() { + Ok(_) => { + eprintln!("Sketch completed successfully"); + exit(0).unwrap(); + } + Err(e) => { + eprintln!("Sketch error: {:?}", e); + exit(1).unwrap(); + } + }; +} + +fn sketch() -> error::Result<()> { + let width = 800; + let height = 600; + let mut glfw_ctx = GlfwContext::new(width, height)?; + init(Config::default())?; + + let surface = glfw_ctx.create_surface(width, height)?; + let graphics = graphics_create(surface, width, height, TextureFormat::Rgba16Float)?; + let box_geo = geometry_box(100.0, 100.0, 100.0)?; + + graphics_mode_3d(graphics)?; + graphics_orbit_camera(graphics)?; + + let dir_light = + light_create_directional(graphics, bevy::color::Color::srgb(1.0, 0.98, 0.95), 1_500.0)?; + transform_set_position(dir_light, Vec3::new(300.0, 400.0, 300.0))?; + transform_look_at(dir_light, Vec3::ZERO)?; + + let mut angle: f32 = 0.0; + let mut mode = 0u8; + + while glfw_ctx.poll_events() { + if input_key_just_pressed(KeyCode::Digit1)? { + graphics_mode_3d(graphics)?; + graphics_orbit_camera(graphics)?; + mode = 0; + } + if input_key_just_pressed(KeyCode::Digit2)? { + graphics_mode_3d(graphics)?; + graphics_free_camera(graphics)?; + mode = 1; + } + if input_key_just_pressed(KeyCode::Digit3)? { + graphics_mode_2d(graphics)?; + graphics_pan_camera(graphics)?; + mode = 2; + } + + graphics_begin_draw(graphics)?; + + graphics_record_command( + graphics, + DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.05, 0.05, 0.07)), + )?; + + if mode < 2 { + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgb(1.0, 0.85, 0.57)), + )?; + graphics_record_command(graphics, DrawCommand::Roughness(0.3))?; + graphics_record_command(graphics, DrawCommand::Metallic(0.8))?; + graphics_record_command(graphics, DrawCommand::PushMatrix)?; + graphics_record_command(graphics, DrawCommand::Rotate { angle })?; + graphics_record_command(graphics, DrawCommand::Geometry(box_geo))?; + graphics_record_command(graphics, DrawCommand::PopMatrix)?; + } else { + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgb(0.8, 0.3, 0.2)), + )?; + graphics_record_command( + graphics, + DrawCommand::Rect { + x: 300.0, + y: 200.0, + w: 200.0, + h: 200.0, + radii: [0.0; 4], + }, + )?; + } + + graphics_end_draw(graphics)?; + angle += 0.02; + } + Ok(()) +}