Skip to content

Commit 2f26512

Browse files
committed
Add Sampler API.
1 parent 3ab8865 commit 2f26512

6 files changed

Lines changed: 132 additions & 60 deletions

File tree

crates/processing_pyo3/src/graphics.rs

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,66 @@ impl PyBlendMode {
132132
const OP_MAX: u8 = 4;
133133
}
134134

135+
/// Configures how an image is sampled when drawn.
136+
///
137+
/// Controls texture filtering and edge wrapping behavior.
138+
///
139+
/// - `filter` — `Sampler.LINEAR` (smooth) or `Sampler.NEAREST` (pixelated).
140+
/// - `wrap` — `Sampler.CLAMP` (default), `Sampler.REPEAT`, or `Sampler.MIRROR`.
141+
/// Use `wrap_x`/`wrap_y` to set each axis independently.
142+
#[pyclass]
143+
#[derive(Clone)]
144+
pub struct Sampler {
145+
pub(crate) filter: u8,
146+
pub(crate) wrap_x: u8,
147+
pub(crate) wrap_y: u8,
148+
}
149+
150+
#[pymethods]
151+
impl Sampler {
152+
#[new]
153+
#[pyo3(signature = (*, filter=0, wrap=0, wrap_x=None, wrap_y=None))]
154+
fn new(filter: u8, wrap: u8, wrap_x: Option<u8>, wrap_y: Option<u8>) -> Self {
155+
Self {
156+
filter,
157+
wrap_x: wrap_x.unwrap_or(wrap),
158+
wrap_y: wrap_y.unwrap_or(wrap),
159+
}
160+
}
161+
162+
fn __repr__(&self) -> String {
163+
let filter_name = match self.filter {
164+
0 => "LINEAR",
165+
1 => "NEAREST",
166+
_ => "?",
167+
};
168+
let wrap_name = |v: u8| match v {
169+
0 => "CLAMP",
170+
1 => "REPEAT",
171+
2 => "MIRROR",
172+
_ => "?",
173+
};
174+
format!(
175+
"Sampler(filter={}, wrap_x={}, wrap_y={})",
176+
filter_name,
177+
wrap_name(self.wrap_x),
178+
wrap_name(self.wrap_y)
179+
)
180+
}
181+
182+
#[classattr]
183+
const LINEAR: u8 = 0;
184+
#[classattr]
185+
const NEAREST: u8 = 1;
186+
187+
#[classattr]
188+
const CLAMP: u8 = 0;
189+
#[classattr]
190+
const REPEAT: u8 = 1;
191+
#[classattr]
192+
const MIRROR: u8 = 2;
193+
}
194+
135195
pub use crate::surface::Surface;
136196

137197
#[pyclass]
@@ -197,6 +257,20 @@ impl<'a, 'py> FromPyObject<'a, 'py> for ImageRef {
197257
}
198258
}
199259

260+
#[pymethods]
261+
impl Image {
262+
/// Applies a `Sampler` to this image, controlling filtering and wrapping.
263+
///
264+
/// ```python
265+
/// s = Sampler(filter=Sampler.NEAREST, wrap=Sampler.REPEAT)
266+
/// img.sampler(s)
267+
/// ```
268+
fn sampler(&self, sampler: &Sampler) -> PyResult<()> {
269+
image_set_sampler(self.entity, sampler.filter, sampler.wrap_x, sampler.wrap_y)
270+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
271+
}
272+
}
273+
200274
impl Drop for Image {
201275
fn drop(&mut self) {
202276
let _ = image_destroy(self.entity);
@@ -1073,21 +1147,6 @@ impl Graphics {
10731147
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
10741148
}
10751149

1076-
pub fn texture(&self, source: ImageRef) -> PyResult<()> {
1077-
graphics_record_command(self.entity, DrawCommand::Texture(source.entity))
1078-
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1079-
}
1080-
1081-
pub fn no_texture(&self) -> PyResult<()> {
1082-
graphics_record_command(self.entity, DrawCommand::NoTexture)
1083-
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1084-
}
1085-
1086-
pub fn texture_transform(&self, transform: crate::math::PyAffine2) -> PyResult<()> {
1087-
graphics_record_command(self.entity, DrawCommand::TextureTransform(transform.0))
1088-
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1089-
}
1090-
10911150
pub fn unlit(&self) -> PyResult<()> {
10921151
graphics_record_command(self.entity, DrawCommand::Unlit)
10931152
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))

crates/processing_pyo3/src/lib.rs

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ mod time;
2626
mod webcam;
2727

2828
use graphics::{
29-
Geometry, Graphics, Image, Light, PyBlendMode, Topology, get_graphics, get_graphics_mut,
29+
Geometry, Graphics, Image, Light, PyBlendMode, Sampler, Topology, get_graphics,
30+
get_graphics_mut,
3031
};
3132
use material::Material;
3233

@@ -340,6 +341,8 @@ mod mewnala {
340341
#[pymodule_export]
341342
use super::PyBlendMode;
342343
#[pymodule_export]
344+
use super::Sampler;
345+
#[pymodule_export]
343346
use super::Shader;
344347
#[pymodule_export]
345348
use super::Topology;
@@ -1518,27 +1521,6 @@ mod mewnala {
15181521
graphics!(module).emissive(args)
15191522
}
15201523

1521-
#[pyfunction]
1522-
#[pyo3(pass_module)]
1523-
fn texture(module: &Bound<'_, PyModule>, source: graphics::ImageRef) -> PyResult<()> {
1524-
graphics!(module).texture(source)
1525-
}
1526-
1527-
#[pyfunction]
1528-
#[pyo3(pass_module)]
1529-
fn no_texture(module: &Bound<'_, PyModule>) -> PyResult<()> {
1530-
graphics!(module).no_texture()
1531-
}
1532-
1533-
#[pyfunction]
1534-
#[pyo3(pass_module)]
1535-
fn texture_transform(
1536-
module: &Bound<'_, PyModule>,
1537-
transform: crate::math::PyAffine2,
1538-
) -> PyResult<()> {
1539-
graphics!(module).texture_transform(transform)
1540-
}
1541-
15421524
#[pyfunction]
15431525
#[pyo3(pass_module)]
15441526
fn unlit(module: &Bound<'_, PyModule>) -> PyResult<()> {

crates/processing_render/src/image.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use bevy::{
99
io::{AssetSourceId, embedded::GetAssetServer},
1010
},
1111
ecs::system::RunSystemOnce,
12+
image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor},
1213
prelude::*,
1314
render::{
1415
RenderApp,
@@ -466,6 +467,50 @@ pub fn create_readback_buffer(
466467
}))
467468
}
468469

