diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index f6db5d2..39b3141 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -1963,7 +1963,7 @@ pub extern "C" fn processing_perspective( graphics_perspective( graphics_entity, fov, - aspect, + Some(aspect), near, far, bevy::math::Vec4::new(0.0, 0.0, -1.0, -near), diff --git a/crates/processing_node/src/lib.rs b/crates/processing_node/src/lib.rs index 7fb6a8d..71d1824 100644 --- a/crates/processing_node/src/lib.rs +++ b/crates/processing_node/src/lib.rs @@ -1345,7 +1345,7 @@ pub fn js_perspective( check(graphics_perspective( ent(graphics_id), fov as f32, - aspect as f32, + Some(aspect as f32), near as f32, far as f32, Vec4::new(0.0, 0.0, -1.0, -near as f32), diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index d805220..b5ec441 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -864,6 +864,11 @@ impl Drop for Graphics { } impl Graphics { + /// The `mode_3d()` view at the canvas's current size. + fn view_defaults(&self) -> PyResult { + graphics_view_defaults(self.entity).map_err(rt_err) + } + pub(crate) fn current_color_mode(&self) -> PyResult { graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -2607,30 +2612,79 @@ impl Graphics { transform_look_at(self.entity, v).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn perspective(&self, fov: f32, aspect: f32, near: f32, far: f32) -> PyResult<()> { + /// `camera()` restores the default `mode_3d()` view. `camera(eye, center, up)` + /// takes three vectors, or nine numbers as in Processing. + #[pyo3(signature = (*args))] + pub fn camera(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + let (eye, center, up) = match args.len() { + 0 => ( + Vec3::new(0.0, 0.0, self.view_defaults()?.camera_z), + Vec3::ZERO, + Vec3::Y, + ), + 3 | 9 => { + let n = args.len() / 3; + let part = |i: usize| extract_vec3(&args.get_slice(i * n, (i + 1) * n)); + (part(0)?, part(1)?, part(2)?) + } + n => { + return Err(pyo3::exceptions::PyTypeError::new_err(format!( + "camera() takes 0, 3 or 9 arguments ({n} given)" + ))); + } + }; + graphics_camera(self.entity, eye, center, up) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + /// Arguments left out take the `mode_3d()` defaults: 60 degree `fov`, the + /// canvas aspect, `near` 1 and `far` ten times the camera distance. + #[pyo3(signature = (fov=None, aspect=None, near=None, far=None))] + pub fn perspective( + &self, + fov: Option, + aspect: Option, + near: Option, + far: Option, + ) -> PyResult<()> { + let defaults = self.view_defaults()?; + let near = near.unwrap_or(defaults.near); graphics_perspective( self.entity, - fov, + fov.unwrap_or(defaults.fov), aspect, near, - far, + far.unwrap_or(defaults.far), Vec4::new(0.0, 0.0, -1.0, -near), ) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + /// Defaults to the canvas bounds, `near` 0 and `far` ten times the camera + /// distance, as Processing's `ortho()`. #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (left=None, right=None, bottom=None, top=None, near=None, far=None))] pub fn ortho( &self, - left: f32, - right: f32, - bottom: f32, - top: f32, - near: f32, - far: f32, + left: Option, + right: Option, + bottom: Option, + top: Option, + near: Option, + far: Option, ) -> PyResult<()> { - graphics_ortho(self.entity, left, right, bottom, top, near, far) - .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + let defaults = self.view_defaults()?; + let (half_w, half_h) = (defaults.width / 2.0, defaults.height / 2.0); + graphics_ortho( + self.entity, + left.unwrap_or(-half_w), + right.unwrap_or(half_w), + bottom.unwrap_or(-half_h), + top.unwrap_or(half_h), + near.unwrap_or(0.0), + far.unwrap_or(defaults.far), + ) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } pub fn orbit_camera(&self) -> PyResult<()> { diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index ee56d52..021c0d4 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -1765,6 +1765,39 @@ pub mod processing_py { graphics!(module).blend_mode(&*mode.extract::>()?) } + #[pyfunction] + #[pyo3(pass_module, signature = (*args))] + fn camera(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { + graphics!(module).camera(args) + } + + #[pyfunction] + #[pyo3(pass_module, signature = (fov=None, aspect=None, near=None, far=None))] + fn perspective( + module: &Bound<'_, PyModule>, + fov: Option, + aspect: Option, + near: Option, + far: Option, + ) -> PyResult<()> { + graphics!(module).perspective(fov, aspect, near, far) + } + + #[allow(clippy::too_many_arguments)] + #[pyfunction] + #[pyo3(pass_module, signature = (left=None, right=None, bottom=None, top=None, near=None, far=None))] + fn ortho( + module: &Bound<'_, PyModule>, + left: Option, + right: Option, + bottom: Option, + top: Option, + near: Option, + far: Option, + ) -> PyResult<()> { + graphics!(module).ortho(left, right, bottom, top, near, far) + } + #[pyfunction] #[pyo3(pass_module, signature = (intensity=processing_render::graphics::DEFAULT_BLOOM_INTENSITY, threshold=0.0))] fn bloom(module: &Bound<'_, PyModule>, intensity: f32, threshold: f32) -> PyResult<()> { diff --git a/crates/processing_render/src/gltf.rs b/crates/processing_render/src/gltf.rs index ec7be59..edac444 100644 --- a/crates/processing_render/src/gltf.rs +++ b/crates/processing_render/src/gltf.rs @@ -355,7 +355,7 @@ pub fn camera(In((gltf_entity, index)): In<(Entity, usize)>, world: &mut World) match projection { Projection::Perspective(p) => { world - .run_system_cached_with(graphics::perspective, (graphics_entity, p)) + .run_system_cached_with(graphics::perspective, (graphics_entity, p, false)) .unwrap()?; } Projection::Orthographic(o) => { diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index b2b78d0..86194b2 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -351,6 +351,47 @@ pub fn sync_to_surface( } } +/// The view `mode_3d()` sets up for a canvas, which `camera()`, `perspective()` and `ortho()` +/// fall back to for arguments left out. +#[derive(Debug, Clone, Copy)] +pub struct ViewDefaults { + pub width: f32, + pub height: f32, + pub fov: f32, + pub aspect: f32, + pub near: f32, + pub far: f32, + /// Distance at which the canvas fills the view. + pub camera_z: f32, +} + +impl ViewDefaults { + pub fn new(width: f32, height: f32) -> Self { + let fov = std::f32::consts::PI / 3.0; // 60 degrees + let camera_z = (height / 2.0) / (fov / 2.0).tan(); + Self { + width, + height, + fov, + aspect: width / height, + // Processing4 uses near = camera_z / 10, but that clips anything closer + // than ~camera_z/10 to the camera. Since `transform_set_position` lets the + // user move the camera without recomputing the projection, a small fixed + // near is safer and matches most engines' defaults. + near: 1.0, + far: camera_z * 10.0, + camera_z, + } + } +} + +pub fn view_defaults(In(entity): In, sizes: Query<&SurfaceSize>) -> Result { + let SurfaceSize(width, height) = sizes + .get(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + Ok(ViewDefaults::new(*width as f32, *height as f32)) +} + pub fn mode_3d( In(entity): In, mut projections: Query<&mut Projection>, @@ -361,18 +402,14 @@ pub fn mode_3d( .get(entity) .map_err(|_| ProcessingError::GraphicsNotFound)?; - let width = *width as f32; - let height = *height as f32; - - let fov = std::f32::consts::PI / 3.0; // 60 degrees - let aspect = width / height; - let camera_z = (height / 2.0) / (fov / 2.0).tan(); - // Processing4 uses near = camera_z / 10, but that clips anything closer - // than ~camera_z/10 to the camera. Since `transform_set_position` lets the - // user move the camera without recomputing the projection, a small fixed - // near is safer and matches most engines' defaults. - let near = 1.0; - let far = camera_z * 10.0; + let ViewDefaults { + fov, + aspect, + near, + far, + camera_z, + .. + } = ViewDefaults::new(*width as f32, *height as f32); let near_clip_plane = vec4(0.0, 0.0, -1.0, -near); let mut projection = projections @@ -427,17 +464,14 @@ pub fn mode_2d( } pub fn perspective( - In((entity, inner)): In<(Entity, PerspectiveProjection)>, + In((entity, inner, fit_canvas)): In<(Entity, PerspectiveProjection, bool)>, mut projections: Query<&mut Projection>, ) -> Result<()> { let mut projection = projections .get_mut(entity) .map_err(|_| ProcessingError::GraphicsNotFound)?; - *projection = Projection::custom(ProcessingPerspective { - inner, - fit_canvas: false, - }); + *projection = Projection::custom(ProcessingPerspective { inner, fit_canvas }); Ok(()) } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index f9fc08b..54363dd 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -864,16 +864,31 @@ pub fn camera_reset(entity: Entity) -> error::Result<()> { }) } +/// The `mode_3d()` view of this graphics at its current size. +pub fn graphics_view_defaults(graphics_entity: Entity) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(graphics::view_defaults, graphics_entity) + .unwrap() + }) +} + +/// `aspect_ratio: None` follows the canvas, including later resizes. pub fn graphics_perspective( graphics_entity: Entity, fov: f32, - aspect_ratio: f32, + aspect_ratio: Option, near: f32, far: f32, near_clip_plane: Vec4, ) -> error::Result<()> { app_mut(|app| { flush(app, graphics_entity)?; + let canvas_aspect = app + .world_mut() + .run_system_cached_with(graphics::view_defaults, graphics_entity) + .unwrap()? + .aspect; app.world_mut() .run_system_cached_with( graphics::perspective, @@ -881,11 +896,12 @@ pub fn graphics_perspective( graphics_entity, PerspectiveProjection { fov, - aspect_ratio, + aspect_ratio: aspect_ratio.unwrap_or(canvas_aspect), near, far, near_clip_plane, }, + aspect_ratio.is_none(), ), ) .unwrap() @@ -1060,6 +1076,8 @@ pub fn transform_look_at(entity: Entity, target: Vec3) -> error::Result<()> { /// Position the camera at `eye`, looking at `center` with the given `up`. pub fn graphics_camera(entity: Entity, eye: Vec3, center: Vec3, up: Vec3) -> error::Result<()> { app_mut(|app| { + // draws queued so far keep the view they were issued under + flush(app, entity)?; app.world_mut() .run_system_cached_with(transform::camera, (entity, eye, center, up)) .unwrap() diff --git a/crates/processing_wasm/src/lib.rs b/crates/processing_wasm/src/lib.rs index 260f005..f6f7331 100644 --- a/crates/processing_wasm/src/lib.rs +++ b/crates/processing_wasm/src/lib.rs @@ -1118,7 +1118,7 @@ pub fn js_perspective( check(graphics_perspective( graphics_entity, fov, - aspect, + Some(aspect), near, far, bevy::math::Vec4::new(0.0, 0.0, -1.0, -near),