From e5145c5a1db1c84cf585c0881d0ed11715ebead3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Wed, 10 Dec 2025 10:19:09 -0800 Subject: [PATCH 1/5] Add Geometry API object --- Cargo.lock | 1 + Cargo.toml | 12 + crates/processing_ffi/src/lib.rs | 437 ++++++++++++++++++ crates/processing_render/Cargo.toml | 1 + crates/processing_render/src/error.rs | 4 + .../src/geometry/attributes.rs | 372 +++++++++++++++ .../processing_render/src/geometry/layout.rs | 200 ++++++++ crates/processing_render/src/geometry/mod.rs | 330 +++++++++++++ crates/processing_render/src/lib.rs | 408 +++++++++++++++- .../processing_render/src/render/command.rs | 1 + crates/processing_render/src/render/mod.rs | 36 +- docs/api.md | 19 +- examples/animated_mesh.rs | 92 ++++ examples/box.rs | 51 ++ examples/custom_attribute.rs | 72 +++ 15 files changed, 2030 insertions(+), 6 deletions(-) create mode 100644 crates/processing_render/src/geometry/attributes.rs create mode 100644 crates/processing_render/src/geometry/layout.rs create mode 100644 crates/processing_render/src/geometry/mod.rs create mode 100644 examples/animated_mesh.rs create mode 100644 examples/box.rs create mode 100644 examples/custom_attribute.rs diff --git a/Cargo.lock b/Cargo.lock index 5e14b7b..3330251 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4426,6 +4426,7 @@ name = "processing_render" version = "0.1.0" dependencies = [ "bevy", + "bitflags 2.10.0", "crossbeam-channel", "half", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 9e64703..bf30fe3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,18 @@ path = "examples/update_pixels.rs" name = "transforms" path = "examples/transforms.rs" +[[example]] +name = "box" +path = "examples/box.rs" + +[[example]] +name = "animated_mesh" +path = "examples/animated_mesh.rs" + +[[example]] +name = "custom_attribute" +path = "examples/custom_attribute.rs" + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 8234147..b9fa5a3 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -585,3 +585,440 @@ pub extern "C" fn processing_ortho( let window_entity = Entity::from_bits(window_id); error::check(|| graphics_ortho(window_entity, left, right, bottom, top, near, far)); } + +pub const PROCESSING_ATTR_POSITION: u32 = 0x01; +pub const PROCESSING_ATTR_NORMAL: u32 = 0x02; +pub const PROCESSING_ATTR_COLOR: u32 = 0x04; +pub const PROCESSING_ATTR_UV: u32 = 0x08; + +pub const PROCESSING_ATTR_FORMAT_FLOAT: u8 = 1; +pub const PROCESSING_ATTR_FORMAT_FLOAT2: u8 = 2; +pub const PROCESSING_ATTR_FORMAT_FLOAT3: u8 = 3; +pub const PROCESSING_ATTR_FORMAT_FLOAT4: u8 = 4; + +pub const PROCESSING_TOPOLOGY_POINT_LIST: u8 = 0; +pub const PROCESSING_TOPOLOGY_LINE_LIST: u8 = 1; +pub const PROCESSING_TOPOLOGY_LINE_STRIP: u8 = 2; +pub const PROCESSING_TOPOLOGY_TRIANGLE_LIST: u8 = 3; +pub const PROCESSING_TOPOLOGY_TRIANGLE_STRIP: u8 = 4; + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_layout_create() -> u64 { + error::clear_error(); + error::check(geometry_layout_create) + .map(|e| e.to_bits()) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_layout_add_position(layout_id: u64) { + error::clear_error(); + let entity = Entity::from_bits(layout_id); + error::check(|| geometry_layout_add_position(entity)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_layout_add_normal(layout_id: u64) { + error::clear_error(); + let entity = Entity::from_bits(layout_id); + error::check(|| geometry_layout_add_normal(entity)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_layout_add_color(layout_id: u64) { + error::clear_error(); + let entity = Entity::from_bits(layout_id); + error::check(|| geometry_layout_add_color(entity)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_layout_add_uv(layout_id: u64) { + error::clear_error(); + let entity = Entity::from_bits(layout_id); + error::check(|| geometry_layout_add_uv(entity)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_layout_add_attribute(layout_id: u64, attr_id: u64) { + error::clear_error(); + let layout_entity = Entity::from_bits(layout_id); + let attr_entity = Entity::from_bits(attr_id); + error::check(|| geometry_layout_add_attribute(layout_entity, attr_entity)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_layout_build(layout_id: u64) { + error::clear_error(); + let entity = Entity::from_bits(layout_id); + error::check(|| geometry_layout_build(entity)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_layout_destroy(layout_id: u64) { + error::clear_error(); + let entity = Entity::from_bits(layout_id); + error::check(|| geometry_layout_destroy(entity)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_create_with_layout(layout_id: u64, topology: u8) -> u64 { + error::clear_error(); + let Some(topo) = geometry::Topology::from_u8(topology) else { + error::set_error("Invalid topology"); + return 0; + }; + let entity = Entity::from_bits(layout_id); + error::check(|| geometry_create_with_layout(entity, topo)) + .map(|e| e.to_bits()) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_create(topology: u8) -> u64 { + error::clear_error(); + let Some(topo) = geometry::Topology::from_u8(topology) else { + error::set_error("Invalid topology"); + return 0; + }; + error::check(|| geometry_create(topo)) + .map(|e| e.to_bits()) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_create_with_attrs(attrs: u32, topology: u8) -> u64 { + error::clear_error(); + let Some(topo) = geometry::Topology::from_u8(topology) else { + error::set_error("Invalid topology"); + return 0; + }; + let attrs = geometry::VertexAttributes::from_bits_truncate(attrs); + error::check(|| geometry_create_with_attributes(attrs, topo)) + .map(|e| e.to_bits()) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_normal(geo_id: u64, nx: f32, ny: f32, nz: f32) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_normal(entity, nx, ny, nz)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_color(geo_id: u64, r: f32, g: f32, b: f32, a: f32) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_color(entity, r, g, b, a)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_uv(geo_id: u64, u: f32, v: f32) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_uv(entity, u, v)); +} + +/// # Safety +/// - `name` must be a valid null-terminated C string. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn processing_geometry_attribute_create( + name: *const std::ffi::c_char, + format: u8, +) -> u64 { + error::clear_error(); + + let c_str = unsafe { std::ffi::CStr::from_ptr(name) }; + let name_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + error::set_error("Invalid UTF-8 in attribute name"); + return 0; + } + }; + + let attr_format = match geometry::AttributeFormat::from_u8(format) { + Some(f) => f, + None => { + error::set_error("Invalid attribute format"); + return 0; + } + }; + + error::check(|| geometry_attribute_create(name_str, attr_format)) + .map(|e| e.to_bits()) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_attribute_destroy(attr_id: u64) { + error::clear_error(); + let entity = Entity::from_bits(attr_id); + error::check(|| geometry_attribute_destroy(entity)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_attribute_position() -> u64 { + geometry_attribute_position().to_bits() +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_attribute_normal() -> u64 { + geometry_attribute_normal().to_bits() +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_attribute_color() -> u64 { + geometry_attribute_color().to_bits() +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_attribute_uv() -> u64 { + geometry_attribute_uv().to_bits() +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_attribute_float(geo_id: u64, attr_id: u64, v: f32) { + error::clear_error(); + let geo_entity = Entity::from_bits(geo_id); + let attr_entity = Entity::from_bits(attr_id); + error::check(|| geometry_attribute_float(geo_entity, attr_entity, v)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_attribute_float2(geo_id: u64, attr_id: u64, x: f32, y: f32) { + error::clear_error(); + let geo_entity = Entity::from_bits(geo_id); + let attr_entity = Entity::from_bits(attr_id); + error::check(|| geometry_attribute_float2(geo_entity, attr_entity, x, y)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_attribute_float3( + geo_id: u64, + attr_id: u64, + x: f32, + y: f32, + z: f32, +) { + error::clear_error(); + let geo_entity = Entity::from_bits(geo_id); + let attr_entity = Entity::from_bits(attr_id); + error::check(|| geometry_attribute_float3(geo_entity, attr_entity, x, y, z)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_attribute_float4( + geo_id: u64, + attr_id: u64, + x: f32, + y: f32, + z: f32, + w: f32, +) { + error::clear_error(); + let geo_entity = Entity::from_bits(geo_id); + let attr_entity = Entity::from_bits(attr_id); + error::check(|| geometry_attribute_float4(geo_entity, attr_entity, x, y, z, w)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_vertex(geo_id: u64, x: f32, y: f32, z: f32) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_vertex(entity, x, y, z)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_index(geo_id: u64, i: u32) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_index(entity, i)); +} + + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_vertex_count(geo_id: u64) -> u32 { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_vertex_count(entity)).unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_index_count(geo_id: u64) -> u32 { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_index_count(entity)).unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn processing_geometry_get_positions( + geo_id: u64, + start: u32, + end: u32, + out: *mut [f32; 3], + out_len: u32, +) -> u32 { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + let positions = error::check(|| geometry_get_positions(entity, start as usize, end as usize)); + match positions { + Some(p) => { + let count = p.len().min(out_len as usize); + std::ptr::copy_nonoverlapping(p.as_ptr(), out, count); + count as u32 + } + None => 0, + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn processing_geometry_get_normals( + geo_id: u64, + start: u32, + end: u32, + out: *mut [f32; 3], + out_len: u32, +) -> u32 { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + let normals = error::check(|| geometry_get_normals(entity, start as usize, end as usize)); + match normals { + Some(n) => { + let count = n.len().min(out_len as usize); + std::ptr::copy_nonoverlapping(n.as_ptr(), out, count); + count as u32 + } + None => 0, + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn processing_geometry_get_colors( + geo_id: u64, + start: u32, + end: u32, + out: *mut [f32; 4], + out_len: u32, +) -> u32 { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + let colors = error::check(|| geometry_get_colors(entity, start as usize, end as usize)); + match colors { + Some(c) => { + let count = c.len().min(out_len as usize); + std::ptr::copy_nonoverlapping(c.as_ptr(), out, count); + count as u32 + } + None => 0, + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn processing_geometry_get_uvs( + geo_id: u64, + start: u32, + end: u32, + out: *mut [f32; 2], + out_len: u32, +) -> u32 { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + let uvs = error::check(|| geometry_get_uvs(entity, start as usize, end as usize)); + match uvs { + Some(u) => { + let count = u.len().min(out_len as usize); + std::ptr::copy_nonoverlapping(u.as_ptr(), out, count); + count as u32 + } + None => 0, + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn processing_geometry_get_indices( + geo_id: u64, + start: u32, + end: u32, + out: *mut u32, + out_len: u32, +) -> u32 { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + let indices = error::check(|| geometry_get_indices(entity, start as usize, end as usize)); + match indices { + Some(i) => { + let count = i.len().min(out_len as usize); + std::ptr::copy_nonoverlapping(i.as_ptr(), out, count); + count as u32 + } + None => 0, + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_set_vertex(geo_id: u64, index: u32, x: f32, y: f32, z: f32) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_set_vertex(entity, index, x, y, z)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_set_normal( + geo_id: u64, + index: u32, + nx: f32, + ny: f32, + nz: f32, +) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_set_normal(entity, index, nx, ny, nz)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_set_color( + geo_id: u64, + index: u32, + r: f32, + g: f32, + b: f32, + a: f32, +) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_set_color(entity, index, r, g, b, a)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_set_uv(geo_id: u64, index: u32, u: f32, v: f32) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_set_uv(entity, index, u, v)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_destroy(geo_id: u64) { + error::clear_error(); + let entity = Entity::from_bits(geo_id); + error::check(|| geometry_destroy(entity)); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_model(window_id: u64, geo_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + let geo_entity = Entity::from_bits(geo_id); + error::check(|| graphics_record_command( + window_entity, + DrawCommand::Geometry(geo_entity), + )); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_box(width: f32, height: f32, depth: f32) -> u64 { + error::clear_error(); + error::check(|| geometry_box(width, height, depth)) + .map(|e| e.to_bits()) + .unwrap_or(0) +} diff --git a/crates/processing_render/Cargo.toml b/crates/processing_render/Cargo.toml index 72272e3..f68ff76 100644 --- a/crates/processing_render/Cargo.toml +++ b/crates/processing_render/Cargo.toml @@ -13,6 +13,7 @@ x11 = ["bevy/x11"] [dependencies] bevy = { workspace = true } +bitflags = "2" lyon = "1.0" raw-window-handle = "0.6" thiserror = "2" diff --git a/crates/processing_render/src/error.rs b/crates/processing_render/src/error.rs index db1af3b..cac70eb 100644 --- a/crates/processing_render/src/error.rs +++ b/crates/processing_render/src/error.rs @@ -24,4 +24,8 @@ pub enum ProcessingError { GraphicsNotFound, #[error("Invalid entity")] InvalidEntity, + #[error("Geometry not found")] + GeometryNotFound, + #[error("Layout not found")] + LayoutNotFound, } diff --git a/crates/processing_render/src/geometry/attributes.rs b/crates/processing_render/src/geometry/attributes.rs new file mode 100644 index 0000000..aa5f027 --- /dev/null +++ b/crates/processing_render/src/geometry/attributes.rs @@ -0,0 +1,372 @@ +use std::ops::Range; + +use bevy::{ + mesh::{Indices, MeshVertexAttribute, VertexAttributeValues}, + prelude::*, + render::render_resource::VertexFormat, +}; + +use crate::error::{ProcessingError, Result}; + +use super::{hash_attr_name, Geometry}; + +fn clamp_range(range: Range, len: usize) -> Range { + range.start.min(len)..range.end.min(len) +} + +fn get_mesh<'a>( + entity: Entity, + geometries: &Query<&Geometry>, + meshes: &'a Assets, +) -> Result<&'a Mesh> { + let geometry = geometries + .get(entity) + .map_err(|_| ProcessingError::GeometryNotFound)?; + meshes + .get(&geometry.handle) + .ok_or(ProcessingError::GeometryNotFound) +} + +fn get_mesh_mut<'a>( + entity: Entity, + geometries: &Query<&Geometry>, + meshes: &'a mut Assets, +) -> Result<&'a mut Mesh> { + let geometry = geometries + .get(entity) + .map_err(|_| ProcessingError::GeometryNotFound)?; + meshes + .get_mut(&geometry.handle) + .ok_or(ProcessingError::GeometryNotFound) +} + +macro_rules! impl_bulk_getter { + ($name:ident, $attr:expr, $variant:ident, $type:ty) => { + pub fn $name( + In((entity, range)): In<(Entity, Range)>, + geometries: Query<&Geometry>, + meshes: Res>, + ) -> Result> { + let mesh = get_mesh(entity, &geometries, &meshes)?; + match mesh.attribute($attr) { + Some(VertexAttributeValues::$variant(data)) => { + Ok(data[clamp_range(range, data.len())].to_vec()) + } + Some(_) => Err(ProcessingError::InvalidArgument( + concat!("Unexpected ", stringify!($name), " format").into(), + )), + None => Err(ProcessingError::GeometryNotFound), + } + } + }; +} + +impl_bulk_getter!(get_positions, Mesh::ATTRIBUTE_POSITION, Float32x3, [f32; 3]); +impl_bulk_getter!(get_normals, Mesh::ATTRIBUTE_NORMAL, Float32x3, [f32; 3]); +impl_bulk_getter!(get_colors, Mesh::ATTRIBUTE_COLOR, Float32x4, [f32; 4]); +impl_bulk_getter!(get_uvs, Mesh::ATTRIBUTE_UV_0, Float32x2, [f32; 2]); + +pub fn get_indices( + In((entity, range)): In<(Entity, Range)>, + geometries: Query<&Geometry>, + meshes: Res>, +) -> Result> { + let mesh = get_mesh(entity, &geometries, &meshes)?; + match mesh.indices() { + Some(Indices::U32(data)) => Ok(data[clamp_range(range, data.len())].to_vec()), + Some(Indices::U16(data)) => { + let range = clamp_range(range, data.len()); + Ok(data[range].iter().map(|&i| i as u32).collect()) + } + None => Ok(Vec::new()), + } +} + +macro_rules! impl_setter { + ($name:ident, $attr:expr, $variant:ident, [$($arg:ident: $arg_ty:ty),+], $arr:expr) => { + pub fn $name( + In((entity, index, $($arg),+)): In<(Entity, u32, $($arg_ty),+)>, + geometries: Query<&Geometry>, + mut meshes: ResMut>, + ) -> Result<()> { + let mesh = get_mesh_mut(entity, &geometries, &mut meshes)?; + match mesh.attribute_mut($attr) { + Some(VertexAttributeValues::$variant(data)) => { + let idx = index as usize; + if idx < data.len() { + data[idx] = $arr; + Ok(()) + } else { + Err(ProcessingError::InvalidArgument(format!( + "Index {} out of bounds (count: {})", index, data.len() + ))) + } + } + Some(_) => Err(ProcessingError::InvalidArgument( + concat!("Unexpected ", stringify!($name), " format").into(), + )), + None => Err(ProcessingError::InvalidArgument( + concat!("Geometry missing ", stringify!($attr)).into(), + )), + } + } + }; +} + +impl_setter!(set_vertex, Mesh::ATTRIBUTE_POSITION, Float32x3, [x: f32, y: f32, z: f32], [x, y, z]); +impl_setter!(set_normal, Mesh::ATTRIBUTE_NORMAL, Float32x3, [nx: f32, ny: f32, nz: f32], [nx, ny, nz]); +impl_setter!(set_color, Mesh::ATTRIBUTE_COLOR, Float32x4, [r: f32, g: f32, b: f32, a: f32], [r, g, b, a]); +impl_setter!(set_uv, Mesh::ATTRIBUTE_UV_0, Float32x2, [u: f32, v: f32], [u, v]); + +#[derive(Clone, Debug)] +pub enum AttributeValue { + Float(f32), + Float2([f32; 2]), + Float3([f32; 3]), + Float4([f32; 4]), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum AttributeFormat { + Float = 1, + Float2 = 2, + Float3 = 3, + Float4 = 4, +} + +impl AttributeFormat { + pub fn to_vertex_format(self) -> VertexFormat { + match self { + Self::Float => VertexFormat::Float32, + Self::Float2 => VertexFormat::Float32x2, + Self::Float3 => VertexFormat::Float32x3, + Self::Float4 => VertexFormat::Float32x4, + } + } + + pub fn from_u8(value: u8) -> Option { + match value { + 1 => Some(Self::Float), + 2 => Some(Self::Float2), + 3 => Some(Self::Float3), + 4 => Some(Self::Float4), + _ => None, + } + } +} + +#[derive(Component, Clone)] +pub struct Attribute { + pub name: &'static str, + pub format: AttributeFormat, + pub(crate) inner: MeshVertexAttribute, +} + +impl Attribute { + pub fn new(name: impl Into, format: AttributeFormat) -> Self { + let name: &'static str = Box::leak(name.into().into_boxed_str()); + let id = hash_attr_name(name); + let inner = MeshVertexAttribute::new(name, id, format.to_vertex_format()); + Self { name, format, inner } + } + + pub fn from_builtin(inner: MeshVertexAttribute, format: AttributeFormat) -> Self { + Self { + name: inner.name, + format, + inner, + } + } + + pub fn id(&self) -> u64 { + hash_attr_name(self.name) + } +} + +#[derive(Resource)] +pub struct BuiltinAttributes { + pub position: Entity, + pub normal: Entity, + pub color: Entity, + pub uv: Entity, +} + +impl FromWorld for BuiltinAttributes { + fn from_world(world: &mut World) -> Self { + let position = world + .spawn(Attribute::from_builtin(Mesh::ATTRIBUTE_POSITION, AttributeFormat::Float3)) + .id(); + let normal = world + .spawn(Attribute::from_builtin(Mesh::ATTRIBUTE_NORMAL, AttributeFormat::Float3)) + .id(); + let color = world + .spawn(Attribute::from_builtin(Mesh::ATTRIBUTE_COLOR, AttributeFormat::Float4)) + .id(); + let uv = world + .spawn(Attribute::from_builtin(Mesh::ATTRIBUTE_UV_0, AttributeFormat::Float2)) + .id(); + + Self { position, normal, color, uv } + } +} + +pub fn create_attribute( + In((name, format)): In<(String, AttributeFormat)>, + mut commands: Commands, +) -> Entity { + commands.spawn(Attribute::new(name, format)).id() +} + +pub fn destroy_attribute(In(entity): In, mut commands: Commands) { + commands.entity(entity).despawn(); +} + +pub fn get_attribute( + In((entity, attribute_id, index)): In<(Entity, MeshVertexAttribute, u32)>, + geometries: Query<&Geometry>, + meshes: Res>, +) -> Result { + let mesh = get_mesh(entity, &geometries, &meshes)?; + let idx = index as usize; + + let attr = mesh.attribute(attribute_id).ok_or_else(|| { + ProcessingError::InvalidArgument(format!( + "Geometry does not have attribute {}", + attribute_id.name + )) + })?; + + macro_rules! get_idx { + ($values:expr, $variant:ident) => { + if idx < $values.len() { + Ok(AttributeValue::$variant($values[idx])) + } else { + Err(ProcessingError::InvalidArgument(format!( + "Index {} out of bounds", + index, + ))) + } + }; + } + + match attr { + VertexAttributeValues::Float32(v) => get_idx!(v, Float), + VertexAttributeValues::Float32x2(v) => get_idx!(v, Float2), + VertexAttributeValues::Float32x3(v) => get_idx!(v, Float3), + VertexAttributeValues::Float32x4(v) => get_idx!(v, Float4), + // TODO: handle other formats as needed + _ => Err(ProcessingError::InvalidArgument( + "Unsupported attribute format".into(), + )), + } +} + +pub fn get_attributes( + In((entity, attribute_id, range)): In<(Entity, MeshVertexAttribute, Range)>, + geometries: Query<&Geometry>, + meshes: Res>, +) -> Result> { + let mesh = get_mesh(entity, &geometries, &meshes)?; + + let attr = mesh.attribute(attribute_id).ok_or_else(|| { + ProcessingError::InvalidArgument(format!( + "Geometry does not have attribute {}", + attribute_id.name + )) + })?; + + match attr { + VertexAttributeValues::Float32(v) => { + Ok(v[clamp_range(range, v.len())].iter().map(|&x| AttributeValue::Float(x)).collect()) + } + VertexAttributeValues::Float32x2(v) => { + Ok(v[clamp_range(range, v.len())].iter().map(|&x| AttributeValue::Float2(x)).collect()) + } + VertexAttributeValues::Float32x3(v) => { + Ok(v[clamp_range(range, v.len())].iter().map(|&x| AttributeValue::Float3(x)).collect()) + } + VertexAttributeValues::Float32x4(v) => { + Ok(v[clamp_range(range, v.len())].iter().map(|&x| AttributeValue::Float4(x)).collect()) + } + _ => Err(ProcessingError::InvalidArgument( + "Unsupported attribute format".into(), + )), + } +} + +pub fn set_attribute( + In((entity, attribute_id, index, value)): In<( + Entity, + MeshVertexAttribute, + u32, + AttributeValue, + )>, + geometries: Query<&Geometry>, + mut meshes: ResMut>, +) -> Result<()> { + let mesh = get_mesh_mut(entity, &geometries, &mut meshes)?; + let idx = index as usize; + + let attr = mesh.attribute_mut(attribute_id).ok_or_else(|| { + ProcessingError::InvalidArgument(format!( + "Geometry does not have attribute {}", + attribute_id.name + )) + })?; + + macro_rules! set_idx { + ($values:expr, $v:expr) => { + if idx < $values.len() { + $values[idx] = $v; + Ok(()) + } else { + Err(ProcessingError::InvalidArgument(format!( + "Index {} out of bounds", + index, + ))) + } + }; + } + + match (attr, value) { + (VertexAttributeValues::Float32(values), AttributeValue::Float(v)) => set_idx!(values, v), + (VertexAttributeValues::Float32x2(values), AttributeValue::Float2(v)) => { + set_idx!(values, v) + } + (VertexAttributeValues::Float32x3(values), AttributeValue::Float3(v)) => { + set_idx!(values, v) + } + (VertexAttributeValues::Float32x4(values), AttributeValue::Float4(v)) => { + set_idx!(values, v) + } + _ => Err(ProcessingError::InvalidArgument( + "Attribute value type does not match attribute format".into(), + )), + } +} + +pub fn to_attribute_values( + values: &[AttributeValue], +) -> Option { + macro_rules! convert { + ($variant:ident, $bevy:ident, $default:expr) => { + Some(VertexAttributeValues::$bevy( + values + .iter() + .map(|v| match v { + AttributeValue::$variant(x) => *x, + _ => $default, + }) + .collect(), + )) + }; + } + + match values.first()? { + AttributeValue::Float(_) => convert!(Float, Float32, 0.0), + AttributeValue::Float2(_) => convert!(Float2, Float32x2, [0.0; 2]), + AttributeValue::Float3(_) => convert!(Float3, Float32x3, [0.0; 3]), + AttributeValue::Float4(_) => convert!(Float4, Float32x4, [0.0; 4]), + } +} diff --git a/crates/processing_render/src/geometry/layout.rs b/crates/processing_render/src/geometry/layout.rs new file mode 100644 index 0000000..e5c3961 --- /dev/null +++ b/crates/processing_render/src/geometry/layout.rs @@ -0,0 +1,200 @@ +use bevy::{mesh::MeshVertexAttribute, prelude::*}; + +use super::{Attribute, BuiltinAttributes}; + +// bevy requires an attribute id for each unique vertex attribute. we don't really want to +// expose this to users, so we hash the attribute name to generate a unique id. in theory +// there could be collisions, but in practice this should be fine? +// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash +pub const fn hash_attr_name(s: &str) -> u64 { + let bytes = s.as_bytes(); + let mut hash = 0xcbf29ce484222325u64; + let mut i = 0; + while i < bytes.len() { + hash ^= bytes[i] as u64; + hash = hash.wrapping_mul(0x100000001b3); + i += 1; + } + hash +} + +/// Describes the layout of vertex attributes within a mesh's vertex buffer. +#[derive(Component, Clone, Debug)] +pub struct VertexLayout { + attributes: Vec, +} + +/// Represents a single vertex attribute within a vertex layout, including its type and offset +/// within the vertex buffer. +#[derive(Clone, Debug)] +pub struct VertexAttribute { + pub attribute: MeshVertexAttribute, + pub offset: u64, +} + +impl VertexLayout { + pub fn new() -> Self { + Self { + attributes: Vec::new(), + } + } + + pub fn default_layout() -> Self { + let attrs = [ + Mesh::ATTRIBUTE_POSITION, + Mesh::ATTRIBUTE_NORMAL, + Mesh::ATTRIBUTE_COLOR, + Mesh::ATTRIBUTE_UV_0, + ]; + + let mut offset = 0u64; + let mut layout_attrs = Vec::with_capacity(attrs.len()); + + for attr in attrs { + let size = attr.format.size(); + layout_attrs.push(VertexAttribute { attribute: attr, offset }); + offset += size; + } + + Self { + attributes: layout_attrs, + } + } + + pub fn attributes(&self) -> &[VertexAttribute] { + &self.attributes + } + + pub fn has_attribute(&self, attr: &MeshVertexAttribute) -> bool { + self.attributes.iter().any(|a| a.attribute.id == attr.id) + } +} + +impl Default for VertexLayout { + fn default() -> Self { + Self::default_layout() + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct VertexAttributes: u32 { + const POSITION = 0x01; + const NORMAL = 0x02; + const COLOR = 0x04; + const UV = 0x08; + } +} + +impl VertexAttributes { + pub fn to_layout(self) -> VertexLayout { + let mut attrs = vec![Mesh::ATTRIBUTE_POSITION]; + + if self.contains(VertexAttributes::NORMAL) { + attrs.push(Mesh::ATTRIBUTE_NORMAL); + } + if self.contains(VertexAttributes::COLOR) { + attrs.push(Mesh::ATTRIBUTE_COLOR); + } + if self.contains(VertexAttributes::UV) { + attrs.push(Mesh::ATTRIBUTE_UV_0); + } + + let mut offset = 0u64; + let mut layout_attrs = Vec::with_capacity(attrs.len()); + + for attr in attrs { + let size = attr.format.size(); + layout_attrs.push(VertexAttribute { attribute: attr, offset }); + offset += size; + } + + VertexLayout { + attributes: layout_attrs, + } + } +} + +#[derive(Component, Clone, Debug, Default)] +pub struct VertexLayoutBuilder { + attributes: Vec, +} + +pub fn create(In(()): In<()>, mut commands: Commands) -> Entity { + commands.spawn(VertexLayoutBuilder::default()).id() +} + +pub fn add_position(world: &mut World, entity: Entity) { + let builtins = world.resource::(); + let position = builtins.position; + if let Some(mut builder) = world.get_mut::(entity) { + builder.attributes.push(position); + } +} + +pub fn add_normal(world: &mut World, entity: Entity) { + let builtins = world.resource::(); + let normal = builtins.normal; + if let Some(mut builder) = world.get_mut::(entity) { + builder.attributes.push(normal); + } +} + +pub fn add_color(world: &mut World, entity: Entity) { + let builtins = world.resource::(); + let color = builtins.color; + if let Some(mut builder) = world.get_mut::(entity) { + builder.attributes.push(color); + } +} + +pub fn add_uv(world: &mut World, entity: Entity) { + let builtins = world.resource::(); + let uv = builtins.uv; + if let Some(mut builder) = world.get_mut::(entity) { + builder.attributes.push(uv); + } +} + +pub fn add_attribute(world: &mut World, layout_entity: Entity, attr_entity: Entity) { + if let Some(mut builder) = world.get_mut::(layout_entity) { + builder.attributes.push(attr_entity); + } +} + +pub fn destroy(In(entity): In, mut commands: Commands) { + commands.entity(entity).despawn(); +} + +pub fn build( + In(entity): In, + mut commands: Commands, + builders: Query<&VertexLayoutBuilder>, + attrs: Query<&Attribute>, +) -> bool { + let Ok(builder) = builders.get(entity) else { + return false; + }; + + let mut offset = 0u64; + let mut layout_attrs = Vec::with_capacity(builder.attributes.len()); + + for &attr_entity in &builder.attributes { + let Ok(attr) = attrs.get(attr_entity) else { + return false; + }; + let size = attr.inner.format.size(); + layout_attrs.push(VertexAttribute { + attribute: attr.inner.clone(), + offset, + }); + offset += size; + } + + let layout = VertexLayout { + attributes: layout_attrs, + }; + + commands.entity(entity).remove::().insert(layout); + true +} diff --git a/crates/processing_render/src/geometry/mod.rs b/crates/processing_render/src/geometry/mod.rs new file mode 100644 index 0000000..3a0a126 --- /dev/null +++ b/crates/processing_render/src/geometry/mod.rs @@ -0,0 +1,330 @@ +//! Geometry is a retained-mode representation of 3D mesh data that can be used for efficient +//! rendering. Typically, Processing's "sketch" API creates new mesh data every frame, which can be +//! inefficient for complex geometries. Geometry is backed by a Bevy [`Mesh`](Mesh) asset. +mod attributes; +pub mod layout; + +pub use attributes::*; +pub use layout::{hash_attr_name, VertexAttributes, VertexLayout, VertexAttribute, VertexLayoutBuilder}; + +use std::collections::HashMap; + +use bevy::{ + asset::RenderAssetUsages, + mesh::{Indices, Meshable, MeshVertexAttributeId, VertexAttributeValues}, + prelude::*, + render::render_resource::PrimitiveTopology, +}; + +use crate::error::{ProcessingError, Result}; + +pub struct GeometryPlugin; + +impl Plugin for GeometryPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[repr(u8)] +pub enum Topology { + PointList = 0, + LineList = 1, + LineStrip = 2, + #[default] + TriangleList = 3, + TriangleStrip = 4, +} + +impl Topology { + pub fn to_primitive_topology(self) -> PrimitiveTopology { + match self { + Self::PointList => PrimitiveTopology::PointList, + Self::LineList => PrimitiveTopology::LineList, + Self::LineStrip => PrimitiveTopology::LineStrip, + Self::TriangleList => PrimitiveTopology::TriangleList, + Self::TriangleStrip => PrimitiveTopology::TriangleStrip, + } + } + + pub fn from_u8(value: u8) -> Option { + match value { + 0 => Some(Self::PointList), + 1 => Some(Self::LineList), + 2 => Some(Self::LineStrip), + 3 => Some(Self::TriangleList), + 4 => Some(Self::TriangleStrip), + _ => None, + } + } +} + +#[derive(Component)] +pub struct Geometry { + pub handle: Handle, + pub layout: VertexLayout, + pub current_normal: [f32; 3], + pub current_color: [f32; 4], + pub current_uv: [f32; 2], + pub custom_current: HashMap, +} + +impl Geometry { + pub fn new(handle: Handle, layout: VertexLayout) -> Self { + Self { + handle, + layout, + current_normal: [0.0, 0.0, 1.0], + current_color: [1.0, 1.0, 1.0, 1.0], + current_uv: [0.0, 0.0], + custom_current: HashMap::new(), + } + } + + pub fn layout(&self) -> &VertexLayout { + &self.layout + } +} + +fn create_empty_mesh(layout: &VertexLayout, topology: Topology) -> Mesh { + let mut mesh = Mesh::new(topology.to_primitive_topology(), RenderAssetUsages::default()); + + for attr in layout.attributes() { + let empty_values = match attr.attribute.format { + bevy::render::render_resource::VertexFormat::Float32 => { + VertexAttributeValues::Float32(Vec::new()) + } + bevy::render::render_resource::VertexFormat::Float32x2 => { + VertexAttributeValues::Float32x2(Vec::new()) + } + bevy::render::render_resource::VertexFormat::Float32x3 => { + VertexAttributeValues::Float32x3(Vec::new()) + } + bevy::render::render_resource::VertexFormat::Float32x4 => { + VertexAttributeValues::Float32x4(Vec::new()) + } + _ => continue, + }; + mesh.insert_attribute(attr.attribute.clone(), empty_values); + } + + mesh +} + +pub fn create( + In(topology): In, + commands: Commands, + meshes: ResMut>, +) -> Entity { + create_with_layout(In((VertexLayout::default(), topology)), commands, meshes) +} + +pub fn create_with_layout( + In((layout, topology)): In<(VertexLayout, Topology)>, + mut commands: Commands, + mut meshes: ResMut>, +) -> Entity { + let mesh = create_empty_mesh(&layout, topology); + let handle = meshes.add(mesh); + + commands.spawn(Geometry::new(handle, layout)).id() +} + +pub fn create_with_attributes( + In((attrs, topology)): In<(VertexAttributes, Topology)>, + commands: Commands, + meshes: ResMut>, +) -> Entity { + create_with_layout(In((attrs.to_layout(), topology)), commands, meshes) +} + +pub fn create_box( + In((width, height, depth)): In<(f32, f32, f32)>, + mut commands: Commands, + mut meshes: ResMut>, +) -> Entity { + let cuboid = Cuboid::new(width, height, depth); + let mesh = cuboid.mesh().build(); + let handle = meshes.add(mesh); + + commands.spawn(Geometry::new(handle, VertexLayout::default())).id() +} + +pub fn normal(world: &mut World, entity: Entity, nx: f32, ny: f32, nz: f32) -> Result<()> { + let mut geometry = world + .get_mut::(entity) + .ok_or(ProcessingError::GeometryNotFound)?; + geometry.current_normal = [nx, ny, nz]; + Ok(()) +} + +pub fn color(world: &mut World, entity: Entity, r: f32, g: f32, b: f32, a: f32) -> Result<()> { + let mut geometry = world + .get_mut::(entity) + .ok_or(ProcessingError::GeometryNotFound)?; + geometry.current_color = [r, g, b, a]; + Ok(()) +} + +pub fn uv(world: &mut World, entity: Entity, u: f32, v: f32) -> Result<()> { + let mut geometry = world + .get_mut::(entity) + .ok_or(ProcessingError::GeometryNotFound)?; + geometry.current_uv = [u, v]; + Ok(()) +} + +pub fn attribute( + world: &mut World, + geo_entity: Entity, + attr_entity: Entity, + value: AttributeValue, +) -> Result<()> { + let attr = world + .get::(attr_entity) + .ok_or(ProcessingError::InvalidEntity)?; + let attr_id = attr.inner.id; + let mut geometry = world + .get_mut::(geo_entity) + .ok_or(ProcessingError::GeometryNotFound)?; + geometry.custom_current.insert(attr_id, value); + Ok(()) +} + +pub fn vertex( + In((entity, x, y, z)): In<(Entity, f32, f32, f32)>, + mut geometries: Query<&mut Geometry>, + mut meshes: ResMut>, +) -> Result<()> { + let geometry = geometries + .get_mut(entity) + .map_err(|_| ProcessingError::GeometryNotFound)?; + + let mesh = meshes + .get_mut(&geometry.handle) + .ok_or(ProcessingError::GeometryNotFound)?; + + if let Some(VertexAttributeValues::Float32x3(positions)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION) + { + positions.push([x, y, z]); + } + + if geometry.layout.has_attribute(&Mesh::ATTRIBUTE_NORMAL) { + if let Some(VertexAttributeValues::Float32x3(normals)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL) + { + normals.push(geometry.current_normal); + } + } + + if geometry.layout.has_attribute(&Mesh::ATTRIBUTE_COLOR) { + if let Some(VertexAttributeValues::Float32x4(colors)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_COLOR) + { + colors.push(geometry.current_color); + } + } + + if geometry.layout.has_attribute(&Mesh::ATTRIBUTE_UV_0) { + if let Some(VertexAttributeValues::Float32x2(uvs)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0) + { + uvs.push(geometry.current_uv); + } + } + + for attr in geometry.layout.attributes() { + if let Some(current) = geometry.custom_current.get(&attr.attribute.id) { + match (mesh.attribute_mut(attr.attribute.clone()), current) { + (Some(VertexAttributeValues::Float32(values)), AttributeValue::Float(v)) => { + values.push(*v); + } + (Some(VertexAttributeValues::Float32x2(values)), AttributeValue::Float2(v)) => { + values.push(*v); + } + (Some(VertexAttributeValues::Float32x3(values)), AttributeValue::Float3(v)) => { + values.push(*v); + } + (Some(VertexAttributeValues::Float32x4(values)), AttributeValue::Float4(v)) => { + values.push(*v); + } + _ => {} + } + } + } + + Ok(()) +} + +pub fn index( + In((entity, i)): In<(Entity, u32)>, + geometries: Query<&Geometry>, + mut meshes: ResMut>, +) -> Result<()> { + let geometry = geometries + .get(entity) + .map_err(|_| ProcessingError::GeometryNotFound)?; + + let mesh = meshes + .get_mut(&geometry.handle) + .ok_or(ProcessingError::GeometryNotFound)?; + + match mesh.indices_mut() { + Some(Indices::U32(indices)) => { + indices.push(i); + } + Some(Indices::U16(indices)) => { + indices.push(i as u16); + } + None => { + mesh.insert_indices(Indices::U32(vec![i])); + } + } + + Ok(()) +} + +pub fn vertex_count( + In(entity): In, + geometries: Query<&Geometry>, + meshes: Res>, +) -> Result { + let geometry = geometries + .get(entity) + .map_err(|_| ProcessingError::GeometryNotFound)?; + let mesh = meshes + .get(&geometry.handle) + .ok_or(ProcessingError::GeometryNotFound)?; + Ok(mesh.count_vertices() as u32) +} + +pub fn index_count( + In(entity): In, + geometries: Query<&Geometry>, + meshes: Res>, +) -> Result { + let geometry = geometries + .get(entity) + .map_err(|_| ProcessingError::GeometryNotFound)?; + let mesh = meshes + .get(&geometry.handle) + .ok_or(ProcessingError::GeometryNotFound)?; + Ok(mesh.indices().map(|i| i.len() as u32).unwrap_or(0)) +} + +pub fn destroy( + In(entity): In, + mut commands: Commands, + geometries: Query<&Geometry>, + mut meshes: ResMut>, +) -> Result<()> { + let geometry = geometries + .get(entity) + .map_err(|_| ProcessingError::GeometryNotFound)?; + + meshes.remove(&geometry.handle); + commands.entity(entity).despawn(); + Ok(()) +} diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 2beef68..7a53989 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1,4 +1,5 @@ pub mod error; +pub mod geometry; mod graphics; pub mod image; pub mod render; @@ -17,6 +18,7 @@ use bevy::{ use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; use tracing::debug; +use crate::geometry::{AttributeFormat, AttributeValue}; use crate::graphics::flush; use crate::{ graphics::GraphicsPlugin, image::ImagePlugin, render::command::DrawCommand, @@ -228,7 +230,7 @@ fn create_app() -> App { }); app.add_plugins(plugins); - app.add_plugins((ImagePlugin, GraphicsPlugin, SurfacePlugin)); + app.add_plugins((ImagePlugin, GraphicsPlugin, SurfacePlugin, geometry::GeometryPlugin)); app.add_systems(First, (clear_transient_meshes, activate_cameras)) .add_systems(Update, flush_draw_commands.before(AssetEventSystems)); @@ -682,3 +684,407 @@ pub fn image_destroy(entity: Entity) -> error::Result<()> { .unwrap() }) } + +pub fn geometry_layout_create() -> error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(geometry::layout::create, ()) + .unwrap()) + }) +} + +pub fn geometry_layout_add_position(entity: Entity) -> error::Result<()> { + app_mut(|app| { + geometry::layout::add_position(app.world_mut(), entity); + Ok(()) + }) +} + +pub fn geometry_layout_add_normal(entity: Entity) -> error::Result<()> { + app_mut(|app| { + geometry::layout::add_normal(app.world_mut(), entity); + Ok(()) + }) +} + +pub fn geometry_layout_add_color(entity: Entity) -> error::Result<()> { + app_mut(|app| { + geometry::layout::add_color(app.world_mut(), entity); + Ok(()) + }) +} + +pub fn geometry_layout_add_uv(entity: Entity) -> error::Result<()> { + app_mut(|app| { + geometry::layout::add_uv(app.world_mut(), entity); + Ok(()) + }) +} + +pub fn geometry_layout_add_attribute(layout_entity: Entity, attr_entity: Entity) -> error::Result<()> { + app_mut(|app| { + geometry::layout::add_attribute(app.world_mut(), layout_entity, attr_entity); + Ok(()) + }) +} + +pub fn geometry_layout_destroy(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::layout::destroy, entity) + .unwrap(); + Ok(()) + }) +} + +pub fn geometry_attribute_create(name: impl Into, format: AttributeFormat) -> error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(geometry::create_attribute, (name.into(), format)) + .unwrap()) + }) +} + +pub fn geometry_attribute_position() -> Entity { + app_mut(|app| Ok(app.world().resource::().position)).unwrap() +} + +pub fn geometry_attribute_normal() -> Entity { + app_mut(|app| Ok(app.world().resource::().normal)).unwrap() +} + +pub fn geometry_attribute_color() -> Entity { + app_mut(|app| Ok(app.world().resource::().color)).unwrap() +} + +pub fn geometry_attribute_uv() -> Entity { + app_mut(|app| Ok(app.world().resource::().uv)).unwrap() +} + +pub fn geometry_attribute_destroy(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::destroy_attribute, entity) + .unwrap(); + Ok(()) + }) +} + +pub fn geometry_create(topology: geometry::Topology) -> error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(geometry::create, topology) + .unwrap()) + }) +} + +pub fn geometry_layout_build(entity: Entity) -> error::Result<()> { + app_mut(|app| { + let success = app + .world_mut() + .run_system_cached_with(geometry::layout::build, entity) + .unwrap(); + if success { + Ok(()) + } else { + Err(error::ProcessingError::LayoutNotFound) + } + }) +} + +pub fn geometry_create_with_layout(layout_entity: Entity, topology: geometry::Topology) -> error::Result { + app_mut(|app| { + let layout = app + .world() + .get::(layout_entity) + .ok_or(error::ProcessingError::LayoutNotFound)? + .clone(); + Ok(app + .world_mut() + .run_system_cached_with(geometry::create_with_layout, (layout, topology)) + .unwrap()) + }) +} + +pub fn geometry_create_with_attributes(attrs: geometry::VertexAttributes, topology: geometry::Topology) -> error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(geometry::create_with_attributes, (attrs, topology)) + .unwrap()) + }) +} + +pub fn geometry_normal(entity: Entity, nx: f32, ny: f32, nz: f32) -> error::Result<()> { + app_mut(|app| geometry::normal(app.world_mut(), entity, nx, ny, nz)) +} + +pub fn geometry_color(entity: Entity, r: f32, g: f32, b: f32, a: f32) -> error::Result<()> { + app_mut(|app| geometry::color(app.world_mut(), entity, r, g, b, a)) +} + +pub fn geometry_uv(entity: Entity, u: f32, v: f32) -> error::Result<()> { + app_mut(|app| geometry::uv(app.world_mut(), entity, u, v)) +} + +pub fn geometry_attribute( + geo_entity: Entity, + attr_entity: Entity, + value: AttributeValue, +) -> error::Result<()> { + app_mut(|app| geometry::attribute(app.world_mut(), geo_entity, attr_entity, value)) +} + +pub fn geometry_attribute_float( + geo_entity: Entity, + attr_entity: Entity, + v: f32, +) -> error::Result<()> { + geometry_attribute(geo_entity, attr_entity, AttributeValue::Float(v)) +} + +pub fn geometry_attribute_float2( + geo_entity: Entity, + attr_entity: Entity, + x: f32, + y: f32, +) -> error::Result<()> { + geometry_attribute(geo_entity, attr_entity, AttributeValue::Float2([x, y])) +} + +pub fn geometry_attribute_float3( + geo_entity: Entity, + attr_entity: Entity, + x: f32, + y: f32, + z: f32, +) -> error::Result<()> { + geometry_attribute(geo_entity, attr_entity, AttributeValue::Float3([x, y, z])) +} + +pub fn geometry_attribute_float4( + geo_entity: Entity, + attr_entity: Entity, + x: f32, + y: f32, + z: f32, + w: f32, +) -> error::Result<()> { + geometry_attribute(geo_entity, attr_entity, AttributeValue::Float4([x, y, z, w])) +} + +pub fn geometry_vertex(entity: Entity, x: f32, y: f32, z: f32) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::vertex, (entity, x, y, z)) + .unwrap() + }) +} + +pub fn geometry_index(entity: Entity, i: u32) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::index, (entity, i)) + .unwrap() + }) +} + +pub fn geometry_vertex_count(entity: Entity) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::vertex_count, entity) + .unwrap() + }) +} + +pub fn geometry_index_count(entity: Entity) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::index_count, entity) + .unwrap() + }) +} + +pub fn geometry_get_positions( + entity: Entity, + start: usize, + end: usize, +) -> error::Result> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::get_positions, (entity, start..end)) + .unwrap() + }) +} + +pub fn geometry_get_normals( + entity: Entity, + start: usize, + end: usize, +) -> error::Result> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::get_normals, (entity, start..end)) + .unwrap() + }) +} + +pub fn geometry_get_colors( + entity: Entity, + start: usize, + end: usize, +) -> error::Result> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::get_colors, (entity, start..end)) + .unwrap() + }) +} + +pub fn geometry_get_uvs( + entity: Entity, + start: usize, + end: usize, +) -> error::Result> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::get_uvs, (entity, start..end)) + .unwrap() + }) +} + +pub fn geometry_get_indices( + entity: Entity, + start: usize, + end: usize, +) -> error::Result> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::get_indices, (entity, start..end)) + .unwrap() + }) +} + +pub fn geometry_destroy(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::destroy, entity) + .unwrap() + }) +} + +pub fn geometry_set_vertex( + entity: Entity, + index: u32, + x: f32, + y: f32, + z: f32, +) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::set_vertex, (entity, index, x, y, z)) + .unwrap() + }) +} + +pub fn geometry_set_normal( + entity: Entity, + index: u32, + nx: f32, + ny: f32, + nz: f32, +) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::set_normal, (entity, index, nx, ny, nz)) + .unwrap() + }) +} + +pub fn geometry_set_color( + entity: Entity, + index: u32, + r: f32, + g: f32, + b: f32, + a: f32, +) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::set_color, (entity, index, r, g, b, a)) + .unwrap() + }) +} + +pub fn geometry_set_uv(entity: Entity, index: u32, u: f32, v: f32) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::set_uv, (entity, index, u, v)) + .unwrap() + }) +} + +pub fn geometry_get_attribute( + geo_entity: Entity, + attr_entity: Entity, + index: u32, +) -> error::Result { + app_mut(|app| { + let attr = app + .world() + .get::(attr_entity) + .ok_or(error::ProcessingError::InvalidEntity)?; + let inner = attr.inner.clone(); + app.world_mut() + .run_system_cached_with(geometry::get_attribute, (geo_entity, inner, index)) + .unwrap() + }) +} + +pub fn geometry_get_attributes( + geo_entity: Entity, + attr_entity: Entity, + start: usize, + end: usize, +) -> error::Result> { + app_mut(|app| { + let attr = app + .world() + .get::(attr_entity) + .ok_or(error::ProcessingError::InvalidEntity)?; + let inner = attr.inner.clone(); + app.world_mut() + .run_system_cached_with(geometry::get_attributes, (geo_entity, inner, start..end)) + .unwrap() + }) +} + +pub fn geometry_set_attribute( + geo_entity: Entity, + attr_entity: Entity, + index: u32, + value: AttributeValue, +) -> error::Result<()> { + app_mut(|app| { + let attr = app + .world() + .get::(attr_entity) + .ok_or(error::ProcessingError::InvalidEntity)?; + let inner = attr.inner.clone(); + app.world_mut() + .run_system_cached_with(geometry::set_attribute, (geo_entity, inner, index, value)) + .unwrap() + }) +} + +pub fn geometry_box(width: f32, height: f32, depth: f32) -> error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(geometry::create_box, (width, height, depth)) + .unwrap()) + }) +} diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index fe85d81..f800a23 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -36,6 +36,7 @@ pub enum DrawCommand { ShearY { angle: f32, }, + Geometry(Entity), } #[derive(Debug, Default, Component)] diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index a5f84ef..abe34eb 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -12,7 +12,9 @@ use material::MaterialKey; use primitive::{TessellationMode, empty_mesh}; use transform::TransformStack; -use crate::{Flush, graphics::SurfaceSize, image::Image, render::primitive::rect}; +use crate::{ + Flush, geometry::Geometry, graphics::SurfaceSize, image::Image, render::primitive::rect, +}; #[derive(Component)] #[relationship(relationship_target = TransientMeshes)] @@ -97,6 +99,7 @@ pub fn flush_draw_commands( With, >, p_images: Query<&Image>, + p_geometries: Query<&Geometry>, ) { for (graphics_entity, mut cmd_buffer, mut state, render_layers, SurfaceSize(width, height)) in graphics.iter_mut() @@ -192,6 +195,37 @@ pub fn flush_draw_commands( DrawCommand::Scale { x, y } => state.transform.scale(x, y), DrawCommand::ShearX { angle } => state.transform.shear_x(angle), DrawCommand::ShearY { angle } => state.transform.shear_y(angle), + DrawCommand::Geometry(entity) => { + let Some(geometry) = p_geometries.get(entity).ok() else { + warn!("Could not find Geometry for entity {:?}", entity); + continue; + }; + + flush_batch(&mut res, &mut batch); + + // TODO: Implement state based material API + // https://github.com/processing/libprocessing/issues/10 + let material_key = MaterialKey { + transparent: false, // TODO: detect from geometry colors + background_image: None, + }; + + let material_handle = res.materials.add(material_key.to_material()); + let z_offset = -(batch.draw_index as f32 * 0.001); + + let mut transform = state.transform.to_bevy_transform(); + transform.translation.z += z_offset; + + res.commands.spawn(( + Mesh3d(geometry.handle.clone()), + MeshMaterial3d(material_handle), + BelongsToGraphics(batch.graphics_entity), + transform, + batch.render_layers.clone(), + )); + + batch.draw_index += 1; + } } } diff --git a/docs/api.md b/docs/api.md index 34e9c3e..c0cc4ce 100644 --- a/docs/api.md +++ b/docs/api.md @@ -9,7 +9,7 @@ explicitly destroyed via a `destroy` function. ### Surface A surface represents a drawing area. It is primarily used as a target for rendering and typically -is associated with a window or an off-screen buffer. The equivalent of a surface in `Bevy` is +is associated with a window or an off-screen buffer. The equivalent of a surface in Bevy is a `RenderTarget`, which can be a window or a texture. Note, a "surface" is also a technical term in graphics APIs like Vulkan and WebGPU, where it refers to @@ -25,7 +25,7 @@ specifically requests a headless drawing context, in which case an off-screen su Graphics objects encapsulate the rendering context and state and provide the core methods for drawing shapes, images, and text. They manage the current drawing state, including colors, stroke weights, transformations, -and other properties that affect rendering. In `Bevy`, this is equivalent to a `Camera` entity. +and other properties that affect rendering. In Bevy, this is equivalent to a `Camera` entity. In the Java Processing API, graphics objects are a subclass of `PImage`. In this API, graphics objects are distinct from images rather than bearing an explicit is-a relationship. Importantly, the "image" for a Bevy `Camera` is the internal @@ -37,7 +37,7 @@ not guaranteed to be the same as a user-created image. ### Image Images are 2D or 3D arrays of pixels that can be drawn onto surfaces. They can be created from files, generated procedurally, -or created as empty canvases for off-screen rendering. In `Bevy`, images are represented as `Image` assets. Images exist +or created as empty canvases for off-screen rendering. In Bevy, images are represented as `Image` assets. Images exist simultaneously as GPU resources and CPU-side data structures and have a lifecycle that requires the use to load pixels from the GPU to the CPU before accessing pixel data directly and to flush pixel data from the CPU to the GPU after modifying pixel data directly. @@ -50,12 +50,23 @@ modifying pixel data directly. Geometry (also known as `PShape` in the Java Processing API) represents complex shapes defined by vertices, edges, and faces. Geometry objects can encapsulate 2D shapes, 3D models, or custom vertex data. They can be created -programmatically or loaded from external files. In `Bevy`, geometry is typically represented using `Mesh` assets and +programmatically or loaded from external files. In Bevy, geometry is typically represented using `Mesh` assets and require an associated `Material` to be rendered. Like an image, geometry exists as both a GPU resource and a CPU-side data structure. Users must ensure that any changes to the CPU-side geometry are synchronized with the GPU resource before rendering. +### Layout + +A layout describes which vertex attributes are present in geometry and how they are arranged in a vertex buffer. +In Bevy, this corresponds to a `MeshVertexBufferLayout`. + +### Attribute + +An attribute describe a single element of the layout of a vertex buffer. Every geometry must have a position attribute, +and may optionally have other standard attributes such as normal, tangent, color, and texture coordinates (UVs). Custom +attributes can be defined for passing additional data to a material shader. In Bevy, these map to `MeshVertexAttribute`. + ### Material A material defines the appearance of geometry when rendered. In Bevy, materials are represented using `Material` assets diff --git a/examples/animated_mesh.rs b/examples/animated_mesh.rs new file mode 100644 index 0000000..0097730 --- /dev/null +++ b/examples/animated_mesh.rs @@ -0,0 +1,92 @@ +mod glfw; + +use glfw::GlfwContext; +use processing::prelude::*; +use processing_render::geometry::Topology; +use processing_render::render::command::DrawCommand; + +fn main() { + match sketch() { + Ok(_) => { + eprintln!("Sketch completed successfully"); + exit(0).unwrap(); + } + Err(e) => { + eprintln!("Sketch error: {:?}", e); + exit(1).unwrap(); + } + }; +} + +fn sketch() -> error::Result<()> { + let mut glfw_ctx = GlfwContext::new(600, 600)?; + init()?; + + let width = 600; + let height = 600; + let scale_factor = 1.0; + + let surface = glfw_ctx.create_surface(width, height, scale_factor)?; + let graphics = graphics_create(surface, width, height)?; + + let grid_size = 20; + let spacing = 10.0; + let offset = (grid_size as f32 * spacing) / 2.0; + + let mesh = geometry_create(Topology::TriangleList)?; + + for z in 0..grid_size { + for x in 0..grid_size { + let px = x as f32 * spacing - offset; + let pz = z as f32 * spacing - offset; + geometry_color(mesh, x as f32 / grid_size as f32, 0.5, z as f32 / grid_size as f32, 1.0)?; + geometry_normal(mesh, 0.0, 1.0, 0.0)?; + geometry_vertex(mesh, px, 0.0, pz)?; + } + } + + for z in 0..(grid_size - 1) { + for x in 0..(grid_size - 1) { + let tl = z * grid_size + x; + let tr = tl + 1; + let bl = (z + 1) * grid_size + x; + let br = bl + 1; + + geometry_index(mesh, tl)?; + geometry_index(mesh, bl)?; + geometry_index(mesh, tr)?; + + geometry_index(mesh, tr)?; + geometry_index(mesh, bl)?; + geometry_index(mesh, br)?; + } + } + + graphics_mode_3d(graphics)?; + graphics_camera_position(graphics, 150.0, 150.0, 150.0)?; + graphics_camera_look_at(graphics, 0.0, 0.0, 0.0)?; + + let mut time = 0.0f32; + + while glfw_ctx.poll_events() { + for z in 0..grid_size { + for x in 0..grid_size { + let idx = (z * grid_size + x) as u32; + let px = x as f32 * spacing - offset; + let pz = z as f32 * spacing - offset; + let wave = (px * 0.1 + time).sin() * (pz * 0.1 + time).cos() * 20.0; + geometry_set_vertex(mesh, idx, px, wave, pz)?; + } + } + + graphics_begin_draw(graphics)?; + graphics_record_command(graphics, DrawCommand::Geometry(mesh))?; + graphics_end_draw(graphics)?; + + time += 0.05; + } + + geometry_destroy(mesh)?; + + Ok(()) +} diff --git a/examples/box.rs b/examples/box.rs new file mode 100644 index 0000000..82d9ebb --- /dev/null +++ b/examples/box.rs @@ -0,0 +1,51 @@ +mod glfw; + +use glfw::GlfwContext; +use processing::prelude::*; +use processing_render::render::command::DrawCommand; + +fn main() { + match sketch() { + Ok(_) => { + eprintln!("Sketch completed successfully"); + exit(0).unwrap(); + } + Err(e) => { + eprintln!("Sketch error: {:?}", e); + exit(1).unwrap(); + } + }; +} + +fn sketch() -> error::Result<()> { + let mut glfw_ctx = GlfwContext::new(400, 400)?; + init()?; + + let width = 400; + let height = 400; + let scale_factor = 1.0; + + let surface = glfw_ctx.create_surface(width, height, scale_factor)?; + let graphics = graphics_create(surface, width, height)?; + let box_geo = geometry_box(100.0, 100.0, 100.0)?; + + graphics_mode_3d(graphics)?; + graphics_camera_position(graphics, 100.0, 100.0, 300.0)?; + graphics_camera_look_at(graphics, 0.0, 0.0, 0.0)?; + + let mut angle = 0.0; + + while glfw_ctx.poll_events() { + graphics_begin_draw(graphics)?; + + graphics_record_command(graphics, DrawCommand::PushMatrix)?; + graphics_record_command(graphics, DrawCommand::Rotate { angle })?; + graphics_record_command(graphics, DrawCommand::Geometry(box_geo))?; + graphics_record_command(graphics, DrawCommand::PopMatrix)?; + + graphics_end_draw(graphics)?; + + angle += 0.02; + } + Ok(()) +} diff --git a/examples/custom_attribute.rs b/examples/custom_attribute.rs new file mode 100644 index 0000000..7d3ab69 --- /dev/null +++ b/examples/custom_attribute.rs @@ -0,0 +1,72 @@ +mod glfw; + +use glfw::GlfwContext; +use processing::prelude::*; +use processing_render::geometry::{AttributeFormat, Topology}; +use processing_render::render::command::DrawCommand; + +fn main() { + match sketch() { + Ok(_) => { + eprintln!("Sketch completed successfully"); + exit(0).unwrap(); + } + Err(e) => { + eprintln!("Sketch error: {:?}", e); + exit(1).unwrap(); + } + }; +} + +fn sketch() -> error::Result<()> { + let mut glfw_ctx = GlfwContext::new(600, 600)?; + init()?; + + let width = 600; + let height = 600; + let scale_factor = 1.0; + + let surface = glfw_ctx.create_surface(width, height, scale_factor)?; + let graphics = graphics_create(surface, width, height)?; + + let weight_attr = geometry_attribute_create("Weight", AttributeFormat::Float)?; + + let layout = geometry_layout_create()?; + geometry_layout_add_position(layout)?; + geometry_layout_add_normal(layout)?; + geometry_layout_add_color(layout)?; + geometry_layout_add_attribute(layout, weight_attr)?; + geometry_layout_build(layout)?; + + let mesh = geometry_create_with_layout(layout, Topology::LineStrip)?; + + geometry_color(mesh, 1.0, 0.0, 0.0, 1.0)?; + geometry_normal(mesh, 0.0, 0.0, 1.0)?; + geometry_attribute_float(mesh, weight_attr, 0.0)?; + geometry_vertex(mesh, -50.0, -50.0, 0.0)?; + + geometry_color(mesh, 0.0, 1.0, 0.0, 1.0)?; + geometry_attribute_float(mesh, weight_attr, 0.5)?; + geometry_vertex(mesh, 50.0, -50.0, 0.0)?; + + geometry_color(mesh, 0.0, 0.0, 1.0, 1.0)?; + geometry_attribute_float(mesh, weight_attr, 1.0)?; + geometry_vertex(mesh, 0.0, 50.0, 0.0)?; + + geometry_index(mesh, 0)?; + geometry_index(mesh, 1)?; + geometry_index(mesh, 2)?; + geometry_index(mesh, 0)?; + + graphics_mode_3d(graphics)?; + graphics_camera_position(graphics, 0.0, 0.0, 200.0)?; + graphics_camera_look_at(graphics, 0.0, 0.0, 0.0)?; + + while glfw_ctx.poll_events() { + graphics_begin_draw(graphics)?; + graphics_record_command(graphics, DrawCommand::Geometry(mesh))?; + graphics_end_draw(graphics)?; + + } + Ok(()) +} From 16ff0ac0002836a27ae6cc1bae341bca9a3d736d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Thu, 25 Dec 2025 18:04:59 -0800 Subject: [PATCH 2/5] Fmt. --- crates/processing_ffi/src/lib.rs | 6 +- .../geometry/{attributes.rs => attribute.rs} | 77 ++++++++++++------- .../processing_render/src/geometry/layout.rs | 15 +++- crates/processing_render/src/geometry/mod.rs | 19 +++-- crates/processing_render/src/lib.rs | 64 +++++++++------ examples/animated_mesh.rs | 8 +- examples/custom_attribute.rs | 1 - 7 files changed, 125 insertions(+), 65 deletions(-) rename crates/processing_render/src/geometry/{attributes.rs => attribute.rs} (85%) diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index b9fa5a3..e59431d 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -836,7 +836,6 @@ pub extern "C" fn processing_geometry_index(geo_id: u64, i: u32) { error::check(|| geometry_index(entity, i)); } - #[unsafe(no_mangle)] pub extern "C" fn processing_geometry_vertex_count(geo_id: u64) -> u32 { error::clear_error(); @@ -1009,10 +1008,7 @@ pub extern "C" fn processing_model(window_id: u64, geo_id: u64) { error::clear_error(); let window_entity = Entity::from_bits(window_id); let geo_entity = Entity::from_bits(geo_id); - error::check(|| graphics_record_command( - window_entity, - DrawCommand::Geometry(geo_entity), - )); + error::check(|| graphics_record_command(window_entity, DrawCommand::Geometry(geo_entity))); } #[unsafe(no_mangle)] diff --git a/crates/processing_render/src/geometry/attributes.rs b/crates/processing_render/src/geometry/attribute.rs similarity index 85% rename from crates/processing_render/src/geometry/attributes.rs rename to crates/processing_render/src/geometry/attribute.rs index aa5f027..14cab0a 100644 --- a/crates/processing_render/src/geometry/attributes.rs +++ b/crates/processing_render/src/geometry/attribute.rs @@ -8,7 +8,7 @@ use bevy::{ use crate::error::{ProcessingError, Result}; -use super::{hash_attr_name, Geometry}; +use super::{Geometry, hash_attr_name}; fn clamp_range(range: Range, len: usize) -> Range { range.start.min(len)..range.end.min(len) @@ -168,7 +168,11 @@ impl Attribute { let name: &'static str = Box::leak(name.into().into_boxed_str()); let id = hash_attr_name(name); let inner = MeshVertexAttribute::new(name, id, format.to_vertex_format()); - Self { name, format, inner } + Self { + name, + format, + inner, + } } pub fn from_builtin(inner: MeshVertexAttribute, format: AttributeFormat) -> Self { @@ -195,31 +199,50 @@ pub struct BuiltinAttributes { impl FromWorld for BuiltinAttributes { fn from_world(world: &mut World) -> Self { let position = world - .spawn(Attribute::from_builtin(Mesh::ATTRIBUTE_POSITION, AttributeFormat::Float3)) + .spawn(Attribute::from_builtin( + Mesh::ATTRIBUTE_POSITION, + AttributeFormat::Float3, + )) .id(); let normal = world - .spawn(Attribute::from_builtin(Mesh::ATTRIBUTE_NORMAL, AttributeFormat::Float3)) + .spawn(Attribute::from_builtin( + Mesh::ATTRIBUTE_NORMAL, + AttributeFormat::Float3, + )) .id(); let color = world - .spawn(Attribute::from_builtin(Mesh::ATTRIBUTE_COLOR, AttributeFormat::Float4)) + .spawn(Attribute::from_builtin( + Mesh::ATTRIBUTE_COLOR, + AttributeFormat::Float4, + )) .id(); let uv = world - .spawn(Attribute::from_builtin(Mesh::ATTRIBUTE_UV_0, AttributeFormat::Float2)) + .spawn(Attribute::from_builtin( + Mesh::ATTRIBUTE_UV_0, + AttributeFormat::Float2, + )) .id(); - Self { position, normal, color, uv } + Self { + position, + normal, + color, + uv, + } } } -pub fn create_attribute( +pub fn create( In((name, format)): In<(String, AttributeFormat)>, mut commands: Commands, -) -> Entity { - commands.spawn(Attribute::new(name, format)).id() +) -> Result { + // TODO: validation? + Ok(commands.spawn(Attribute::new(name, format)).id()) } -pub fn destroy_attribute(In(entity): In, mut commands: Commands) { +pub fn destroy(In(entity): In, mut commands: Commands) -> Result<()> { commands.entity(entity).despawn(); + Ok(()) } pub fn get_attribute( @@ -277,18 +300,22 @@ pub fn get_attributes( })?; match attr { - VertexAttributeValues::Float32(v) => { - Ok(v[clamp_range(range, v.len())].iter().map(|&x| AttributeValue::Float(x)).collect()) - } - VertexAttributeValues::Float32x2(v) => { - Ok(v[clamp_range(range, v.len())].iter().map(|&x| AttributeValue::Float2(x)).collect()) - } - VertexAttributeValues::Float32x3(v) => { - Ok(v[clamp_range(range, v.len())].iter().map(|&x| AttributeValue::Float3(x)).collect()) - } - VertexAttributeValues::Float32x4(v) => { - Ok(v[clamp_range(range, v.len())].iter().map(|&x| AttributeValue::Float4(x)).collect()) - } + VertexAttributeValues::Float32(v) => Ok(v[clamp_range(range, v.len())] + .iter() + .map(|&x| AttributeValue::Float(x)) + .collect()), + VertexAttributeValues::Float32x2(v) => Ok(v[clamp_range(range, v.len())] + .iter() + .map(|&x| AttributeValue::Float2(x)) + .collect()), + VertexAttributeValues::Float32x3(v) => Ok(v[clamp_range(range, v.len())] + .iter() + .map(|&x| AttributeValue::Float3(x)) + .collect()), + VertexAttributeValues::Float32x4(v) => Ok(v[clamp_range(range, v.len())] + .iter() + .map(|&x| AttributeValue::Float4(x)) + .collect()), _ => Err(ProcessingError::InvalidArgument( "Unsupported attribute format".into(), )), @@ -346,9 +373,7 @@ pub fn set_attribute( } } -pub fn to_attribute_values( - values: &[AttributeValue], -) -> Option { +pub fn to_attribute_values(values: &[AttributeValue]) -> Option { macro_rules! convert { ($variant:ident, $bevy:ident, $default:expr) => { Some(VertexAttributeValues::$bevy( diff --git a/crates/processing_render/src/geometry/layout.rs b/crates/processing_render/src/geometry/layout.rs index e5c3961..4294004 100644 --- a/crates/processing_render/src/geometry/layout.rs +++ b/crates/processing_render/src/geometry/layout.rs @@ -52,7 +52,10 @@ impl VertexLayout { for attr in attrs { let size = attr.format.size(); - layout_attrs.push(VertexAttribute { attribute: attr, offset }); + layout_attrs.push(VertexAttribute { + attribute: attr, + offset, + }); offset += size; } @@ -105,7 +108,10 @@ impl VertexAttributes { for attr in attrs { let size = attr.format.size(); - layout_attrs.push(VertexAttribute { attribute: attr, offset }); + layout_attrs.push(VertexAttribute { + attribute: attr, + offset, + }); offset += size; } @@ -195,6 +201,9 @@ pub fn build( attributes: layout_attrs, }; - commands.entity(entity).remove::().insert(layout); + commands + .entity(entity) + .remove::() + .insert(layout); true } diff --git a/crates/processing_render/src/geometry/mod.rs b/crates/processing_render/src/geometry/mod.rs index 3a0a126..77d9e84 100644 --- a/crates/processing_render/src/geometry/mod.rs +++ b/crates/processing_render/src/geometry/mod.rs @@ -1,17 +1,19 @@ //! Geometry is a retained-mode representation of 3D mesh data that can be used for efficient //! rendering. Typically, Processing's "sketch" API creates new mesh data every frame, which can be //! inefficient for complex geometries. Geometry is backed by a Bevy [`Mesh`](Mesh) asset. -mod attributes; +pub(crate) mod attribute; pub mod layout; -pub use attributes::*; -pub use layout::{hash_attr_name, VertexAttributes, VertexLayout, VertexAttribute, VertexLayoutBuilder}; +pub use attribute::*; +pub use layout::{ + VertexAttribute, VertexAttributes, VertexLayout, VertexLayoutBuilder, hash_attr_name, +}; use std::collections::HashMap; use bevy::{ asset::RenderAssetUsages, - mesh::{Indices, Meshable, MeshVertexAttributeId, VertexAttributeValues}, + mesh::{Indices, MeshVertexAttributeId, Meshable, VertexAttributeValues}, prelude::*, render::render_resource::PrimitiveTopology, }; @@ -88,7 +90,10 @@ impl Geometry { } fn create_empty_mesh(layout: &VertexLayout, topology: Topology) -> Mesh { - let mut mesh = Mesh::new(topology.to_primitive_topology(), RenderAssetUsages::default()); + let mut mesh = Mesh::new( + topology.to_primitive_topology(), + RenderAssetUsages::default(), + ); for attr in layout.attributes() { let empty_values = match attr.attribute.format { @@ -148,7 +153,9 @@ pub fn create_box( let mesh = cuboid.mesh().build(); let handle = meshes.add(mesh); - commands.spawn(Geometry::new(handle, VertexLayout::default())).id() + commands + .spawn(Geometry::new(handle, VertexLayout::default())) + .id() } pub fn normal(world: &mut World, entity: Entity, nx: f32, ny: f32, nz: f32) -> Result<()> { diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 7a53989..1c3886d 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -230,7 +230,12 @@ fn create_app() -> App { }); app.add_plugins(plugins); - app.add_plugins((ImagePlugin, GraphicsPlugin, SurfacePlugin, geometry::GeometryPlugin)); + app.add_plugins(( + ImagePlugin, + GraphicsPlugin, + SurfacePlugin, + geometry::GeometryPlugin, + )); app.add_systems(First, (clear_transient_meshes, activate_cameras)) .add_systems(Update, flush_draw_commands.before(AssetEventSystems)); @@ -722,7 +727,10 @@ pub fn geometry_layout_add_uv(entity: Entity) -> error::Result<()> { }) } -pub fn geometry_layout_add_attribute(layout_entity: Entity, attr_entity: Entity) -> error::Result<()> { +pub fn geometry_layout_add_attribute( + layout_entity: Entity, + attr_entity: Entity, +) -> error::Result<()> { app_mut(|app| { geometry::layout::add_attribute(app.world_mut(), layout_entity, attr_entity); Ok(()) @@ -738,17 +746,25 @@ pub fn geometry_layout_destroy(entity: Entity) -> error::Result<()> { }) } -pub fn geometry_attribute_create(name: impl Into, format: AttributeFormat) -> error::Result { +pub fn geometry_attribute_create( + name: impl Into, + format: AttributeFormat, +) -> error::Result { app_mut(|app| { - Ok(app - .world_mut() - .run_system_cached_with(geometry::create_attribute, (name.into(), format)) - .unwrap()) + app.world_mut() + .run_system_cached_with(geometry::attribute::create, (name.into(), format)) + .unwrap() }) } pub fn geometry_attribute_position() -> Entity { - app_mut(|app| Ok(app.world().resource::().position)).unwrap() + app_mut(|app| { + Ok(app + .world() + .resource::() + .position) + }) + .unwrap() } pub fn geometry_attribute_normal() -> Entity { @@ -766,8 +782,8 @@ pub fn geometry_attribute_uv() -> Entity { pub fn geometry_attribute_destroy(entity: Entity) -> error::Result<()> { app_mut(|app| { app.world_mut() - .run_system_cached_with(geometry::destroy_attribute, entity) - .unwrap(); + .run_system_cached_with(geometry::attribute::destroy, entity) + .unwrap()?; Ok(()) }) } @@ -795,7 +811,10 @@ pub fn geometry_layout_build(entity: Entity) -> error::Result<()> { }) } -pub fn geometry_create_with_layout(layout_entity: Entity, topology: geometry::Topology) -> error::Result { +pub fn geometry_create_with_layout( + layout_entity: Entity, + topology: geometry::Topology, +) -> error::Result { app_mut(|app| { let layout = app .world() @@ -809,7 +828,10 @@ pub fn geometry_create_with_layout(layout_entity: Entity, topology: geometry::To }) } -pub fn geometry_create_with_attributes(attrs: geometry::VertexAttributes, topology: geometry::Topology) -> error::Result { +pub fn geometry_create_with_attributes( + attrs: geometry::VertexAttributes, + topology: geometry::Topology, +) -> error::Result { app_mut(|app| { Ok(app .world_mut() @@ -873,7 +895,11 @@ pub fn geometry_attribute_float4( z: f32, w: f32, ) -> error::Result<()> { - geometry_attribute(geo_entity, attr_entity, AttributeValue::Float4([x, y, z, w])) + geometry_attribute( + geo_entity, + attr_entity, + AttributeValue::Float4([x, y, z, w]), + ) } pub fn geometry_vertex(entity: Entity, x: f32, y: f32, z: f32) -> error::Result<()> { @@ -944,11 +970,7 @@ pub fn geometry_get_colors( }) } -pub fn geometry_get_uvs( - entity: Entity, - start: usize, - end: usize, -) -> error::Result> { +pub fn geometry_get_uvs(entity: Entity, start: usize, end: usize) -> error::Result> { app_mut(|app| { app.world_mut() .run_system_cached_with(geometry::get_uvs, (entity, start..end)) @@ -956,11 +978,7 @@ pub fn geometry_get_uvs( }) } -pub fn geometry_get_indices( - entity: Entity, - start: usize, - end: usize, -) -> error::Result> { +pub fn geometry_get_indices(entity: Entity, start: usize, end: usize) -> error::Result> { app_mut(|app| { app.world_mut() .run_system_cached_with(geometry::get_indices, (entity, start..end)) diff --git a/examples/animated_mesh.rs b/examples/animated_mesh.rs index 0097730..22e2cd9 100644 --- a/examples/animated_mesh.rs +++ b/examples/animated_mesh.rs @@ -39,7 +39,13 @@ fn sketch() -> error::Result<()> { for x in 0..grid_size { let px = x as f32 * spacing - offset; let pz = z as f32 * spacing - offset; - geometry_color(mesh, x as f32 / grid_size as f32, 0.5, z as f32 / grid_size as f32, 1.0)?; + geometry_color( + mesh, + x as f32 / grid_size as f32, + 0.5, + z as f32 / grid_size as f32, + 1.0, + )?; geometry_normal(mesh, 0.0, 1.0, 0.0)?; geometry_vertex(mesh, px, 0.0, pz)?; } diff --git a/examples/custom_attribute.rs b/examples/custom_attribute.rs index 7d3ab69..b200ba1 100644 --- a/examples/custom_attribute.rs +++ b/examples/custom_attribute.rs @@ -66,7 +66,6 @@ fn sketch() -> error::Result<()> { graphics_begin_draw(graphics)?; graphics_record_command(graphics, DrawCommand::Geometry(mesh))?; graphics_end_draw(graphics)?; - } Ok(()) } From 3f73945f8113c34362e4bf503d127541bd9d3b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 26 Dec 2025 12:58:51 -0800 Subject: [PATCH 3/5] Rewrite to fully normalize on entities. --- crates/processing_ffi/src/lib.rs | 20 -- .../processing_render/src/geometry/layout.rs | 175 ++++-------------- crates/processing_render/src/geometry/mod.rs | 129 ++++++++----- crates/processing_render/src/lib.rs | 33 +--- examples/custom_attribute.rs | 11 +- 5 files changed, 116 insertions(+), 252 deletions(-) diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index e59431d..9f48e37 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -646,13 +646,6 @@ pub extern "C" fn processing_geometry_layout_add_attribute(layout_id: u64, attr_ error::check(|| geometry_layout_add_attribute(layout_entity, attr_entity)); } -#[unsafe(no_mangle)] -pub extern "C" fn processing_geometry_layout_build(layout_id: u64) { - error::clear_error(); - let entity = Entity::from_bits(layout_id); - error::check(|| geometry_layout_build(entity)); -} - #[unsafe(no_mangle)] pub extern "C" fn processing_geometry_layout_destroy(layout_id: u64) { error::clear_error(); @@ -685,19 +678,6 @@ pub extern "C" fn processing_geometry_create(topology: u8) -> u64 { .unwrap_or(0) } -#[unsafe(no_mangle)] -pub extern "C" fn processing_geometry_create_with_attrs(attrs: u32, topology: u8) -> u64 { - error::clear_error(); - let Some(topo) = geometry::Topology::from_u8(topology) else { - error::set_error("Invalid topology"); - return 0; - }; - let attrs = geometry::VertexAttributes::from_bits_truncate(attrs); - error::check(|| geometry_create_with_attributes(attrs, topo)) - .map(|e| e.to_bits()) - .unwrap_or(0) -} - #[unsafe(no_mangle)] pub extern "C" fn processing_geometry_normal(geo_id: u64, nx: f32, ny: f32, nz: f32) { error::clear_error(); diff --git a/crates/processing_render/src/geometry/layout.rs b/crates/processing_render/src/geometry/layout.rs index 4294004..62a3c02 100644 --- a/crates/processing_render/src/geometry/layout.rs +++ b/crates/processing_render/src/geometry/layout.rs @@ -1,6 +1,6 @@ -use bevy::{mesh::MeshVertexAttribute, prelude::*}; +use bevy::prelude::*; -use super::{Attribute, BuiltinAttributes}; +use super::BuiltinAttributes; // bevy requires an attribute id for each unique vertex attribute. we don't really want to // expose this to users, so we hash the attribute name to generate a unique id. in theory @@ -18,18 +18,9 @@ pub const fn hash_attr_name(s: &str) -> u64 { hash } -/// Describes the layout of vertex attributes within a mesh's vertex buffer. -#[derive(Component, Clone, Debug)] +#[derive(Component, Clone, Debug, Default)] pub struct VertexLayout { - attributes: Vec, -} - -/// Represents a single vertex attribute within a vertex layout, including its type and offset -/// within the vertex buffer. -#[derive(Clone, Debug)] -pub struct VertexAttribute { - pub attribute: MeshVertexAttribute, - pub offset: u64, + attributes: Vec, } impl VertexLayout { @@ -39,171 +30,67 @@ impl VertexLayout { } } - pub fn default_layout() -> Self { - let attrs = [ - Mesh::ATTRIBUTE_POSITION, - Mesh::ATTRIBUTE_NORMAL, - Mesh::ATTRIBUTE_COLOR, - Mesh::ATTRIBUTE_UV_0, - ]; - - let mut offset = 0u64; - let mut layout_attrs = Vec::with_capacity(attrs.len()); - - for attr in attrs { - let size = attr.format.size(); - layout_attrs.push(VertexAttribute { - attribute: attr, - offset, - }); - offset += size; - } - - Self { - attributes: layout_attrs, - } + pub fn with_attributes(attrs: Vec) -> Self { + Self { attributes: attrs } } - pub fn attributes(&self) -> &[VertexAttribute] { + pub fn attributes(&self) -> &[Entity] { &self.attributes } - pub fn has_attribute(&self, attr: &MeshVertexAttribute) -> bool { - self.attributes.iter().any(|a| a.attribute.id == attr.id) + pub fn push(&mut self, attr: Entity) { + self.attributes.push(attr); } -} - -impl Default for VertexLayout { - fn default() -> Self { - Self::default_layout() - } -} - -bitflags::bitflags! { - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub struct VertexAttributes: u32 { - const POSITION = 0x01; - const NORMAL = 0x02; - const COLOR = 0x04; - const UV = 0x08; - } -} - -impl VertexAttributes { - pub fn to_layout(self) -> VertexLayout { - let mut attrs = vec![Mesh::ATTRIBUTE_POSITION]; - - if self.contains(VertexAttributes::NORMAL) { - attrs.push(Mesh::ATTRIBUTE_NORMAL); - } - if self.contains(VertexAttributes::COLOR) { - attrs.push(Mesh::ATTRIBUTE_COLOR); - } - if self.contains(VertexAttributes::UV) { - attrs.push(Mesh::ATTRIBUTE_UV_0); - } - - let mut offset = 0u64; - let mut layout_attrs = Vec::with_capacity(attrs.len()); - for attr in attrs { - let size = attr.format.size(); - layout_attrs.push(VertexAttribute { - attribute: attr, - offset, - }); - offset += size; - } - - VertexLayout { - attributes: layout_attrs, - } + pub fn has_attribute(&self, attr_entity: Entity) -> bool { + self.attributes.contains(&attr_entity) } } -#[derive(Component, Clone, Debug, Default)] -pub struct VertexLayoutBuilder { - attributes: Vec, +pub fn create(In(()): In<()>, mut commands: Commands) -> Entity { + commands.spawn(VertexLayout::new()).id() } -pub fn create(In(()): In<()>, mut commands: Commands) -> Entity { - commands.spawn(VertexLayoutBuilder::default()).id() +pub fn create_default(world: &mut World) -> Entity { + let builtins = world.resource::(); + let attrs = vec![builtins.position, builtins.normal, builtins.color, builtins.uv]; + world.spawn(VertexLayout::with_attributes(attrs)).id() } pub fn add_position(world: &mut World, entity: Entity) { - let builtins = world.resource::(); - let position = builtins.position; - if let Some(mut builder) = world.get_mut::(entity) { - builder.attributes.push(position); + let position = world.resource::().position; + if let Some(mut layout) = world.get_mut::(entity) { + layout.push(position); } } pub fn add_normal(world: &mut World, entity: Entity) { - let builtins = world.resource::(); - let normal = builtins.normal; - if let Some(mut builder) = world.get_mut::(entity) { - builder.attributes.push(normal); + let normal = world.resource::().normal; + if let Some(mut layout) = world.get_mut::(entity) { + layout.push(normal); } } pub fn add_color(world: &mut World, entity: Entity) { - let builtins = world.resource::(); - let color = builtins.color; - if let Some(mut builder) = world.get_mut::(entity) { - builder.attributes.push(color); + let color = world.resource::().color; + if let Some(mut layout) = world.get_mut::(entity) { + layout.push(color); } } pub fn add_uv(world: &mut World, entity: Entity) { - let builtins = world.resource::(); - let uv = builtins.uv; - if let Some(mut builder) = world.get_mut::(entity) { - builder.attributes.push(uv); + let uv = world.resource::().uv; + if let Some(mut layout) = world.get_mut::(entity) { + layout.push(uv); } } pub fn add_attribute(world: &mut World, layout_entity: Entity, attr_entity: Entity) { - if let Some(mut builder) = world.get_mut::(layout_entity) { - builder.attributes.push(attr_entity); + if let Some(mut layout) = world.get_mut::(layout_entity) { + layout.push(attr_entity); } } pub fn destroy(In(entity): In, mut commands: Commands) { commands.entity(entity).despawn(); } - -pub fn build( - In(entity): In, - mut commands: Commands, - builders: Query<&VertexLayoutBuilder>, - attrs: Query<&Attribute>, -) -> bool { - let Ok(builder) = builders.get(entity) else { - return false; - }; - - let mut offset = 0u64; - let mut layout_attrs = Vec::with_capacity(builder.attributes.len()); - - for &attr_entity in &builder.attributes { - let Ok(attr) = attrs.get(attr_entity) else { - return false; - }; - let size = attr.inner.format.size(); - layout_attrs.push(VertexAttribute { - attribute: attr.inner.clone(), - offset, - }); - offset += size; - } - - let layout = VertexLayout { - attributes: layout_attrs, - }; - - commands - .entity(entity) - .remove::() - .insert(layout); - true -} diff --git a/crates/processing_render/src/geometry/mod.rs b/crates/processing_render/src/geometry/mod.rs index 77d9e84..7e07a28 100644 --- a/crates/processing_render/src/geometry/mod.rs +++ b/crates/processing_render/src/geometry/mod.rs @@ -5,9 +5,7 @@ pub(crate) mod attribute; pub mod layout; pub use attribute::*; -pub use layout::{ - VertexAttribute, VertexAttributes, VertexLayout, VertexLayoutBuilder, hash_attr_name, -}; +pub use layout::{VertexLayout, hash_attr_name}; use std::collections::HashMap; @@ -65,7 +63,7 @@ impl Topology { #[derive(Component)] pub struct Geometry { pub handle: Handle, - pub layout: VertexLayout, + pub layout: Entity, pub current_normal: [f32; 3], pub current_color: [f32; 4], pub current_uv: [f32; 2], @@ -73,7 +71,7 @@ pub struct Geometry { } impl Geometry { - pub fn new(handle: Handle, layout: VertexLayout) -> Self { + pub fn new(handle: Handle, layout: Entity) -> Self { Self { handle, layout, @@ -83,20 +81,52 @@ impl Geometry { custom_current: HashMap::new(), } } +} - pub fn layout(&self) -> &VertexLayout { - &self.layout - } +pub fn create( + In(topology): In, + mut commands: Commands, + mut meshes: ResMut>, + builtins: Res, +) -> Entity { + let layout_entity = commands + .spawn(VertexLayout::with_attributes(vec![ + builtins.position, + builtins.normal, + builtins.color, + builtins.uv, + ])) + .id(); + + let mut mesh = Mesh::new( + topology.to_primitive_topology(), + RenderAssetUsages::default(), + ); + mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, Vec::<[f32; 3]>::new()); + mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, Vec::<[f32; 3]>::new()); + mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, Vec::<[f32; 4]>::new()); + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, Vec::<[f32; 2]>::new()); + + let handle = meshes.add(mesh); + commands.spawn(Geometry::new(handle, layout_entity)).id() } -fn create_empty_mesh(layout: &VertexLayout, topology: Topology) -> Mesh { +pub fn create_with_layout( + In((layout_entity, topology)): In<(Entity, Topology)>, + mut commands: Commands, + mut meshes: ResMut>, + layouts: Query<&VertexLayout>, + attrs: Query<&Attribute>, +) -> Entity { + let layout = layouts.get(layout_entity).unwrap(); let mut mesh = Mesh::new( topology.to_primitive_topology(), RenderAssetUsages::default(), ); - for attr in layout.attributes() { - let empty_values = match attr.attribute.format { + for &attr_entity in layout.attributes() { + let attr = attrs.get(attr_entity).unwrap(); + let empty_values = match attr.inner.format { bevy::render::render_resource::VertexFormat::Float32 => { VertexAttributeValues::Float32(Vec::new()) } @@ -111,51 +141,33 @@ fn create_empty_mesh(layout: &VertexLayout, topology: Topology) -> Mesh { } _ => continue, }; - mesh.insert_attribute(attr.attribute.clone(), empty_values); + mesh.insert_attribute(attr.inner.clone(), empty_values); } - mesh -} - -pub fn create( - In(topology): In, - commands: Commands, - meshes: ResMut>, -) -> Entity { - create_with_layout(In((VertexLayout::default(), topology)), commands, meshes) -} - -pub fn create_with_layout( - In((layout, topology)): In<(VertexLayout, Topology)>, - mut commands: Commands, - mut meshes: ResMut>, -) -> Entity { - let mesh = create_empty_mesh(&layout, topology); let handle = meshes.add(mesh); - - commands.spawn(Geometry::new(handle, layout)).id() -} - -pub fn create_with_attributes( - In((attrs, topology)): In<(VertexAttributes, Topology)>, - commands: Commands, - meshes: ResMut>, -) -> Entity { - create_with_layout(In((attrs.to_layout(), topology)), commands, meshes) + commands.spawn(Geometry::new(handle, layout_entity)).id() } pub fn create_box( In((width, height, depth)): In<(f32, f32, f32)>, mut commands: Commands, mut meshes: ResMut>, + builtins: Res, ) -> Entity { let cuboid = Cuboid::new(width, height, depth); let mesh = cuboid.mesh().build(); let handle = meshes.add(mesh); - commands - .spawn(Geometry::new(handle, VertexLayout::default())) - .id() + let layout_entity = commands + .spawn(VertexLayout::with_attributes(vec![ + builtins.position, + builtins.normal, + builtins.color, + builtins.uv, + ])) + .id(); + + commands.spawn(Geometry::new(handle, layout_entity)).id() } pub fn normal(world: &mut World, entity: Entity, nx: f32, ny: f32, nz: f32) -> Result<()> { @@ -201,13 +213,20 @@ pub fn attribute( pub fn vertex( In((entity, x, y, z)): In<(Entity, f32, f32, f32)>, - mut geometries: Query<&mut Geometry>, + geometries: Query<&Geometry>, + layouts: Query<&VertexLayout>, + attrs: Query<&Attribute>, + builtins: Res, mut meshes: ResMut>, ) -> Result<()> { let geometry = geometries - .get_mut(entity) + .get(entity) .map_err(|_| ProcessingError::GeometryNotFound)?; + let layout = layouts + .get(geometry.layout) + .map_err(|_| ProcessingError::LayoutNotFound)?; + let mesh = meshes .get_mut(&geometry.handle) .ok_or(ProcessingError::GeometryNotFound)?; @@ -218,7 +237,7 @@ pub fn vertex( positions.push([x, y, z]); } - if geometry.layout.has_attribute(&Mesh::ATTRIBUTE_NORMAL) { + if layout.has_attribute(builtins.normal) { if let Some(VertexAttributeValues::Float32x3(normals)) = mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL) { @@ -226,7 +245,7 @@ pub fn vertex( } } - if geometry.layout.has_attribute(&Mesh::ATTRIBUTE_COLOR) { + if layout.has_attribute(builtins.color) { if let Some(VertexAttributeValues::Float32x4(colors)) = mesh.attribute_mut(Mesh::ATTRIBUTE_COLOR) { @@ -234,7 +253,7 @@ pub fn vertex( } } - if geometry.layout.has_attribute(&Mesh::ATTRIBUTE_UV_0) { + if layout.has_attribute(builtins.uv) { if let Some(VertexAttributeValues::Float32x2(uvs)) = mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0) { @@ -242,9 +261,19 @@ pub fn vertex( } } - for attr in geometry.layout.attributes() { - if let Some(current) = geometry.custom_current.get(&attr.attribute.id) { - match (mesh.attribute_mut(attr.attribute.clone()), current) { + for &attr_entity in layout.attributes() { + if attr_entity == builtins.position + || attr_entity == builtins.normal + || attr_entity == builtins.color + || attr_entity == builtins.uv + { + continue; + } + let attr = attrs + .get(attr_entity) + .map_err(|_| ProcessingError::InvalidEntity)?; + if let Some(current) = geometry.custom_current.get(&attr.inner.id) { + match (mesh.attribute_mut(attr.inner.clone()), current) { (Some(VertexAttributeValues::Float32(values)), AttributeValue::Float(v)) => { values.push(*v); } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 1c3886d..557a131 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -797,45 +797,14 @@ pub fn geometry_create(topology: geometry::Topology) -> error::Result { }) } -pub fn geometry_layout_build(entity: Entity) -> error::Result<()> { - app_mut(|app| { - let success = app - .world_mut() - .run_system_cached_with(geometry::layout::build, entity) - .unwrap(); - if success { - Ok(()) - } else { - Err(error::ProcessingError::LayoutNotFound) - } - }) -} - pub fn geometry_create_with_layout( layout_entity: Entity, topology: geometry::Topology, -) -> error::Result { - app_mut(|app| { - let layout = app - .world() - .get::(layout_entity) - .ok_or(error::ProcessingError::LayoutNotFound)? - .clone(); - Ok(app - .world_mut() - .run_system_cached_with(geometry::create_with_layout, (layout, topology)) - .unwrap()) - }) -} - -pub fn geometry_create_with_attributes( - attrs: geometry::VertexAttributes, - topology: geometry::Topology, ) -> error::Result { app_mut(|app| { Ok(app .world_mut() - .run_system_cached_with(geometry::create_with_attributes, (attrs, topology)) + .run_system_cached_with(geometry::create_with_layout, (layout_entity, topology)) .unwrap()) }) } diff --git a/examples/custom_attribute.rs b/examples/custom_attribute.rs index b200ba1..c71b0d0 100644 --- a/examples/custom_attribute.rs +++ b/examples/custom_attribute.rs @@ -29,28 +29,27 @@ fn sketch() -> error::Result<()> { let surface = glfw_ctx.create_surface(width, height, scale_factor)?; let graphics = graphics_create(surface, width, height)?; - let weight_attr = geometry_attribute_create("Weight", AttributeFormat::Float)?; + let custom_attr = geometry_attribute_create("Custom", AttributeFormat::Float)?; let layout = geometry_layout_create()?; geometry_layout_add_position(layout)?; geometry_layout_add_normal(layout)?; geometry_layout_add_color(layout)?; - geometry_layout_add_attribute(layout, weight_attr)?; - geometry_layout_build(layout)?; + geometry_layout_add_attribute(layout, custom_attr)?; let mesh = geometry_create_with_layout(layout, Topology::LineStrip)?; geometry_color(mesh, 1.0, 0.0, 0.0, 1.0)?; geometry_normal(mesh, 0.0, 0.0, 1.0)?; - geometry_attribute_float(mesh, weight_attr, 0.0)?; + geometry_attribute_float(mesh, custom_attr, 0.0)?; geometry_vertex(mesh, -50.0, -50.0, 0.0)?; geometry_color(mesh, 0.0, 1.0, 0.0, 1.0)?; - geometry_attribute_float(mesh, weight_attr, 0.5)?; + geometry_attribute_float(mesh, custom_attr, 0.5)?; geometry_vertex(mesh, 50.0, -50.0, 0.0)?; geometry_color(mesh, 0.0, 0.0, 1.0, 1.0)?; - geometry_attribute_float(mesh, weight_attr, 1.0)?; + geometry_attribute_float(mesh, custom_attr, 1.0)?; geometry_vertex(mesh, 0.0, 50.0, 0.0)?; geometry_index(mesh, 0)?; From 723ccf934ea63ce386d93672af3f4a848f1f2e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 26 Dec 2025 13:19:13 -0800 Subject: [PATCH 4/5] More cleanup. --- Cargo.lock | 1 - crates/processing_render/Cargo.toml | 1 - .../src/geometry/attribute.rs | 36 +++-------- .../processing_render/src/geometry/layout.rs | 62 ++++++++++++------- crates/processing_render/src/geometry/mod.rs | 43 ++++++------- crates/processing_render/src/lib.rs | 36 +++-------- 6 files changed, 80 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3330251..5e14b7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4426,7 +4426,6 @@ name = "processing_render" version = "0.1.0" dependencies = [ "bevy", - "bitflags 2.10.0", "crossbeam-channel", "half", "js-sys", diff --git a/crates/processing_render/Cargo.toml b/crates/processing_render/Cargo.toml index f68ff76..72272e3 100644 --- a/crates/processing_render/Cargo.toml +++ b/crates/processing_render/Cargo.toml @@ -13,7 +13,6 @@ x11 = ["bevy/x11"] [dependencies] bevy = { workspace = true } -bitflags = "2" lyon = "1.0" raw-window-handle = "0.6" thiserror = "2" diff --git a/crates/processing_render/src/geometry/attribute.rs b/crates/processing_render/src/geometry/attribute.rs index 14cab0a..d1384a5 100644 --- a/crates/processing_render/src/geometry/attribute.rs +++ b/crates/processing_render/src/geometry/attribute.rs @@ -40,7 +40,7 @@ fn get_mesh_mut<'a>( .ok_or(ProcessingError::GeometryNotFound) } -macro_rules! impl_bulk_getter { +macro_rules! impl_getter { ($name:ident, $attr:expr, $variant:ident, $type:ty) => { pub fn $name( In((entity, range)): In<(Entity, Range)>, @@ -61,10 +61,10 @@ macro_rules! impl_bulk_getter { }; } -impl_bulk_getter!(get_positions, Mesh::ATTRIBUTE_POSITION, Float32x3, [f32; 3]); -impl_bulk_getter!(get_normals, Mesh::ATTRIBUTE_NORMAL, Float32x3, [f32; 3]); -impl_bulk_getter!(get_colors, Mesh::ATTRIBUTE_COLOR, Float32x4, [f32; 4]); -impl_bulk_getter!(get_uvs, Mesh::ATTRIBUTE_UV_0, Float32x2, [f32; 2]); +impl_getter!(get_positions, Mesh::ATTRIBUTE_POSITION, Float32x3, [f32; 3]); +impl_getter!(get_normals, Mesh::ATTRIBUTE_NORMAL, Float32x3, [f32; 3]); +impl_getter!(get_colors, Mesh::ATTRIBUTE_COLOR, Float32x4, [f32; 4]); +impl_getter!(get_uvs, Mesh::ATTRIBUTE_UV_0, Float32x2, [f32; 2]); pub fn get_indices( In((entity, range)): In<(Entity, Range)>, @@ -165,6 +165,9 @@ pub struct Attribute { impl Attribute { pub fn new(name: impl Into, format: AttributeFormat) -> Self { + // we leak here to get a 'static str for the attribute name, but this is okay because + // we never expect to unload attributes during the lifetime of the application + // and attribute names are generally small in number let name: &'static str = Box::leak(name.into().into_boxed_str()); let id = hash_attr_name(name); let inner = MeshVertexAttribute::new(name, id, format.to_vertex_format()); @@ -372,26 +375,3 @@ pub fn set_attribute( )), } } - -pub fn to_attribute_values(values: &[AttributeValue]) -> Option { - macro_rules! convert { - ($variant:ident, $bevy:ident, $default:expr) => { - Some(VertexAttributeValues::$bevy( - values - .iter() - .map(|v| match v { - AttributeValue::$variant(x) => *x, - _ => $default, - }) - .collect(), - )) - }; - } - - match values.first()? { - AttributeValue::Float(_) => convert!(Float, Float32, 0.0), - AttributeValue::Float2(_) => convert!(Float2, Float32x2, [0.0; 2]), - AttributeValue::Float3(_) => convert!(Float3, Float32x3, [0.0; 3]), - AttributeValue::Float4(_) => convert!(Float4, Float32x4, [0.0; 4]), - } -} diff --git a/crates/processing_render/src/geometry/layout.rs b/crates/processing_render/src/geometry/layout.rs index 62a3c02..5b88a38 100644 --- a/crates/processing_render/src/geometry/layout.rs +++ b/crates/processing_render/src/geometry/layout.rs @@ -1,6 +1,7 @@ use bevy::prelude::*; use super::BuiltinAttributes; +use crate::error::{ProcessingError, Result}; // bevy requires an attribute id for each unique vertex attribute. we don't really want to // expose this to users, so we hash the attribute name to generate a unique id. in theory @@ -39,7 +40,9 @@ impl VertexLayout { } pub fn push(&mut self, attr: Entity) { - self.attributes.push(attr); + if !self.attributes.contains(&attr) { + self.attributes.push(attr); + } } pub fn has_attribute(&self, attr_entity: Entity) -> bool { @@ -53,42 +56,57 @@ pub fn create(In(()): In<()>, mut commands: Commands) -> Entity { pub fn create_default(world: &mut World) -> Entity { let builtins = world.resource::(); - let attrs = vec![builtins.position, builtins.normal, builtins.color, builtins.uv]; + let attrs = vec![ + builtins.position, + builtins.normal, + builtins.color, + builtins.uv, + ]; world.spawn(VertexLayout::with_attributes(attrs)).id() } -pub fn add_position(world: &mut World, entity: Entity) { +pub fn add_position(world: &mut World, entity: Entity) -> Result<()> { let position = world.resource::().position; - if let Some(mut layout) = world.get_mut::(entity) { - layout.push(position); - } + let mut layout = world + .get_mut::(entity) + .ok_or(ProcessingError::LayoutNotFound)?; + layout.push(position); + Ok(()) } -pub fn add_normal(world: &mut World, entity: Entity) { +pub fn add_normal(world: &mut World, entity: Entity) -> Result<()> { let normal = world.resource::().normal; - if let Some(mut layout) = world.get_mut::(entity) { - layout.push(normal); - } + let mut layout = world + .get_mut::(entity) + .ok_or(ProcessingError::LayoutNotFound)?; + layout.push(normal); + Ok(()) } -pub fn add_color(world: &mut World, entity: Entity) { +pub fn add_color(world: &mut World, entity: Entity) -> Result<()> { let color = world.resource::().color; - if let Some(mut layout) = world.get_mut::(entity) { - layout.push(color); - } + let mut layout = world + .get_mut::(entity) + .ok_or(ProcessingError::LayoutNotFound)?; + layout.push(color); + Ok(()) } -pub fn add_uv(world: &mut World, entity: Entity) { +pub fn add_uv(world: &mut World, entity: Entity) -> Result<()> { let uv = world.resource::().uv; - if let Some(mut layout) = world.get_mut::(entity) { - layout.push(uv); - } + let mut layout = world + .get_mut::(entity) + .ok_or(ProcessingError::LayoutNotFound)?; + layout.push(uv); + Ok(()) } -pub fn add_attribute(world: &mut World, layout_entity: Entity, attr_entity: Entity) { - if let Some(mut layout) = world.get_mut::(layout_entity) { - layout.push(attr_entity); - } +pub fn add_attribute(world: &mut World, layout_entity: Entity, attr_entity: Entity) -> Result<()> { + let mut layout = world + .get_mut::(layout_entity) + .ok_or(ProcessingError::LayoutNotFound)?; + layout.push(attr_entity); + Ok(()) } pub fn destroy(In(entity): In, mut commands: Commands) { diff --git a/crates/processing_render/src/geometry/mod.rs b/crates/processing_render/src/geometry/mod.rs index 7e07a28..95f50fb 100644 --- a/crates/processing_render/src/geometry/mod.rs +++ b/crates/processing_render/src/geometry/mod.rs @@ -117,15 +117,19 @@ pub fn create_with_layout( mut meshes: ResMut>, layouts: Query<&VertexLayout>, attrs: Query<&Attribute>, -) -> Entity { - let layout = layouts.get(layout_entity).unwrap(); +) -> Result { + let layout = layouts + .get(layout_entity) + .map_err(|_| ProcessingError::LayoutNotFound)?; let mut mesh = Mesh::new( topology.to_primitive_topology(), RenderAssetUsages::default(), ); for &attr_entity in layout.attributes() { - let attr = attrs.get(attr_entity).unwrap(); + let attr = attrs + .get(attr_entity) + .map_err(|_| ProcessingError::InvalidEntity)?; let empty_values = match attr.inner.format { bevy::render::render_resource::VertexFormat::Float32 => { VertexAttributeValues::Float32(Vec::new()) @@ -141,11 +145,11 @@ pub fn create_with_layout( } _ => continue, }; - mesh.insert_attribute(attr.inner.clone(), empty_values); + mesh.insert_attribute(attr.inner, empty_values); } let handle = meshes.add(mesh); - commands.spawn(Geometry::new(handle, layout_entity)).id() + Ok(commands.spawn(Geometry::new(handle, layout_entity)).id()) } pub fn create_box( @@ -237,28 +241,25 @@ pub fn vertex( positions.push([x, y, z]); } - if layout.has_attribute(builtins.normal) { - if let Some(VertexAttributeValues::Float32x3(normals)) = + if layout.has_attribute(builtins.normal) + && let Some(VertexAttributeValues::Float32x3(normals)) = mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL) - { - normals.push(geometry.current_normal); - } + { + normals.push(geometry.current_normal); } - if layout.has_attribute(builtins.color) { - if let Some(VertexAttributeValues::Float32x4(colors)) = + if layout.has_attribute(builtins.color) + && let Some(VertexAttributeValues::Float32x4(colors)) = mesh.attribute_mut(Mesh::ATTRIBUTE_COLOR) - { - colors.push(geometry.current_color); - } + { + colors.push(geometry.current_color); } - if layout.has_attribute(builtins.uv) { - if let Some(VertexAttributeValues::Float32x2(uvs)) = + if layout.has_attribute(builtins.uv) + && let Some(VertexAttributeValues::Float32x2(uvs)) = mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0) - { - uvs.push(geometry.current_uv); - } + { + uvs.push(geometry.current_uv); } for &attr_entity in layout.attributes() { @@ -273,7 +274,7 @@ pub fn vertex( .get(attr_entity) .map_err(|_| ProcessingError::InvalidEntity)?; if let Some(current) = geometry.custom_current.get(&attr.inner.id) { - match (mesh.attribute_mut(attr.inner.clone()), current) { + match (mesh.attribute_mut(attr.inner), current) { (Some(VertexAttributeValues::Float32(values)), AttributeValue::Float(v)) => { values.push(*v); } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 557a131..0e35a8c 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -700,41 +700,26 @@ pub fn geometry_layout_create() -> error::Result { } pub fn geometry_layout_add_position(entity: Entity) -> error::Result<()> { - app_mut(|app| { - geometry::layout::add_position(app.world_mut(), entity); - Ok(()) - }) + app_mut(|app| geometry::layout::add_position(app.world_mut(), entity)) } pub fn geometry_layout_add_normal(entity: Entity) -> error::Result<()> { - app_mut(|app| { - geometry::layout::add_normal(app.world_mut(), entity); - Ok(()) - }) + app_mut(|app| geometry::layout::add_normal(app.world_mut(), entity)) } pub fn geometry_layout_add_color(entity: Entity) -> error::Result<()> { - app_mut(|app| { - geometry::layout::add_color(app.world_mut(), entity); - Ok(()) - }) + app_mut(|app| geometry::layout::add_color(app.world_mut(), entity)) } pub fn geometry_layout_add_uv(entity: Entity) -> error::Result<()> { - app_mut(|app| { - geometry::layout::add_uv(app.world_mut(), entity); - Ok(()) - }) + app_mut(|app| geometry::layout::add_uv(app.world_mut(), entity)) } pub fn geometry_layout_add_attribute( layout_entity: Entity, attr_entity: Entity, ) -> error::Result<()> { - app_mut(|app| { - geometry::layout::add_attribute(app.world_mut(), layout_entity, attr_entity); - Ok(()) - }) + app_mut(|app| geometry::layout::add_attribute(app.world_mut(), layout_entity, attr_entity)) } pub fn geometry_layout_destroy(entity: Entity) -> error::Result<()> { @@ -802,10 +787,9 @@ pub fn geometry_create_with_layout( topology: geometry::Topology, ) -> error::Result { app_mut(|app| { - Ok(app - .world_mut() + app.world_mut() .run_system_cached_with(geometry::create_with_layout, (layout_entity, topology)) - .unwrap()) + .unwrap() }) } @@ -1024,7 +1008,7 @@ pub fn geometry_get_attribute( .world() .get::(attr_entity) .ok_or(error::ProcessingError::InvalidEntity)?; - let inner = attr.inner.clone(); + let inner = attr.inner; app.world_mut() .run_system_cached_with(geometry::get_attribute, (geo_entity, inner, index)) .unwrap() @@ -1042,7 +1026,7 @@ pub fn geometry_get_attributes( .world() .get::(attr_entity) .ok_or(error::ProcessingError::InvalidEntity)?; - let inner = attr.inner.clone(); + let inner = attr.inner; app.world_mut() .run_system_cached_with(geometry::get_attributes, (geo_entity, inner, start..end)) .unwrap() @@ -1060,7 +1044,7 @@ pub fn geometry_set_attribute( .world() .get::(attr_entity) .ok_or(error::ProcessingError::InvalidEntity)?; - let inner = attr.inner.clone(); + let inner = attr.inner; app.world_mut() .run_system_cached_with(geometry::set_attribute, (geo_entity, inner, index, value)) .unwrap() From dcb48d97a5e44146edd4edcf60c11b52426ae2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 26 Dec 2025 13:41:50 -0800 Subject: [PATCH 5/5] More cleanup. --- crates/processing_ffi/src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 9f48e37..ec63d91 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -586,11 +586,6 @@ pub extern "C" fn processing_ortho( error::check(|| graphics_ortho(window_entity, left, right, bottom, top, near, far)); } -pub const PROCESSING_ATTR_POSITION: u32 = 0x01; -pub const PROCESSING_ATTR_NORMAL: u32 = 0x02; -pub const PROCESSING_ATTR_COLOR: u32 = 0x04; -pub const PROCESSING_ATTR_UV: u32 = 0x08; - pub const PROCESSING_ATTR_FORMAT_FLOAT: u8 = 1; pub const PROCESSING_ATTR_FORMAT_FLOAT2: u8 = 2; pub const PROCESSING_ATTR_FORMAT_FLOAT3: u8 = 3;