Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 87 additions & 38 deletions crates/processing_pyo3/src/graphics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,23 +124,6 @@ fn normalize_src_rect(entity: Entity, rect: Option<[f32; 4]>) -> PyResult<[f32;
}
}

/// Resolve a composite source (`Image`, `Webcam`, or `Graphics`) to a
/// texture-bearing entity, plus the graphics entity that must be flushed first
/// when the source is a graphics (so its latest content is what gets sampled).
/// A `Graphics` source uses its render-target surface entity, which carries an
/// `Image` component for offscreen graphics.
fn resolve_composite_source(src: &Bound<'_, PyAny>) -> PyResult<(Entity, Option<Entity>)> {
if let Ok(img) = src.extract::<ImageRef>() {
return Ok((img.entity, None));
}
if let Ok(g) = src.extract::<PyRef<Graphics>>() {
return Ok((g.surface.entity, Some(g.entity)));
}
Err(PyValueError::new_err(
"composite source must be an Image, Webcam, or Graphics",
))
}

/// Run the built-in composite filter: blend the `src` texture into the `dst`
/// graphics target using `mode`, with normalized src/dst rects. This is the
/// shader-based composite pass shared by `copy`/`blend`/`mask`.
Expand Down Expand Up @@ -446,6 +429,36 @@ impl Light {
let v = extract_vec3(args)?;
transform_look_at(self.entity, v).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

#[pyo3(signature = (*args))]
pub fn translate(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> {
let v = extract_vec3(args)?;
transform_translate(self.entity, v).map_err(rt_err)
}

pub fn rotate_x(&self, angle: f32) -> PyResult<()> {
transform_rotate_x(self.entity, angle).map_err(rt_err)
}

pub fn rotate_y(&self, angle: f32) -> PyResult<()> {
transform_rotate_y(self.entity, angle).map_err(rt_err)
}

pub fn rotate_z(&self, angle: f32) -> PyResult<()> {
transform_rotate_z(self.entity, angle).map_err(rt_err)
}

/// Rotate by `angle` radians around `axis`.
#[pyo3(signature = (angle, *axis))]
pub fn rotate_axis(&self, angle: f32, axis: &Bound<'_, PyTuple>) -> PyResult<()> {
let axis = extract_vec3(axis)?;
transform_rotate_axis(self.entity, angle, axis).map_err(rt_err)
}

/// Back to the origin with no rotation.
pub fn reset(&self) -> PyResult<()> {
transform_reset(self.entity).map_err(rt_err)
}
}

impl Light {
Expand Down Expand Up @@ -565,16 +578,33 @@ impl Image {
}
}

/// Anything that can be sampled as an image: an `Image`, a `Webcam`, or an
/// offscreen `Graphics`.
pub(crate) struct ImageRef {
pub entity: Entity,
entity: Entity,
graphics: Option<Entity>,
}

impl ImageRef {
/// The entity carrying the image. A `Graphics` source with pending draws is
/// flushed first, so what it has drawn so far is what gets sampled.
pub(crate) fn texture(&self) -> PyResult<Entity> {
if let Some(graphics) = self.graphics {
graphics_flush_pending(graphics).map_err(rt_err)?;
}
Ok(self.entity)
}
}

impl<'a, 'py> FromPyObject<'a, 'py> for ImageRef {
type Error = PyErr;

fn extract(ob: pyo3::Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
if let Ok(img) = ob.extract::<PyRef<Image>>() {
return Ok(ImageRef { entity: img.entity });
return Ok(ImageRef {
entity: img.entity,
graphics: None,
});
}
// an offscreen Graphics renders into an image; a window has no texture to sample
if let Ok(g) = ob.extract::<PyRef<Graphics>>() {
Expand All @@ -585,12 +615,14 @@ impl<'a, 'py> FromPyObject<'a, 'py> for ImageRef {
}
return Ok(ImageRef {
entity: g.surface.entity,
graphics: Some(g.entity),
});
}
#[cfg(feature = "webcam")]
if let Ok(cam) = ob.extract::<PyRef<crate::webcam::Webcam>>() {
return Ok(ImageRef {
entity: cam.image_entity()?,
graphics: None,
});
}
Err(pyo3::exceptions::PyTypeError::new_err(
Expand Down Expand Up @@ -629,6 +661,26 @@ impl Image {
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

/// Resize to `width` x `height` pixels. A 0 for either keeps the aspect ratio.
pub fn resize(&self, width: u32, height: u32) -> PyResult<()> {
let (w, h) = image_size(self.entity).map_err(rt_err)?;
let (width, height) = match (width, height) {
(0, 0) => return Err(PyValueError::new_err("resize() needs a non-zero size")),
(0, height) => ((w as f32 * height as f32 / h as f32).round() as u32, height),
(width, 0) => (width, (h as f32 * width as f32 / w as f32).round() as u32),
size => size,
};
image_resize(
self.entity,
Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
)
.map_err(rt_err)
}

/// Image width in pixels.
#[getter]
fn width(&self) -> PyResult<u32> {
Expand Down Expand Up @@ -1258,8 +1310,19 @@ impl Graphics {
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

/// A color (same arguments as `fill()`), or an `Image`, `Graphics` or
/// `Webcam` stretched over the canvas.
#[pyo3(signature = (*args))]
pub fn background(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> {
if args.len() == 1
&& let Ok(image) = args.get_item(0)?.extract::<ImageRef>()
{
return graphics_record_command(
self.entity,
DrawCommand::BackgroundImage(image.texture()?),
)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")));
}
let color = extract_color_with_mode(
args,
&graphics_get_color_mode(self.entity)
Expand All @@ -1269,11 +1332,6 @@ impl Graphics {
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn background_image(&self, image: &Image) -> PyResult<()> {
graphics_record_command(self.entity, DrawCommand::BackgroundImage(image.entity))
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn clear(&self) -> PyResult<()> {
graphics_record_command(
self.entity,
Expand Down Expand Up @@ -1951,7 +2009,7 @@ impl Graphics {
graphics_record_command(
self.entity,
DrawCommand::Image {
entity: source.entity,
entity: source.texture()?,
dx,
dy,
d_width,
Expand Down Expand Up @@ -2340,7 +2398,7 @@ impl Graphics {
}

pub fn texture(&self, source: ImageRef) -> PyResult<()> {
graphics_record_command(self.entity, DrawCommand::Texture(source.entity))
graphics_record_command(self.entity, DrawCommand::Texture(source.texture()?))
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

Expand Down Expand Up @@ -2473,10 +2531,7 @@ impl Graphics {
MULTIPLY, SCREEN, REPLACE)",
)
})?;
let (src_entity, flush) = resolve_composite_source(src)?;
if let Some(g) = flush {
graphics_flush(g).map_err(rt_err)?;
}
let src_entity = src.extract::<ImageRef>()?.texture()?;
let sr = normalize_src_rect(src_entity, src_rect)?;
let dr = normalize_rect(dst_rect, self.width as f32, self.height as f32);
composite_apply(self.entity, src_entity, code, sr, dr, opacity)
Expand All @@ -2492,10 +2547,7 @@ impl Graphics {
src_rect: Option<[f32; 4]>,
dst_rect: Option<[f32; 4]>,
) -> PyResult<()> {
let (src_entity, flush) = resolve_composite_source(src)?;
if let Some(g) = flush {
graphics_flush(g).map_err(rt_err)?;
}
let src_entity = src.extract::<ImageRef>()?.texture()?;
let sr = normalize_src_rect(src_entity, src_rect)?;
let dr = normalize_rect(dst_rect, self.width as f32, self.height as f32);
composite_apply(self.entity, src_entity, COMPOSITE_MODE_REPLACE, sr, dr, 1.0)
Expand All @@ -2519,10 +2571,7 @@ impl Graphics {
/// Applies a mask source's brightness as this graphics' alpha channel
/// (Processing `mask`). `mask` may be an `Image`, webcam, or `Graphics`.
pub fn mask(&self, mask: &Bound<'_, PyAny>) -> PyResult<()> {
let (src_entity, flush) = resolve_composite_source(mask)?;
if let Some(g) = flush {
graphics_flush(g).map_err(rt_err)?;
}
let src_entity = mask.extract::<ImageRef>()?.texture()?;
composite_apply(
self.entity,
src_entity,
Expand Down
8 changes: 1 addition & 7 deletions crates/processing_pyo3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1687,13 +1687,7 @@ pub mod processing_py {
#[pyfunction]
#[pyo3(pass_module, signature = (*args))]
fn background(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> {
let graphics = graphics!(module);
let first = args.get_item(0)?;
if first.is_instance_of::<Image>() {
graphics.background_image(&*first.extract::<PyRef<Image>>()?)
} else {
graphics.background(args)
}
graphics!(module).background(args)
}

#[pyfunction]
Expand Down
103 changes: 102 additions & 1 deletion crates/processing_pyo3/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct Material {

pub(crate) fn py_to_shader_value(value: &Bound<'_, PyAny>) -> PyResult<shader_value::ShaderValue> {
if let Ok(img_ref) = value.extract::<ImageRef>() {
return Ok(shader_value::ShaderValue::Texture(img_ref.entity));
return Ok(shader_value::ShaderValue::Texture(img_ref.texture()?));
}
if let Ok(int_val) = value.cast::<PyInt>() {
if let Ok(v) = int_val.extract::<i32>() {
Expand Down Expand Up @@ -47,6 +47,16 @@ pub(crate) fn py_to_shader_value(value: &Bound<'_, PyAny>) -> PyResult<shader_va
return Ok(shader_value::ShaderValue::Grid(grid.entity));
}

// row-major like `get_matrix()`, WGSL wants columns
if let Ok(rows) = value.extract::<[f32; 16]>() {
let m = bevy::math::Mat4::from_cols_array(&rows).transpose();
return Ok(shader_value::ShaderValue::Mat4(m.to_cols_array()));
}
if let Ok(rows) = value.extract::<[[f32; 4]; 4]>() {
let m = bevy::math::Mat4::from_cols_array_2d(&rows).transpose();
return Ok(shader_value::ShaderValue::Mat4(m.to_cols_array()));
}

if let Ok(v) = value.extract::<[f32; 4]>() {
return Ok(shader_value::ShaderValue::Float4(v));
}
Expand Down Expand Up @@ -88,6 +98,32 @@ fn apply_albedo(entity: Entity, value: &Bound<'_, PyAny>) -> PyResult<()> {
)))
}

fn apply_emissive(entity: Entity, value: &Bound<'_, PyAny>) -> PyResult<()> {
let rt = |e| PyRuntimeError::new_err(format!("{e}"));
if let Ok(buf) = value.extract::<PyRef<Buffer>>() {
return material_set_emissive_buffer(entity, buf.entity).map_err(rt);
}
// emissive is a linear radiance, not an sRGB color
let linear: bevy::color::LinearRgba = if let Ok(c) = value.extract::<PyRef<PyColor>>() {
c.0.into()
} else if let Ok([r, g, b, a]) = value.extract::<[f32; 4]>() {
bevy::color::LinearRgba::new(r, g, b, a)
} else if let Ok([r, g, b]) = value.extract::<[f32; 3]>() {
bevy::color::LinearRgba::new(r, g, b, 1.0)
} else {
return Err(PyRuntimeError::new_err(format!(
"unsupported emissive type: {} (expected Color, Buffer, or [r,g,b,(a)])",
value.get_type().name()?
)));
};
material_set(
entity,
"emissive",
shader_value::ShaderValue::Float4([linear.red, linear.green, linear.blue, linear.alpha]),
)
.map_err(rt)
}

fn py_truthy(value: &Bound<'_, PyAny>) -> PyResult<bool> {
value
.extract::<bool>()
Expand All @@ -100,6 +136,7 @@ fn apply_kwargs(entity: Entity, kwargs: &Bound<'_, PyDict>) -> PyResult<()> {
let rt = |e| PyRuntimeError::new_err(format!("{e}"));
match name.as_str() {
"albedo" => apply_albedo(entity, &value)?,
"emissive" => apply_emissive(entity, &value)?,
"unlit" => material_set_unlit(entity, py_truthy(&value)?).map_err(rt)?,
"double_sided" => material_set_double_sided(entity, py_truthy(&value)?).map_err(rt)?,
"depth_write" => material_set_depth_write(entity, py_truthy(&value)?).map_err(rt)?,
Expand Down Expand Up @@ -166,6 +203,70 @@ impl Material {
};
apply_kwargs(self.entity, kwargs)
}

/// Base color: a `Color`, `[r, g, b, (a)]`, or a per-instance `Buffer`.
pub fn albedo(&self, value: &Bound<'_, PyAny>) -> PyResult<()> {
apply_albedo(self.entity, value)
}

pub fn metalness(&self, value: f32) -> PyResult<()> {
self.set_pbr("metallic", value)
}

pub fn roughness(&self, value: f32) -> PyResult<()> {
self.set_pbr("roughness", value)
}

pub fn reflectance(&self, value: f32) -> PyResult<()> {
self.set_pbr("reflectance", value)
}

/// Emitted light: a `Color`, linear `[r, g, b, (a)]`, or a per-instance `Buffer`.
pub fn emissive(&self, value: &Bound<'_, PyAny>) -> PyResult<()> {
apply_emissive(self.entity, value)
}

pub fn opaque(&self) -> PyResult<()> {
material_set_alpha_mode(self.entity, 0, 0.0).map_err(rt)
}

/// Discards fragments with alpha below `cutoff`.
#[pyo3(signature = (cutoff=0.5))]
pub fn mask(&self, cutoff: f32) -> PyResult<()> {
material_set_alpha_mode(self.entity, 1, cutoff).map_err(rt)
}

pub fn transparent(&self) -> PyResult<()> {
material_set_alpha_mode(self.entity, 2, 0.0).map_err(rt)
}

#[pyo3(signature = (on=true))]
pub fn double_sided(&self, on: bool) -> PyResult<()> {
material_set_double_sided(self.entity, on).map_err(rt)
}

#[pyo3(signature = (on=true))]
pub fn depth_write(&self, on: bool) -> PyResult<()> {
material_set_depth_write(self.entity, on).map_err(rt)
}

/// Blends with what's behind it using a `BlendMode` (`ADD`, `MULTIPLY`, a custom one, ...).
pub fn blend_mode(&self, mode: &crate::graphics::PyBlendMode) -> PyResult<()> {
let state = mode
.blend_state
.unwrap_or(bevy::render::render_resource::BlendState::ALPHA_BLENDING);
material_set_custom_blend(self.entity, state).map_err(rt)
}
}

fn rt(e: impl std::fmt::Display) -> PyErr {
PyRuntimeError::new_err(format!("{e}"))
}

impl Material {
fn set_pbr(&self, name: &str, value: f32) -> PyResult<()> {
material_set(self.entity, name, shader_value::ShaderValue::Float(value)).map_err(rt)
}
}

impl Drop for Material {
Expand Down
9 changes: 9 additions & 0 deletions crates/processing_render/src/graphics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,15 @@ pub fn flush(app: &mut App, entity: Entity) -> Result<()> {
Ok(())
}

/// Brings the target up to date with what was drawn so far; no update when nothing is queued
pub fn flush_pending(app: &mut App, entity: Entity) -> Result<()> {
if has_commands(app, entity) {
flush(app, entity)
} else {
upload_dirty(app, entity)
}
}

fn has_commands(app: &App, entity: Entity) -> bool {
app.world()
.get::<CommandBuffer>(entity)
Expand Down
Loading
Loading