470+
pub fn set_sampler(
471+
In((entity, filter, wrap_x, wrap_y)): In<(Entity, u8, u8, u8)>,
472+
p_images: Query<&Image>,
473+
mut images: ResMut<Assets<bevy::image::Image>>,
474+
) -> Result<()> {
475+
let p_image = p_images
476+
.get(entity)
477+
.map_err(|_| ProcessingError::ImageNotFound)?;
478+
479+
let mut image = images
480+
.get_mut(&p_image.handle)
481+
.ok_or(ProcessingError::ImageNotFound)?;
482+
483+
let filter_mode = match filter {
484+
0 => ImageFilterMode::Linear,
485+
1 => ImageFilterMode::Nearest,
486+
_ => {
487+
return Err(ProcessingError::InvalidArgument(format!(
488+
"unknown filter mode: {filter}"
489+
)));
490+
}
491+
};
492+
493+
let addr_mode = |v: u8| match v {
494+
0 => Ok(ImageAddressMode::ClampToEdge),
495+
1 => Ok(ImageAddressMode::Repeat),
496+
2 => Ok(ImageAddressMode::MirrorRepeat),
497+
_ => Err(ProcessingError::InvalidArgument(format!(
498+
"unknown wrap mode: {v}"
499+
))),
500+
};
501+
502+
image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
503+
mag_filter: filter_mode,
504+
min_filter: filter_mode,
505+
mipmap_filter: filter_mode,
506+
address_mode_u: addr_mode(wrap_x)?,
507+
address_mode_v: addr_mode(wrap_y)?,
508+
..Default::default()
509+
});
510+
511+
Ok(())
512+
}
513+
469514
pub fn gpu_image(app: &mut App, entity: Entity) -> Result<&GpuImage> {
470515
let handle = app
471516
.world()

crates/processing_render/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,15 @@ pub fn image_update_region(
933933
})
934934
}
935935

936+
/// Set the sampler for an image (filter mode + wrap modes).
937+
pub fn image_set_sampler(entity: Entity, filter: u8, wrap_x: u8, wrap_y: u8) -> error::Result<()> {
938+
app_mut(|app| {
939+
app.world_mut()
940+
.run_system_cached_with(image::set_sampler, (entity, filter, wrap_x, wrap_y))
941+
.unwrap()
942+
})
943+
}
944+
936945
/// Destroy an existing image and free its resources.
937946
pub fn image_destroy(entity: Entity) -> error::Result<()> {
938947
app_mut(|app| {

crates/processing_render/src/render/command.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use bevy::math::Affine2;
21
use bevy::prelude::*;
32
use bevy::render::render_resource::{BlendComponent, BlendFactor, BlendOperation, BlendState};
43
use processing_core::error::{self, ProcessingError};
@@ -307,9 +306,6 @@ pub enum DrawCommand {
307306
Roughness(f32),
308307
Metallic(f32),
309308
Emissive(Color),
310-
Texture(Entity),
311-
NoTexture,
312-
TextureTransform(Affine2),
313309
Unlit,
314310
Tint(Color),
315311
NoTint,

crates/processing_render/src/render/mod.rs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -217,25 +217,6 @@ pub fn flush_draw_commands(
217217
pbr.blend_state = None;
218218
state.material_key = pbr.into();
219219
}
220-
DrawCommand::Texture(entity) => {
221-
if let Ok(img) = p_images.get(entity) {
222-
let mut pbr = state.material_key.as_pbr();
223-
pbr.base_color_texture = Some(img.handle.clone());
224-
state.material_key = pbr.into();
225-
}
226-
}
227-
DrawCommand::NoTexture => {
228-
if let MaterialKey::Pbr { .. } = &state.material_key {
229-
let mut pbr = state.material_key.as_pbr();
230-
pbr.base_color_texture = None;
231-
state.material_key = pbr.into();
232-
}
233-
}
234-
DrawCommand::TextureTransform(transform) => {
235-
let mut pbr = state.material_key.as_pbr();
236-
pbr.uv_transform = transform;
237-
state.material_key = pbr.into();
238-
}
239220
DrawCommand::Unlit => {
240221
state.material_key = MaterialKey::Color {
241222
transparent: state.fill_is_transparent(),

0 commit comments

Comments
 (0)