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..ec63d91 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -585,3 +585,411 @@ 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_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_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_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/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/attribute.rs b/crates/processing_render/src/geometry/attribute.rs new file mode 100644 index 0000000..d1384a5 --- /dev/null +++ b/crates/processing_render/src/geometry/attribute.rs @@ -0,0 +1,377 @@ +use std::ops::Range; + +use bevy::{ + mesh::{Indices, MeshVertexAttribute, VertexAttributeValues}, + prelude::*, + render::render_resource::VertexFormat, +}; + +use crate::error::{ProcessingError, Result}; + +use super::{Geometry, hash_attr_name}; + +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_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_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)>, + 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 { + // 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()); + 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( + In((name, format)): In<(String, AttributeFormat)>, + mut commands: Commands, +) -> Result { + // TODO: validation? + Ok(commands.spawn(Attribute::new(name, format)).id()) +} + +pub fn destroy(In(entity): In, mut commands: Commands) -> Result<()> { + commands.entity(entity).despawn(); + Ok(()) +} + +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(), + )), + } +} diff --git a/crates/processing_render/src/geometry/layout.rs b/crates/processing_render/src/geometry/layout.rs new file mode 100644 index 0000000..5b88a38 --- /dev/null +++ b/crates/processing_render/src/geometry/layout.rs @@ -0,0 +1,114 @@ +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 +// 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 +} + +#[derive(Component, Clone, Debug, Default)] +pub struct VertexLayout { + attributes: Vec, +} + +impl VertexLayout { + pub fn new() -> Self { + Self { + attributes: Vec::new(), + } + } + + pub fn with_attributes(attrs: Vec) -> Self { + Self { attributes: attrs } + } + + pub fn attributes(&self) -> &[Entity] { + &self.attributes + } + + pub fn push(&mut self, attr: Entity) { + if !self.attributes.contains(&attr) { + self.attributes.push(attr); + } + } + + pub fn has_attribute(&self, attr_entity: Entity) -> bool { + self.attributes.contains(&attr_entity) + } +} + +pub fn create(In(()): In<()>, mut commands: Commands) -> Entity { + commands.spawn(VertexLayout::new()).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) -> Result<()> { + let position = world.resource::().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) -> Result<()> { + let normal = world.resource::().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) -> Result<()> { + let color = world.resource::().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) -> Result<()> { + let uv = world.resource::().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) -> 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) { + commands.entity(entity).despawn(); +} diff --git a/crates/processing_render/src/geometry/mod.rs b/crates/processing_render/src/geometry/mod.rs new file mode 100644 index 0000000..95f50fb --- /dev/null +++ b/crates/processing_render/src/geometry/mod.rs @@ -0,0 +1,367 @@ +//! 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. +pub(crate) mod attribute; +pub mod layout; + +pub use attribute::*; +pub use layout::{VertexLayout, hash_attr_name}; + +use std::collections::HashMap; + +use bevy::{ + asset::RenderAssetUsages, + mesh::{Indices, MeshVertexAttributeId, Meshable, 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: Entity, + 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: Entity) -> 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 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() +} + +pub fn create_with_layout( + In((layout_entity, topology)): In<(Entity, Topology)>, + mut commands: Commands, + mut meshes: ResMut>, + layouts: Query<&VertexLayout>, + attrs: Query<&Attribute>, +) -> 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) + .map_err(|_| ProcessingError::InvalidEntity)?; + let empty_values = match attr.inner.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.inner, empty_values); + } + + let handle = meshes.add(mesh); + Ok(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); + + 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<()> { + 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)>, + geometries: Query<&Geometry>, + layouts: Query<&VertexLayout>, + attrs: Query<&Attribute>, + builtins: Res, + mut meshes: ResMut>, +) -> Result<()> { + let geometry = geometries + .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)?; + + if let Some(VertexAttributeValues::Float32x3(positions)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION) + { + positions.push([x, y, z]); + } + + if layout.has_attribute(builtins.normal) + && let Some(VertexAttributeValues::Float32x3(normals)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL) + { + normals.push(geometry.current_normal); + } + + if layout.has_attribute(builtins.color) + && let Some(VertexAttributeValues::Float32x4(colors)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_COLOR) + { + colors.push(geometry.current_color); + } + + if layout.has_attribute(builtins.uv) + && let Some(VertexAttributeValues::Float32x2(uvs)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0) + { + uvs.push(geometry.current_uv); + } + + 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), 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..0e35a8c 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,12 @@ 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 +689,373 @@ 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)) +} + +pub fn geometry_layout_add_normal(entity: Entity) -> error::Result<()> { + 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)) +} + +pub fn geometry_layout_add_uv(entity: Entity) -> error::Result<()> { + 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)) +} + +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| { + 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() +} + +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::attribute::destroy, 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_create_with_layout( + layout_entity: Entity, + topology: geometry::Topology, +) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(geometry::create_with_layout, (layout_entity, 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; + 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; + 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; + 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..22e2cd9 --- /dev/null +++ b/examples/animated_mesh.rs @@ -0,0 +1,98 @@ +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..c71b0d0 --- /dev/null +++ b/examples/custom_attribute.rs @@ -0,0 +1,70 @@ +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 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, 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, 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, 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, custom_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(()) +}