Skip to content

Commit c725352

Browse files
committed
Additional features for rendering images.
1 parent a9f0774 commit c725352

9 files changed

Lines changed: 415 additions & 85 deletions

File tree

crates/processing_pyo3/src/graphics.rs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,32 @@ pub struct Image {
168168
pub(crate) entity: Entity,
169169
}
170170

171-
impl Image {
172-
#[expect(dead_code)] // it's only used by webcam atm
173-
pub(crate) fn from_entity(entity: Entity) -> Self {
174-
Self { entity }
171+
pub(crate) struct ImageRef {
172+
pub entity: Entity,
173+
}
174+
175+
impl<'a, 'py> FromPyObject<'a, 'py> for ImageRef {
176+
type Error = PyErr;
177+
178+
fn extract(ob: pyo3::Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
179+
if let Ok(img) = ob.extract::<PyRef<Image>>() {
180+
return Ok(ImageRef { entity: img.entity });
181+
}
182+
#[cfg(feature = "video")]
183+
if let Ok(vid) = ob.extract::<PyRef<crate::video::Video>>() {
184+
return Ok(ImageRef {
185+
entity: vid.image_entity()?,
186+
});
187+
}
188+
#[cfg(feature = "webcam")]
189+
if let Ok(cam) = ob.extract::<PyRef<crate::webcam::Webcam>>() {
190+
return Ok(ImageRef {
191+
entity: cam.image_entity()?,
192+
});
193+
}
194+
Err(pyo3::exceptions::PyTypeError::new_err(
195+
"expected an Image, Video, or Webcam",
196+
))
175197
}
176198
}
177199

@@ -831,6 +853,21 @@ impl Graphics {
831853
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
832854
}
833855

856+
pub fn rotate_x(&self, angle: f32) -> PyResult<()> {
857+
graphics_record_command(self.entity, DrawCommand::RotateX { angle })
858+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
859+
}
860+
861+
pub fn rotate_y(&self, angle: f32) -> PyResult<()> {
862+
graphics_record_command(self.entity, DrawCommand::RotateY { angle })
863+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
864+
}
865+
866+
pub fn rotate_z(&self, angle: f32) -> PyResult<()> {
867+
graphics_record_command(self.entity, DrawCommand::RotateZ { angle })
868+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
869+
}
870+
834871
pub fn draw_box(&self, width: f32, height: f32, depth: f32) -> PyResult<()> {
835872
graphics_record_command(
836873
self.entity,
@@ -960,6 +997,21 @@ impl Graphics {
960997
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
961998
}
962999

1000+
pub fn texture(&self, source: ImageRef) -> PyResult<()> {
1001+
graphics_record_command(self.entity, DrawCommand::Texture(source.entity))
1002+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1003+
}
1004+
1005+
pub fn no_texture(&self) -> PyResult<()> {
1006+
graphics_record_command(self.entity, DrawCommand::NoTexture)
1007+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1008+
}
1009+
1010+
pub fn texture_transform(&self, transform: crate::math::PyAffine2) -> PyResult<()> {
1011+
graphics_record_command(self.entity, DrawCommand::TextureTransform(transform.0))
1012+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1013+
}
1014+
9631015
pub fn unlit(&self) -> PyResult<()> {
9641016
graphics_record_command(self.entity, DrawCommand::Unlit)
9651017
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))

crates/processing_pyo3/src/lib.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ mod mewnala {
623623
mod math {
624624
use super::*;
625625

626+
#[pymodule_export]
627+
use crate::math::PyAffine2;
628+
#[pymodule_export]
629+
use crate::math::PyMat2;
626630
#[pymodule_export]
627631
use crate::math::PyQuat;
628632
#[pymodule_export]
@@ -1236,6 +1240,24 @@ mod mewnala {
12361240
graphics!(module).rotate(angle)
12371241
}
12381242

1243+
#[pyfunction]
1244+
#[pyo3(pass_module)]
1245+
fn rotate_x(module: &Bound<'_, PyModule>, angle: f32) -> PyResult<()> {
1246+
graphics!(module).rotate_x(angle)
1247+
}
1248+
1249+
#[pyfunction]
1250+
#[pyo3(pass_module)]
1251+
fn rotate_y(module: &Bound<'_, PyModule>, angle: f32) -> PyResult<()> {
1252+
graphics!(module).rotate_y(angle)
1253+
}
1254+
1255+
#[pyfunction]
1256+
#[pyo3(pass_module)]
1257+
fn rotate_z(module: &Bound<'_, PyModule>, angle: f32) -> PyResult<()> {
1258+
graphics!(module).rotate_z(angle)
1259+
}
1260+
12391261
#[pyfunction(name = "box")]
12401262
#[pyo3(pass_module)]
12411263
fn draw_box(module: &Bound<'_, PyModule>, x: f32, y: f32, z: f32) -> PyResult<()> {
@@ -1447,6 +1469,27 @@ mod mewnala {
14471469
graphics!(module).emissive(args)
14481470
}
14491471

1472+
#[pyfunction]
1473+
#[pyo3(pass_module)]
1474+
fn texture(module: &Bound<'_, PyModule>, source: graphics::ImageRef) -> PyResult<()> {
1475+
graphics!(module).texture(source)
1476+
}
1477+
1478+
#[pyfunction]
1479+
#[pyo3(pass_module)]
1480+
fn no_texture(module: &Bound<'_, PyModule>) -> PyResult<()> {
1481+
graphics!(module).no_texture()
1482+
}
1483+
1484+
#[pyfunction]
1485+
#[pyo3(pass_module)]
1486+
fn texture_transform(
1487+
module: &Bound<'_, PyModule>,
1488+
transform: crate::math::PyAffine2,
1489+
) -> PyResult<()> {
1490+
graphics!(module).texture_transform(transform)
1491+
}
1492+
14501493
#[pyfunction]
14511494
#[pyo3(pass_module)]
14521495
fn unlit(module: &Bound<'_, PyModule>) -> PyResult<()> {

crates/processing_pyo3/src/material.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use processing::prelude::*;
33
use pyo3::types::PyDict;
44
use pyo3::{exceptions::PyRuntimeError, prelude::*};
55

6+
use crate::graphics::ImageRef;
67
use crate::math::{PyVec2, PyVec3, PyVec4};
78
use crate::shader::Shader;
89

@@ -12,6 +13,9 @@ pub struct Material {
1213
}
1314

1415
fn py_to_material_value(value: &Bound<'_, PyAny>) -> PyResult<material::MaterialValue> {
16+
if let Ok(img_ref) = value.extract::<ImageRef>() {
17+
return Ok(material::MaterialValue::Texture(img_ref.entity));
18+
}
1519
if let Ok(v) = value.extract::<f32>() {
1620
return Ok(material::MaterialValue::Float(v));
1721
}

crates/processing_pyo3/src/math.rs

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::hash::{Hash, Hasher};
22

3-
use bevy::math::{EulerRot, Quat, Vec2, Vec3, Vec4};
3+
use bevy::math::{Affine2, EulerRot, Mat2, Quat, Vec2, Vec3, Vec4};
44
use pyo3::{
55
exceptions::{PyAttributeError, PyTypeError},
66
prelude::*,
@@ -771,6 +771,174 @@ impl PyVecIter {
771771
}
772772
}
773773

774+
#[pyclass(name = "Mat2", from_py_object)]
775+
#[derive(Clone, Debug)]
776+
pub struct PyMat2(pub(crate) Mat2);
777+
778+
impl From<Mat2> for PyMat2 {
779+
fn from(m: Mat2) -> Self {
780+
Self(m)
781+
}
782+
}
783+
784+
#[pymethods]
785+
impl PyMat2 {
786+
#[new]
787+
#[pyo3(signature = (*args))]
788+
pub fn py_new(args: &Bound<'_, PyTuple>) -> PyResult<Self> {
789+
match args.len() {
790+
0 => Ok(Self(Mat2::IDENTITY)),
791+
4 => {
792+
let m00: f32 = args.get_item(0)?.extract()?;
793+
let m01: f32 = args.get_item(1)?.extract()?;
794+
let m10: f32 = args.get_item(2)?.extract()?;
795+
let m11: f32 = args.get_item(3)?.extract()?;
796+
Ok(Self(Mat2::from_cols(Vec2::new(m00, m01), Vec2::new(m10, m11))))
797+
}
798+
_ => Err(PyTypeError::new_err("Mat2 takes 0 or 4 arguments")),
799+
}
800+
}
801+
802+
#[staticmethod]
803+
fn from_scale(scale: PyVec2) -> Self {
804+
Self(Mat2::from_diagonal(scale.0))
805+
}
806+
807+
#[staticmethod]
808+
fn from_angle(angle: f32) -> Self {
809+
Self(Mat2::from_angle(angle))
810+
}
811+
812+
fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
813+
let py = rhs.py();
814+
if let Ok(other) = rhs.extract::<PyRef<PyMat2>>() {
815+
return Ok(PyMat2(self.0 * other.0)
816+
.into_pyobject(py)?
817+
.into_any()
818+
.unbind());
819+
}
820+
if let Ok(v) = rhs.extract::<PyRef<PyVec2>>() {
821+
return Ok(PyVec2(self.0 * v.0)
822+
.into_pyobject(py)?
823+
.into_any()
824+
.unbind());
825+
}
826+
Err(PyTypeError::new_err(
827+
"unsupported operand type(s) for *: 'Mat2'",
828+
))
829+
}
830+
831+
fn determinant(&self) -> f32 {
832+
self.0.determinant()
833+
}
834+
835+
fn inverse(&self) -> Self {
836+
Self(self.0.inverse())
837+
}
838+
839+
fn transpose(&self) -> Self {
840+
Self(self.0.transpose())
841+
}
842+
843+
fn __repr__(&self) -> String {
844+
let c = self.0.to_cols_array();
845+
format!("Mat2({}, {}, {}, {})", c[0], c[1], c[2], c[3])
846+
}
847+
848+
fn __str__(&self) -> String {
849+
self.__repr__()
850+
}
851+
852+
fn __eq__(&self, other: &Self) -> bool {
853+
self.0 == other.0
854+
}
855+
856+
fn __hash__(&self) -> u64 {
857+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
858+
for &c in self.0.to_cols_array().iter() {
859+
hash_f32(c, &mut hasher);
860+
}
861+
std::hash::Hasher::finish(&hasher)
862+
}
863+
}
864+
865+
#[pyclass(name = "Affine2", from_py_object)]
866+
#[derive(Clone, Debug)]
867+
pub struct PyAffine2(pub(crate) Affine2);
868+
869+
impl From<Affine2> for PyAffine2 {
870+
fn from(a: Affine2) -> Self {
871+
Self(a)
872+
}
873+
}
874+
875+
#[pymethods]
876+
impl PyAffine2 {
877+
#[new]
878+
#[pyo3(signature = (matrix=None, translation=None))]
879+
pub fn py_new(matrix: Option<PyRef<PyMat2>>, translation: Option<PyRef<PyVec2>>) -> Self {
880+
Self(Affine2 {
881+
matrix2: matrix.map(|m| m.0).unwrap_or(Mat2::IDENTITY),
882+
translation: translation.map(|t| t.0).unwrap_or(Vec2::ZERO),
883+
})
884+
}
885+
886+
#[staticmethod]
887+
fn from_scale(scale: PyVec2) -> Self {
888+
Self(Affine2::from_scale(scale.0))
889+
}
890+
891+
#[staticmethod]
892+
fn from_scale_angle_translation(scale: PyVec2, angle: f32, translation: PyVec2) -> Self {
893+
Self(Affine2::from_scale_angle_translation(
894+
scale.0,
895+
angle,
896+
translation.0,
897+
))
898+
}
899+
900+
#[getter]
901+
fn matrix(&self) -> PyMat2 {
902+
PyMat2(self.0.matrix2)
903+
}
904+
905+
#[getter]
906+
fn translation(&self) -> PyVec2 {
907+
PyVec2(self.0.translation)
908+
}
909+
910+
fn inverse(&self) -> Self {
911+
Self(self.0.inverse())
912+
}
913+
914+
fn __repr__(&self) -> String {
915+
let m = self.0.matrix2.to_cols_array();
916+
let t = self.0.translation;
917+
format!(
918+
"Affine2(Mat2({}, {}, {}, {}), Vec2({}, {}))",
919+
m[0], m[1], m[2], m[3], t.x, t.y
920+
)
921+
}
922+
923+
fn __str__(&self) -> String {
924+
self.__repr__()
925+
}
926+
927+
fn __eq__(&self, other: &Self) -> bool {
928+
self.0 == other.0
929+
}
930+
931+
fn __hash__(&self) -> u64 {
932+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
933+
for &c in self.0.matrix2.to_cols_array().iter() {
934+
hash_f32(c, &mut hasher);
935+
}
936+
hash_f32(self.0.translation.x, &mut hasher);
937+
hash_f32(self.0.translation.y, &mut hasher);
938+
std::hash::Hasher::finish(&hasher)
939+
}
940+
}
941+
774942
#[cfg(test)]
775943
mod tests {
776944
use super::*;

crates/processing_render/src/material/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,21 @@ pub fn create_pbr(
7171
pub fn set_property(
7272
In((entity, name, value)): In<(Entity, String, MaterialValue)>,
7373
material_handles: Query<&UntypedMaterial>,
74+
images: Query<&crate::image::Image>,
7475
mut extended_materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, ProcessingMaterial>>>,
7576
mut custom_materials: ResMut<Assets<custom::CustomMaterial>>,
7677
) -> error::Result<()> {
78+
let texture_handle = match &value {
79+
MaterialValue::Texture(img_entity) => Some(
80+
images
81+
.get(*img_entity)
82+
.map_err(|_| ProcessingError::ImageNotFound)?
83+
.handle
84+
.clone(),
85+
),
86+
_ => None,
87+
};
88+
7789
let untyped = material_handles
7890
.get(entity)
7991
.map_err(|_| ProcessingError::MaterialNotFound)?;
@@ -86,7 +98,7 @@ pub fn set_property(
8698
let mut extended = extended_materials
8799
.get_mut(&handle)
88100
.ok_or(ProcessingError::MaterialNotFound)?;
89-
return pbr::set_property(&mut extended.base, &name, &value);
101+
return pbr::set_property(&mut extended.base, &name, &value, texture_handle);
90102
}
91103

92104
if let Ok(handle) = untyped.0.clone().try_typed::<custom::CustomMaterial>() {

crates/processing_render/src/material/pbr.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub fn set_property(
88
material: &mut StandardMaterial,
99
name: &str,
1010
value: &MaterialValue,
11+
texture_handle: Option<Handle<Image>>,
1112
) -> Result<()> {
1213
match name {
1314
"base_color" | "color" => {
@@ -87,6 +88,14 @@ pub fn set_property(
8788
}
8889
};
8990
}
91+
"base_color_texture" | "texture" => {
92+
let Some(handle) = texture_handle else {
93+
return Err(ProcessingError::InvalidArgument(format!(
94+
"'{name}' expects Texture, got {value:?}"
95+
)));
96+
};
97+
material.base_color_texture = Some(handle);
98+
}
9099
_ => {
91100
return Err(ProcessingError::UnknownMaterialProperty(name.to_string()));
92101
}

0 commit comments

Comments
 (0)