diff --git a/crates/processing_core/src/lib.rs b/crates/processing_core/src/lib.rs index 9de4d39..b15688b 100644 --- a/crates/processing_core/src/lib.rs +++ b/crates/processing_core/src/lib.rs @@ -14,14 +14,21 @@ thread_local! { } pub fn app_mut(cb: impl FnOnce(&mut App) -> error::Result) -> error::Result { - let res = APP.with(|app_cell| { - let mut app_borrow = app_cell.borrow_mut(); + // `try_with` so a `Drop` running after the TLS is destroyed sees an + // `AppAccess` error rather than panicking + let res = APP.try_with(|app_cell| { + let mut app_borrow = app_cell + .try_borrow_mut() + .map_err(|_| error::ProcessingError::AppAccess)?; let app = app_borrow .as_mut() .ok_or(error::ProcessingError::AppAccess)?; cb(app) - })?; - Ok(res) + }); + match res { + Ok(inner) => inner, + Err(_) => Err(error::ProcessingError::AppAccess), + } } pub fn is_already_init() -> error::Result { diff --git a/crates/processing_pyo3/examples/camera_controllers.py b/crates/processing_pyo3/examples/camera_controllers.py index 7feae0f..562d5b6 100644 --- a/crates/processing_pyo3/examples/camera_controllers.py +++ b/crates/processing_pyo3/examples/camera_controllers.py @@ -8,7 +8,7 @@ def setup(): mode_3d() orbit_camera() - dir_light = create_directional_light((1.0, 0.98, 0.95), 1500.0) + dir_light = 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) diff --git a/crates/processing_pyo3/examples/lights.py b/crates/processing_pyo3/examples/lights.py index e825893..66552a9 100644 --- a/crates/processing_pyo3/examples/lights.py +++ b/crates/processing_pyo3/examples/lights.py @@ -7,19 +7,19 @@ def setup(): mode_3d() # Directional Light - dir_light = create_directional_light((0.5, 0.24, 1.0), 1500.0) + dir_light = directional_light((0.5, 0.24, 1.0), 1500.0) # Point Lights - point_light_a = create_point_light((1.0, 0.5, 0.25), 1000000.0, 200.0, 0.5) + point_light_a = point_light((1.0, 0.5, 0.25), 1000000.0, 200.0, 0.5) point_light_a.position(-25.0, 5.0, 51.0) point_light_a.look_at(0.0, 0.0, 0.0) - point_light_b = create_point_light((0.0, 0.5, 0.75), 2000000.0, 200.0, 0.25) + point_light_b = point_light((0.0, 0.5, 0.75), 2000000.0, 200.0, 0.25) point_light_b.position(0.0, 5.0, 50.5) point_light_b.look_at(0.0, 0.0, 0.0) # Spot Light - spot_light = create_spot_light((0.25, 0.8, 0.19), 15.0 * 1000000.0, 200.0, 0.84, 0.0, 0.7854) + spot_light = spot_light((0.25, 0.8, 0.19), 15.0 * 1000000.0, 200.0, 0.84, 0.0, 0.7854) spot_light.position(40.0, 0.0, 70.0) spot_light.look_at(0.0, 0.0, 0.0) diff --git a/crates/processing_pyo3/examples/materials.py b/crates/processing_pyo3/examples/materials.py index ffa66cc..2767e8a 100644 --- a/crates/processing_pyo3/examples/materials.py +++ b/crates/processing_pyo3/examples/materials.py @@ -7,8 +7,8 @@ def setup(): size(800, 600) mode_3d() - dir_light = create_directional_light((1.0, 0.98, 0.95), 1500.0) - point_light = create_point_light((1.0, 1.0, 1.0), 100000.0, 800.0, 0.0) + dir_light = directional_light((1.0, 0.98, 0.95), 1500.0) + point_light = point_light((1.0, 1.0, 1.0), 100000.0, 800.0, 0.0) point_light.position(200.0, 200.0, 400.0) mat = Material() diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 7902979..c32d62e 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -666,6 +666,19 @@ mod mewnala { #[pymodule_export] use crate::color::PyColor; + #[pyfunction(name = "color")] + #[pyo3(signature = (*args))] + fn color_ctor(py: Python<'_>, args: &Bound<'_, PyTuple>) -> PyResult { + let parent = py.import("mewnala.mewnala")?; + match get_graphics(&parent)? { + Some(g) => g.color(args), + None => { + let mode = crate::color::ColorMode::default(); + crate::color::extract_color_with_mode(args, &mode).map(PyColor::from) + } + } + } + #[pyfunction] fn hex(s: &str) -> PyResult { PyColor::hex(s) @@ -1004,7 +1017,7 @@ mod mewnala { return Ok(()); } - Python::attach(|py| { + let result: PyResult<()> = Python::attach(|py| { let builtins = PyModule::import(py, "builtins")?; let locals = builtins.getattr("locals")?.call0()?; @@ -1125,7 +1138,13 @@ mod mewnala { } Ok(()) - }) + }); + + // tear the app down here while the TLS is still alive; the eager + // TLS destructor aborts inside a Bevy resource drop + let _ = ::processing::exit(0); + + result } #[pyfunction] @@ -1248,21 +1267,6 @@ mod mewnala { graphics!(module).draw_geometry(&*geometry.extract::>()?) } - #[pyfunction(name = "color")] - #[pyo3(pass_module, signature = (*args))] - fn create_color( - module: &Bound<'_, PyModule>, - args: &Bound<'_, PyTuple>, - ) -> PyResult { - match get_graphics(module)? { - Some(g) => g.color(args), - None => { - let mode = super::color::ColorMode::default(); - super::color::extract_color_with_mode(args, &mode).map(super::color::PyColor::from) - } - } - } - #[pyfunction] #[pyo3(pass_module, signature = (*args))] fn background(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { @@ -1370,35 +1374,59 @@ mod mewnala { graphics.create_image(width, height) } + fn apply_light_transform( + light: &Light, + position: Option, + look_at: Option, + ) -> PyResult<()> { + if let Some(p) = position { + ::processing::prelude::transform_set_position(light.entity, p.into_vec3()) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + } + if let Some(la) = look_at { + ::processing::prelude::transform_look_at(light.entity, la.into_vec3()) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + } + Ok(()) + } + #[pyfunction] - #[pyo3(pass_module)] - fn create_directional_light( + #[pyo3(pass_module, signature = (color, illuminance, *, position=None, look_at=None))] + fn directional_light( module: &Bound<'_, PyModule>, color: super::color::ColorLike, illuminance: f32, + position: Option, + look_at: Option, ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - graphics.light_directional(color, illuminance) + let light = graphics.light_directional(color, illuminance)?; + apply_light_transform(&light, position, look_at)?; + Ok(light) } #[pyfunction] - #[pyo3(pass_module)] - fn create_point_light( + #[pyo3(pass_module, signature = (color, intensity, range, radius, *, position=None, look_at=None))] + fn point_light( module: &Bound<'_, PyModule>, color: super::color::ColorLike, intensity: f32, range: f32, radius: f32, + position: Option, + look_at: Option, ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - graphics.light_point(color, intensity, range, radius) + let light = graphics.light_point(color, intensity, range, radius)?; + apply_light_transform(&light, position, look_at)?; + Ok(light) } #[pyfunction] - #[pyo3(pass_module)] - fn create_spot_light( + #[pyo3(pass_module, signature = (color, intensity, range, radius, inner_angle, outer_angle, *, position=None, look_at=None))] + fn spot_light( module: &Bound<'_, PyModule>, color: super::color::ColorLike, intensity: f32, @@ -1406,10 +1434,15 @@ mod mewnala { radius: f32, inner_angle: f32, outer_angle: f32, + position: Option, + look_at: Option, ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - graphics.light_spot(color, intensity, range, radius, inner_angle, outer_angle) + let light = + graphics.light_spot(color, intensity, range, radius, inner_angle, outer_angle)?; + apply_light_transform(&light, position, look_at)?; + Ok(light) } #[pyfunction(name = "sphere")] diff --git a/crates/processing_pyo3/src/math.rs b/crates/processing_pyo3/src/math.rs index 2abde01..5e19d9b 100644 --- a/crates/processing_pyo3/src/math.rs +++ b/crates/processing_pyo3/src/math.rs @@ -504,6 +504,11 @@ macro_rules! impl_py_vec { } impl_py_vec!(PyVec2, "Vec2", 2, [(x, set_x, 0), (y, set_y, 1)], Vec2, extra { + #[classattr] #[allow(non_snake_case)] fn ZERO() -> Self { Self(Vec2::ZERO) } + #[classattr] #[allow(non_snake_case)] fn ONE() -> Self { Self(Vec2::ONE) } + #[classattr] #[allow(non_snake_case)] fn X() -> Self { Self(Vec2::X) } + #[classattr] #[allow(non_snake_case)] fn Y() -> Self { Self(Vec2::Y) } + fn angle(&self) -> f32 { self.0.y.atan2(self.0.x) } @@ -543,6 +548,12 @@ impl_py_vec!(PyVec2, "Vec2", 2, [(x, set_x, 0), (y, set_y, 1)], Vec2, extra { }); impl_py_vec!(PyVec3, "Vec3", 3, [(x, set_x, 0), (y, set_y, 1), (z, set_z, 2)], Vec3, extra { + #[classattr] #[allow(non_snake_case)] fn ZERO() -> Self { Self(Vec3::ZERO) } + #[classattr] #[allow(non_snake_case)] fn ONE() -> Self { Self(Vec3::ONE) } + #[classattr] #[allow(non_snake_case)] fn X() -> Self { Self(Vec3::X) } + #[classattr] #[allow(non_snake_case)] fn Y() -> Self { Self(Vec3::Y) } + #[classattr] #[allow(non_snake_case)] fn Z() -> Self { Self(Vec3::Z) } + fn cross(&self, other: &Self) -> Self { Self(self.0.cross(other.0)) } @@ -570,6 +581,13 @@ impl_py_vec!( [(x, set_x, 0), (y, set_y, 1), (z, set_z, 2), (w, set_w, 3)], Vec4, extra { + #[classattr] #[allow(non_snake_case)] fn ZERO() -> Self { Self(Vec4::ZERO) } + #[classattr] #[allow(non_snake_case)] fn ONE() -> Self { Self(Vec4::ONE) } + #[classattr] #[allow(non_snake_case)] fn X() -> Self { Self(Vec4::X) } + #[classattr] #[allow(non_snake_case)] fn Y() -> Self { Self(Vec4::Y) } + #[classattr] #[allow(non_snake_case)] fn Z() -> Self { Self(Vec4::Z) } + #[classattr] #[allow(non_snake_case)] fn W() -> Self { Self(Vec4::W) } + fn truncate(&self) -> PyVec3 { PyVec3(self.0.truncate()) } diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index f885750..001f9f5 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -303,11 +303,12 @@ pub fn mode_3d( let fov = std::f32::consts::PI / 3.0; // 60 degrees let aspect = width / height; let camera_z = (height / 2.0) / (fov / 2.0).tan(); - let near = camera_z / 10.0; + // 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; - - // TODO: Setting this as a default, but we need to think about API around - // a user defined value let near_clip_plane = vec4(0.0, 0.0, -1.0, -near); let mut projection = projections