diff --git a/Cargo.toml b/Cargo.toml index 5540d14..91cab9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,14 @@ path = "examples/custom_attribute.rs" name = "lights" path = "examples/lights.rs" +[[example]] +name = "materials" +path = "examples/materials.rs" + +[[example]] +name = "pbr" +path = "examples/pbr.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 ad795cf..23e2cfa 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -1092,6 +1092,14 @@ pub extern "C" fn processing_geometry_box(width: f32, height: f32, depth: f32) - .unwrap_or(0) } +#[unsafe(no_mangle)] +pub extern "C" fn processing_geometry_sphere(radius: f32, sectors: u32, stacks: u32) -> u64 { + error::clear_error(); + error::check(|| geometry_sphere(radius, sectors, stacks)) + .map(|e| e.to_bits()) + .unwrap_or(0) +} + #[unsafe(no_mangle)] pub extern "C" fn processing_light_create_directional( graphics_id: u64, @@ -1146,3 +1154,62 @@ pub extern "C" fn processing_light_create_spot( .map(|e| e.to_bits()) .unwrap_or(0) } + +#[unsafe(no_mangle)] +pub extern "C" fn processing_material_create_pbr() -> u64 { + error::clear_error(); + error::check(|| material_create_pbr()) + .map(|e| e.to_bits()) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_material_set_float( + mat_id: u64, + name: *const std::ffi::c_char, + value: f32, +) { + error::clear_error(); + let name = unsafe { std::ffi::CStr::from_ptr(name) }.to_str().unwrap(); + error::check(|| { + material_set( + Entity::from_bits(mat_id), + name, + material::MaterialValue::Float(value), + ) + }); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_material_set_float4( + mat_id: u64, + name: *const std::ffi::c_char, + r: f32, + g: f32, + b: f32, + a: f32, +) { + error::clear_error(); + let name = unsafe { std::ffi::CStr::from_ptr(name) }.to_str().unwrap(); + error::check(|| { + material_set( + Entity::from_bits(mat_id), + name, + material::MaterialValue::Float4([r, g, b, a]), + ) + }); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_material_destroy(mat_id: u64) { + error::clear_error(); + error::check(|| material_destroy(Entity::from_bits(mat_id))); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_material(window_id: u64, mat_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + let mat_entity = Entity::from_bits(mat_id); + error::check(|| graphics_record_command(window_entity, DrawCommand::Material(mat_entity))); +} diff --git a/crates/processing_pyo3/examples/materials.py b/crates/processing_pyo3/examples/materials.py new file mode 100644 index 0000000..9a96ef5 --- /dev/null +++ b/crates/processing_pyo3/examples/materials.py @@ -0,0 +1,27 @@ +from processing import * + +mat = None + +def setup(): + global mat + size(800, 600) + mode_3d() + + dir_light = create_directional_light(1.0, 0.98, 0.95, 1500.0) + point_light = create_point_light(1.0, 1.0, 1.0, 100000.0, 800.0, 0.0) + point_light.position(200.0, 200.0, 400.0) + + mat = Material() + mat.set_float("roughness", 0.3) + mat.set_float("metallic", 0.8) + mat.set_float4("base_color", 1.0, 0.85, 0.57, 1.0) + +def draw(): + camera_position(0.0, 0.0, 200.0) + camera_look_at(0.0, 0.0, 0.0) + background(12, 12, 18) + + use_material(mat) + draw_sphere(50.0) + +run() diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 7ddef92..a14f6db 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -293,11 +293,23 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + pub fn draw_sphere(&self, radius: f32, sectors: u32, stacks: u32) -> PyResult<()> { + let sphere_geo = geometry_sphere(radius, sectors, stacks) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + graphics_record_command(self.entity, DrawCommand::Geometry(sphere_geo)) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn draw_geometry(&self, geometry: &Geometry) -> PyResult<()> { graphics_record_command(self.entity, DrawCommand::Geometry(geometry.entity)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + pub fn use_material(&self, material: &crate::material::Material) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::Material(material.entity)) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn scale(&self, x: f32, y: f32) -> PyResult<()> { graphics_record_command(self.entity, DrawCommand::Scale { x, y }) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) @@ -313,6 +325,11 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + pub fn set_material(&self, material: &crate::material::Material) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::Material(material.entity)) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn begin_draw(&self) -> PyResult<()> { graphics_begin_draw(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 8d56118..3ab375a 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -10,8 +10,10 @@ //! functions that forward to a singleton Graphics object pub(crate) behind the scenes. mod glfw; mod graphics; +pub(crate) mod material; use graphics::{Geometry, Graphics, Image, Light, Topology, get_graphics, get_graphics_mut}; +use material::Material; use pyo3::{ exceptions::PyRuntimeError, prelude::*, @@ -27,6 +29,7 @@ fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(size, m)?)?; m.add_function(wrap_pyfunction!(run, m)?)?; m.add_function(wrap_pyfunction!(mode_3d, m)?)?; @@ -48,6 +51,8 @@ fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(create_directional_light, m)?)?; m.add_function(wrap_pyfunction!(create_point_light, m)?)?; m.add_function(wrap_pyfunction!(create_spot_light, m)?)?; + m.add_function(wrap_pyfunction!(draw_sphere, m)?)?; + m.add_function(wrap_pyfunction!(use_material, m)?)?; Ok(()) } @@ -320,3 +325,20 @@ fn create_spot_light( ) -> PyResult { get_graphics(module)?.light_spot(r, g, b, intensity, range, radius, inner_angle, outer_angle) } + +#[pyfunction] +#[pyo3(pass_module, signature = (radius, sectors=32, stacks=18))] +fn draw_sphere( + module: &Bound<'_, PyModule>, + radius: f32, + sectors: u32, + stacks: u32, +) -> PyResult<()> { + get_graphics(module)?.draw_sphere(radius, sectors, stacks) +} + +#[pyfunction] +#[pyo3(pass_module, signature = (material))] +fn use_material(module: &Bound<'_, PyModule>, material: &Bound<'_, Material>) -> PyResult<()> { + get_graphics(module)?.use_material(&*material.extract::>()?) +} diff --git a/crates/processing_pyo3/src/material.rs b/crates/processing_pyo3/src/material.rs new file mode 100644 index 0000000..544ded1 --- /dev/null +++ b/crates/processing_pyo3/src/material.rs @@ -0,0 +1,37 @@ +use bevy::prelude::Entity; +use processing::prelude::*; +use pyo3::{exceptions::PyRuntimeError, prelude::*}; + +#[pyclass(unsendable)] +pub struct Material { + pub(crate) entity: Entity, +} + +#[pymethods] +impl Material { + #[new] + pub fn new() -> PyResult { + let entity = material_create_pbr().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(Self { entity }) + } + + pub fn set_float(&self, name: &str, value: f32) -> PyResult<()> { + material_set(self.entity, name, material::MaterialValue::Float(value)) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn set_float4(&self, name: &str, r: f32, g: f32, b: f32, a: f32) -> PyResult<()> { + material_set( + self.entity, + name, + material::MaterialValue::Float4([r, g, b, a]), + ) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } +} + +impl Drop for Material { + fn drop(&mut self) { + let _ = material_destroy(self.entity); + } +} diff --git a/crates/processing_render/src/error.rs b/crates/processing_render/src/error.rs index aca2f20..91eec57 100644 --- a/crates/processing_render/src/error.rs +++ b/crates/processing_render/src/error.rs @@ -30,4 +30,8 @@ pub enum ProcessingError { LayoutNotFound, #[error("Transform not found")] TransformNotFound, + #[error("Material not found")] + MaterialNotFound, + #[error("Unknown material property: {0}")] + UnknownMaterialProperty(String), } diff --git a/crates/processing_render/src/geometry/mod.rs b/crates/processing_render/src/geometry/mod.rs index 95f50fb..03a63ac 100644 --- a/crates/processing_render/src/geometry/mod.rs +++ b/crates/processing_render/src/geometry/mod.rs @@ -174,6 +174,28 @@ pub fn create_box( commands.spawn(Geometry::new(handle, layout_entity)).id() } +pub fn create_sphere( + In((radius, sectors, stacks)): In<(f32, u32, u32)>, + mut commands: Commands, + mut meshes: ResMut>, + builtins: Res, +) -> Entity { + let sphere = Sphere::new(radius); + let mesh = sphere.mesh().uv(sectors, stacks); + 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) diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index b6b5b80..ef282a0 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -29,6 +29,7 @@ use crate::{ Flush, error::{ProcessingError, Result}, image::{Image, bytes_to_pixels, create_readback_buffer, pixel_size, pixels_to_bytes}, + material::DefaultMaterial, render::{ RenderState, command::{CommandBuffer, DrawCommand}, @@ -186,6 +187,7 @@ pub fn create( mut layer_manager: ResMut, p_images: Query<&Image, With>, render_device: Res, + default_material: Res, ) -> Result { // find the surface entity, if it is an image, we will render to that image // otherwise we will render to the window @@ -243,7 +245,7 @@ pub fn create( Transform::from_xyz(0.0, 0.0, 999.9), render_layer, CommandBuffer::new(), - RenderState::default(), + RenderState::new(default_material.0), SurfaceSize(width, height), Graphics { readback_buffer, @@ -424,11 +426,15 @@ pub fn destroy( Ok(()) } -pub fn begin_draw(In(entity): In, mut state_query: Query<&mut RenderState>) -> Result<()> { +pub fn begin_draw( + In(entity): In, + mut state_query: Query<&mut RenderState>, + default_material: Res, +) -> Result<()> { let mut state = state_query .get_mut(entity) .map_err(|_| ProcessingError::GraphicsNotFound)?; - state.reset(); + state.reset(default_material.0); Ok(()) } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 7497e0e..24103a6 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -4,6 +4,7 @@ pub mod geometry; mod graphics; pub mod image; pub mod light; +pub mod material; pub mod render; pub mod sketch; mod surface; @@ -21,12 +22,12 @@ use bevy::{ prelude::*, render::render_resource::{Extent3d, TextureFormat}, }; +use render::material::add_standard_materials; use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; use tracing::debug; use crate::geometry::{AttributeFormat, AttributeValue}; use crate::graphics::flush; -use crate::render::material::add_standard_materials; use crate::{ graphics::GraphicsPlugin, image::ImagePlugin, light::LightPlugin, render::command::DrawCommand, surface::SurfacePlugin, @@ -265,14 +266,14 @@ fn create_app(config: Config) -> App { SurfacePlugin, geometry::GeometryPlugin, LightPlugin, + material::MaterialPlugin, )); app.add_systems(First, (clear_transient_meshes, activate_cameras)) .add_systems( Update, - ( - flush_draw_commands.before(AssetEventSystems), - add_standard_materials.after(flush_draw_commands), - ), + (flush_draw_commands, add_standard_materials) + .chain() + .before(AssetEventSystems), ); app @@ -1226,6 +1227,15 @@ pub fn geometry_box(width: f32, height: f32, depth: f32) -> error::Result error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(geometry::create_sphere, (radius, sectors, stacks)) + .unwrap()) + }) +} + pub fn poll_for_sketch_updates() -> error::Result> { app_mut(|app| { Ok(app @@ -1234,3 +1244,32 @@ pub fn poll_for_sketch_updates() -> error::Result> { .unwrap()) }) } + +pub fn material_create_pbr() -> error::Result { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached(material::create_pbr) + .unwrap()) + }) +} + +pub fn material_set( + entity: Entity, + name: impl Into, + value: material::MaterialValue, +) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(material::set_property, (entity, name.into(), value)) + .unwrap() + }) +} + +pub fn material_destroy(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(material::destroy, entity) + .unwrap() + }) +} diff --git a/crates/processing_render/src/material/mod.rs b/crates/processing_render/src/material/mod.rs new file mode 100644 index 0000000..4d89da0 --- /dev/null +++ b/crates/processing_render/src/material/mod.rs @@ -0,0 +1,90 @@ +pub mod pbr; + +use bevy::prelude::*; + +use crate::error::{ProcessingError, Result}; +use crate::render::material::UntypedMaterial; + +pub struct MaterialPlugin; + +impl Plugin for MaterialPlugin { + fn build(&self, app: &mut App) { + let world = app.world_mut(); + let handle = world + .resource_mut::>() + .add(StandardMaterial { + unlit: true, + cull_mode: None, + base_color: Color::WHITE, + ..default() + }); + let entity = world.spawn(UntypedMaterial(handle.untyped())).id(); + world.insert_resource(DefaultMaterial(entity)); + } +} + +#[derive(Resource)] +pub struct DefaultMaterial(pub Entity); + +#[derive(Debug, Clone)] +pub enum MaterialValue { + Float(f32), + Float2([f32; 2]), + Float3([f32; 3]), + Float4([f32; 4]), + Int(i32), + Int2([i32; 2]), + Int3([i32; 3]), + Int4([i32; 4]), + UInt(u32), + Mat4([f32; 16]), + Texture(Entity), +} + +pub fn create_pbr( + mut commands: Commands, + mut materials: ResMut>, +) -> Entity { + let handle = materials.add(StandardMaterial { + unlit: false, + cull_mode: None, + ..default() + }); + commands.spawn(UntypedMaterial(handle.untyped())).id() +} + +pub fn set_property( + In((entity, name, value)): In<(Entity, String, MaterialValue)>, + material_handles: Query<&UntypedMaterial>, + mut standard_materials: ResMut>, +) -> Result<()> { + let untyped = material_handles + .get(entity) + .map_err(|_| ProcessingError::MaterialNotFound)?; + let handle = untyped + .0 + .clone() + .try_typed::() + .map_err(|_| ProcessingError::MaterialNotFound)?; + let standard = standard_materials + .get_mut(&handle) + .ok_or(ProcessingError::MaterialNotFound)?; + pbr::set_property(standard, &name, &value)?; + Ok(()) +} + +pub fn destroy( + In(entity): In, + mut commands: Commands, + material_handles: Query<&UntypedMaterial>, + mut standard_materials: ResMut>, +) -> Result<()> { + let untyped = material_handles + .get(entity) + .map_err(|_| ProcessingError::MaterialNotFound)?; + if let Ok(handle) = untyped.0.clone().try_typed::() { + standard_materials.remove(&handle); + } + commands.entity(entity).despawn(); + Ok(()) +} diff --git a/crates/processing_render/src/material/pbr.rs b/crates/processing_render/src/material/pbr.rs new file mode 100644 index 0000000..c29e8fe --- /dev/null +++ b/crates/processing_render/src/material/pbr.rs @@ -0,0 +1,96 @@ +use bevy::prelude::*; +use bevy::render::alpha::AlphaMode; + +use super::MaterialValue; +use crate::error::{ProcessingError, Result}; + +/// Set a property on a StandardMaterial by name. +pub fn set_property( + material: &mut StandardMaterial, + name: &str, + value: &MaterialValue, +) -> Result<()> { + match name { + "base_color" | "color" => { + let MaterialValue::Float4(c) = value else { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects Float4, got {value:?}" + ))); + }; + material.base_color = Color::srgba(c[0], c[1], c[2], c[3]); + } + "metallic" => { + let MaterialValue::Float(v) = value else { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects Float, got {value:?}" + ))); + }; + material.metallic = *v; + } + "roughness" | "perceptual_roughness" => { + let MaterialValue::Float(v) = value else { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects Float, got {value:?}" + ))); + }; + material.perceptual_roughness = *v; + } + "reflectance" => { + let MaterialValue::Float(v) = value else { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects Float, got {value:?}" + ))); + }; + material.reflectance = *v; + } + "emissive" => { + let MaterialValue::Float4(c) = value else { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects Float4, got {value:?}" + ))); + }; + material.emissive = LinearRgba::new(c[0], c[1], c[2], c[3]); + } + "unlit" => { + let MaterialValue::Float(v) = value else { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects Float, got {value:?}" + ))); + }; + material.unlit = *v > 0.5; + } + "double_sided" => { + let MaterialValue::Float(v) = value else { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects Float, got {value:?}" + ))); + }; + material.double_sided = *v > 0.5; + } + "alpha_mode" => { + let MaterialValue::Int(v) = value else { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects Int, got {value:?}" + ))); + }; + material.alpha_mode = match v { + 0 => AlphaMode::Opaque, + // TODO: allow configuring the alpha cutoff value + 1 => AlphaMode::Mask(0.5), + 2 => AlphaMode::Blend, + 3 => AlphaMode::Premultiplied, + 4 => AlphaMode::Add, + 5 => AlphaMode::Multiply, + _ => { + return Err(ProcessingError::InvalidArgument(format!( + "unknown alpha_mode value: {v}" + ))); + } + }; + } + _ => { + return Err(ProcessingError::UnknownMaterialProperty(name.to_string())); + } + } + Ok(()) +} diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index f800a23..a26f915 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -9,6 +9,10 @@ pub enum DrawCommand { StrokeColor(Color), NoStroke, StrokeWeight(f32), + Roughness(f32), + Metallic(f32), + Emissive(Color), + Unlit, Rect { x: f32, y: f32, @@ -37,6 +41,17 @@ pub enum DrawCommand { angle: f32, }, Geometry(Entity), + Material(Entity), + Box { + width: f32, + height: f32, + depth: f32, + }, + Sphere { + radius: f32, + sectors: u32, + stacks: u32, + }, } #[derive(Debug, Default, Component)] diff --git a/crates/processing_render/src/render/material.rs b/crates/processing_render/src/render/material.rs index bfdccf5..92c19c1 100644 --- a/crates/processing_render/src/render/material.rs +++ b/crates/processing_render/src/render/material.rs @@ -15,19 +15,21 @@ pub enum MaterialKey { transparent: bool, background_image: Option>, }, - Pbr {}, + Pbr { + albedo: [u8; 4], + roughness: u8, + metallic: u8, + emissive: [u8; 4], + }, Custom(Entity), } impl MaterialKey { - pub fn to_material( - &self, - standard_materials: &mut ResMut>, - ) -> UntypedHandle { + pub fn to_material(&self, materials: &mut ResMut>) -> UntypedHandle { match self { MaterialKey::Color { - background_image, transparent, + background_image, } => { let mat = StandardMaterial { base_color: Color::WHITE, @@ -41,12 +43,37 @@ impl MaterialKey { }, ..default() }; - standard_materials.add(mat).untyped() + materials.add(mat).untyped() } - MaterialKey::Pbr { .. } => { - todo!("implement pbr materials") + MaterialKey::Pbr { + albedo, + roughness, + metallic, + emissive, + } => { + let base_color = Color::srgba( + albedo[0] as f32 / 255.0, + albedo[1] as f32 / 255.0, + albedo[2] as f32 / 255.0, + albedo[3] as f32 / 255.0, + ); + let mat = StandardMaterial { + base_color, + unlit: false, + cull_mode: None, + perceptual_roughness: *roughness as f32 / 255.0, + metallic: *metallic as f32 / 255.0, + emissive: LinearRgba::new( + emissive[0] as f32 / 255.0, + emissive[1] as f32 / 255.0, + emissive[2] as f32 / 255.0, + emissive[3] as f32 / 255.0, + ), + ..default() + }; + materials.add(mat).untyped() } - MaterialKey::Custom(_) => { + MaterialKey::Custom(_entity) => { todo!("implement custom materials") } } @@ -55,10 +82,7 @@ impl MaterialKey { /// A system that adds a `MeshMaterial3d` component to any entity with an `UntypedMaterial` that can /// be typed as a `StandardMaterial`. -pub(crate) fn add_standard_materials( - mut commands: Commands, - meshes: Query<(Entity, &UntypedMaterial)>, -) { +pub fn add_standard_materials(mut commands: Commands, meshes: Query<(Entity, &UntypedMaterial)>) { for (entity, handle) in meshes.iter() { let handle = handle.deref().clone(); if let Ok(handle) = handle.try_typed::() { diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index e19188c..bf6f467 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -12,11 +12,15 @@ use bevy::{ }; use command::{CommandBuffer, DrawCommand}; use material::MaterialKey; -use primitive::{TessellationMode, empty_mesh}; +use primitive::{TessellationMode, box_mesh, empty_mesh, sphere_mesh}; use transform::TransformStack; -use crate::render::material::UntypedMaterial; -use crate::{Flush, geometry::Geometry, image::Image, render::primitive::rect}; +use crate::{ + Flush, + geometry::Geometry, + image::Image, + render::{material::UntypedMaterial, primitive::rect}, +}; #[derive(Component)] #[relationship(relationship_target = TransientMeshes)] @@ -36,6 +40,7 @@ pub struct RenderResources<'w, 's> { struct BatchState { current_mesh: Option, material_key: Option, + active_material: Entity, transform: Affine3A, draw_index: u32, render_layers: RenderLayers, @@ -43,10 +48,11 @@ struct BatchState { } impl BatchState { - fn new(graphics_entity: Entity, render_layers: RenderLayers) -> Self { + fn new(graphics_entity: Entity, render_layers: RenderLayers, active_material: Entity) -> Self { Self { current_mesh: None, material_key: None, + active_material, transform: Affine3A::IDENTITY, draw_index: 0, render_layers, @@ -60,23 +66,36 @@ pub struct RenderState { pub fill_color: Option, pub stroke_color: Option, pub stroke_weight: f32, + pub material_key: MaterialKey, pub transform: TransformStack, + pub active_material: Entity, } -impl Default for RenderState { - fn default() -> Self { +impl RenderState { + pub fn new(default_material: Entity) -> Self { Self { fill_color: Some(Color::WHITE), stroke_color: Some(Color::BLACK), stroke_weight: 1.0, + material_key: MaterialKey::Color { + transparent: false, + background_image: None, + }, transform: TransformStack::new(), + active_material: default_material, } } -} -impl RenderState { - pub fn reset(&mut self) { - *self = Self::default(); + pub fn reset(&mut self, default_material: Entity) { + self.fill_color = Some(Color::WHITE); + self.stroke_color = Some(Color::BLACK); + self.stroke_weight = 1.0; + self.material_key = MaterialKey::Color { + transparent: false, + background_image: None, + }; + self.transform = TransformStack::new(); + self.active_material = default_material; } pub fn fill_is_transparent(&self) -> bool { @@ -103,6 +122,7 @@ pub fn flush_draw_commands( >, p_images: Query<&Image>, p_geometries: Query<&Geometry>, + p_material_handles: Query<&UntypedMaterial>, ) { for (graphics_entity, mut cmd_buffer, mut state, render_layers, projection, camera_transform) in graphics.iter_mut() @@ -111,7 +131,11 @@ pub fn flush_draw_commands( let view_from_world = camera_transform.to_matrix().inverse(); let world_from_clip = (clip_from_view * view_from_world).inverse(); let draw_commands = std::mem::take(&mut cmd_buffer.commands); - let mut batch = BatchState::new(graphics_entity, render_layers.clone()); + let mut batch = BatchState::new( + graphics_entity, + render_layers.clone(), + state.active_material, + ); for cmd in draw_commands { match cmd { @@ -130,6 +154,76 @@ pub fn flush_draw_commands( DrawCommand::StrokeWeight(weight) => { state.stroke_weight = weight; } + DrawCommand::Roughness(r) => { + state.material_key = match state.material_key { + MaterialKey::Pbr { + albedo, + metallic, + emissive, + .. + } => MaterialKey::Pbr { + albedo, + roughness: (r * 255.0) as u8, + metallic, + emissive, + }, + _ => MaterialKey::Pbr { + albedo: [255, 255, 255, 255], + roughness: (r * 255.0) as u8, + metallic: 0, + emissive: [0, 0, 0, 0], + }, + }; + } + DrawCommand::Metallic(m) => { + state.material_key = match state.material_key { + MaterialKey::Pbr { + albedo, + roughness, + emissive, + .. + } => MaterialKey::Pbr { + albedo, + roughness, + metallic: (m * 255.0) as u8, + emissive, + }, + _ => MaterialKey::Pbr { + albedo: [255, 255, 255, 255], + roughness: 128, + metallic: (m * 255.0) as u8, + emissive: [0, 0, 0, 0], + }, + }; + } + DrawCommand::Emissive(color) => { + let [r, g, b, a] = color.to_srgba().to_u8_array(); + state.material_key = match state.material_key { + MaterialKey::Pbr { + albedo, + roughness, + metallic, + .. + } => MaterialKey::Pbr { + albedo, + roughness, + metallic, + emissive: [r, g, b, a], + }, + _ => MaterialKey::Pbr { + albedo: [255, 255, 255, 255], + roughness: 128, + metallic: 0, + emissive: [r, g, b, a], + }, + }; + } + DrawCommand::Unlit => { + state.material_key = MaterialKey::Color { + transparent: state.fill_is_transparent(), + background_image: None, + }; + } DrawCommand::Rect { x, y, w, h, radii } => { add_fill(&mut res, &mut batch, &state, |mesh, color| { rect(mesh, x, y, w, h, radii, color, TessellationMode::Fill) @@ -211,24 +305,24 @@ pub fn flush_draw_commands( continue; }; - flush_batch(&mut res, &mut batch); - - // TODO: Implement state based material API - // https://github.com/processing/libprocessing/issues/10 - let material_key = MaterialKey::Color { - transparent: false, // TODO: detect from geometry colors - background_image: None, + let Some(mat_handle) = p_material_handles.get(batch.active_material).ok() + else { + warn!( + "Could not find material for entity {:?}", + batch.active_material + ); + continue; }; - let material_handle = material_key.to_material(&mut res.materials); - let z_offset = -(batch.draw_index as f32 * 0.001); + flush_batch(&mut res, &mut batch); + 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()), - UntypedMaterial(material_handle), + UntypedMaterial(mat_handle.0.clone()), BelongsToGraphics(batch.graphics_entity), transform, batch.render_layers.clone(), @@ -236,6 +330,30 @@ pub fn flush_draw_commands( batch.draw_index += 1; } + DrawCommand::Material(entity) => { + state.active_material = entity; + batch.active_material = entity; + flush_batch(&mut res, &mut batch); + } + DrawCommand::Box { + width, + height, + depth, + } => { + add_shape3d(&mut res, &mut batch, &state, box_mesh(width, height, depth)); + } + DrawCommand::Sphere { + radius, + sectors, + stacks, + } => { + add_shape3d( + &mut res, + &mut batch, + &state, + sphere_mesh(radius, sectors, stacks), + ); + } } } @@ -261,12 +379,11 @@ pub fn clear_transient_meshes( } fn spawn_mesh(res: &mut RenderResources, batch: &mut BatchState, mesh: Mesh, z_offset: f32) { - let Some(material_key) = &batch.material_key else { + let Some(key) = &batch.material_key else { return; }; let mesh_handle = res.meshes.add(mesh); - let material_handle = material_key.to_material(&mut res.materials); let (scale, rotation, translation) = batch.transform.to_scale_rotation_translation(); let transform = Transform { @@ -275,6 +392,8 @@ fn spawn_mesh(res: &mut RenderResources, batch: &mut BatchState, mesh: Mesh, z_o scale, }; + let material_handle = key.to_material(&mut res.materials); + res.commands.spawn(( Mesh3d(mesh_handle), UntypedMaterial(material_handle), @@ -285,9 +404,8 @@ fn spawn_mesh(res: &mut RenderResources, batch: &mut BatchState, mesh: Mesh, z_o } fn needs_batch(batch: &BatchState, state: &RenderState, material_key: &MaterialKey) -> bool { - let current_transform = state.transform.current(); let material_changed = batch.material_key.as_ref() != Some(material_key); - let transform_changed = batch.transform != current_transform; + let transform_changed = batch.transform != state.transform.current(); material_changed || transform_changed } @@ -303,6 +421,37 @@ fn start_batch( batch.current_mesh = Some(empty_mesh()); } +fn material_key_with_color(key: &MaterialKey, color: Color) -> MaterialKey { + match key { + MaterialKey::Color { + background_image, .. + } => MaterialKey::Color { + transparent: color.alpha() < 1.0, + background_image: background_image.clone(), + }, + MaterialKey::Pbr { + roughness, + metallic, + emissive, + .. + } => { + let [r, g, b, a] = color.to_srgba().to_u8_array(); + MaterialKey::Pbr { + albedo: [r, g, b, a], + roughness: *roughness, + metallic: *metallic, + emissive: *emissive, + } + } + MaterialKey::Custom(e) => MaterialKey::Custom(*e), + } +} + +fn material_key_with_fill(state: &RenderState) -> MaterialKey { + let color = state.fill_color.unwrap_or(Color::WHITE); + material_key_with_color(&state.material_key, color) +} + fn add_fill( res: &mut RenderResources, batch: &mut BatchState, @@ -312,10 +461,7 @@ fn add_fill( let Some(color) = state.fill_color else { return; }; - let material_key = MaterialKey::Color { - transparent: state.fill_is_transparent(), - background_image: None, - }; + let material_key = material_key_with_color(&state.material_key, color); if needs_batch(batch, state, &material_key) { start_batch(res, batch, state, material_key); @@ -336,10 +482,7 @@ fn add_stroke( return; }; let stroke_weight = state.stroke_weight; - let material_key = MaterialKey::Color { - transparent: state.stroke_is_transparent(), - background_image: None, - }; + let material_key = material_key_with_color(&state.material_key, color); if needs_batch(batch, state, &material_key) { start_batch(res, batch, state, material_key); @@ -359,6 +502,28 @@ fn flush_batch(res: &mut RenderResources, batch: &mut BatchState) { batch.material_key = None; } +fn add_shape3d(res: &mut RenderResources, batch: &mut BatchState, state: &RenderState, mesh: Mesh) { + flush_batch(res, batch); + + let mesh_handle = res.meshes.add(mesh); + let material_key = material_key_with_fill(state); + let material_handle = material_key.to_material(&mut res.materials); + + 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(mesh_handle), + UntypedMaterial(material_handle), + BelongsToGraphics(batch.graphics_entity), + transform, + batch.render_layers.clone(), + )); + + batch.draw_index += 1; +} + /// Creates a fullscreen quad by transforming NDC fullscreen by inverse of the clip-from-world matrix /// so that when the vertex shader applies clip_from_world, the vertices end up correctly back in /// NDC space. diff --git a/crates/processing_render/src/render/primitive/mod.rs b/crates/processing_render/src/render/primitive/mod.rs index 0b24e40..6ef4e93 100644 --- a/crates/processing_render/src/render/primitive/mod.rs +++ b/crates/processing_render/src/render/primitive/mod.rs @@ -1,4 +1,5 @@ mod rect; +mod shape3d; use bevy::{ asset::RenderAssetUsages, @@ -12,6 +13,7 @@ use lyon::{ }, }; pub use rect::rect; +pub use shape3d::{box_mesh, sphere_mesh}; use super::mesh_builder::MeshBuilder; diff --git a/crates/processing_render/src/render/primitive/shape3d.rs b/crates/processing_render/src/render/primitive/shape3d.rs new file mode 100644 index 0000000..2a630e2 --- /dev/null +++ b/crates/processing_render/src/render/primitive/shape3d.rs @@ -0,0 +1,11 @@ +use bevy::prelude::*; + +pub fn box_mesh(width: f32, height: f32, depth: f32) -> Mesh { + let cuboid = bevy::math::primitives::Cuboid::new(width, height, depth); + Mesh::from(cuboid) +} + +pub fn sphere_mesh(radius: f32, sectors: u32, stacks: u32) -> Mesh { + let sphere = bevy::math::primitives::Sphere::new(radius); + sphere.mesh().uv(sectors, stacks) +} diff --git a/crates/processing_wasm/src/lib.rs b/crates/processing_wasm/src/lib.rs index 8f269ac..572623d 100644 --- a/crates/processing_wasm/src/lib.rs +++ b/crates/processing_wasm/src/lib.rs @@ -2,9 +2,10 @@ use bevy::prelude::Entity; use processing_render::{ - config::Config, exit, graphics_begin_draw, graphics_end_draw, graphics_flush, - graphics_record_command, image_create, image_destroy, image_load, image_readback, image_resize, - init, render::command::DrawCommand, surface_create_from_canvas, surface_destroy, + config::Config, exit, geometry_box, geometry_sphere, graphics_begin_draw, graphics_end_draw, + graphics_flush, graphics_record_command, image_create, image_destroy, image_load, + image_readback, image_resize, init, material, material_create_pbr, material_destroy, + material_set, render::command::DrawCommand, surface_create_from_canvas, surface_destroy, surface_resize, transform_look_at, transform_reset, transform_rotate_axis, transform_rotate_x, transform_rotate_y, transform_rotate_z, transform_scale, transform_set_position, transform_set_rotation, transform_set_scale, transform_translate, @@ -342,3 +343,56 @@ pub fn js_transform_look_at( pub fn js_transform_reset(entity_id: u64) -> Result<(), JsValue> { check(transform_reset(Entity::from_bits(entity_id))) } + +#[wasm_bindgen(js_name = "materialCreatePbr")] +pub fn js_material_create_pbr() -> Result { + check(material_create_pbr().map(|e| e.to_bits())) +} + +#[wasm_bindgen(js_name = "materialSetFloat")] +pub fn js_material_set_float(mat_id: u64, name: &str, value: f32) -> Result<(), JsValue> { + check(material_set( + Entity::from_bits(mat_id), + name, + material::MaterialValue::Float(value), + )) +} + +#[wasm_bindgen(js_name = "materialSetFloat4")] +pub fn js_material_set_float4( + mat_id: u64, + name: &str, + r: f32, + g: f32, + b: f32, + a: f32, +) -> Result<(), JsValue> { + check(material_set( + Entity::from_bits(mat_id), + name, + material::MaterialValue::Float4([r, g, b, a]), + )) +} + +#[wasm_bindgen(js_name = "materialDestroy")] +pub fn js_material_destroy(mat_id: u64) -> Result<(), JsValue> { + check(material_destroy(Entity::from_bits(mat_id))) +} + +#[wasm_bindgen(js_name = "material")] +pub fn js_material(surface_id: u64, mat_id: u64) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::Material(Entity::from_bits(mat_id)), + )) +} + +#[wasm_bindgen(js_name = "geometryBox")] +pub fn js_geometry_box(width: f32, height: f32, depth: f32) -> Result { + check(geometry_box(width, height, depth).map(|e| e.to_bits())) +} + +#[wasm_bindgen(js_name = "geometrySphere")] +pub fn js_geometry_sphere(radius: f32, sectors: u32, stacks: u32) -> Result { + check(geometry_sphere(radius, sectors, stacks).map(|e| e.to_bits())) +} diff --git a/examples/lights.rs b/examples/lights.rs index d2aa4b8..270e2cf 100644 --- a/examples/lights.rs +++ b/examples/lights.rs @@ -28,6 +28,8 @@ fn sketch() -> error::Result<()> { 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)?; + let pbr_mat = material_create_pbr()?; + material_set(pbr_mat, "roughness", material::MaterialValue::Float(0.0))?; // We will only declare lights in `setup` // rather than calling some sort of `light()` method inside of `draw` @@ -85,6 +87,9 @@ fn sketch() -> error::Result<()> { DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.18, 0.20, 0.15)), )?; + graphics_record_command(graphics, DrawCommand::Fill(bevy::color::Color::WHITE))?; + graphics_record_command(graphics, DrawCommand::Material(pbr_mat))?; + graphics_record_command(graphics, DrawCommand::PushMatrix)?; graphics_record_command(graphics, DrawCommand::Rotate { angle })?; graphics_record_command(graphics, DrawCommand::Geometry(box_geo))?; diff --git a/examples/materials.rs b/examples/materials.rs new file mode 100644 index 0000000..7ebc42f --- /dev/null +++ b/examples/materials.rs @@ -0,0 +1,101 @@ +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 width = 800; + let height = 600; + let mut glfw_ctx = GlfwContext::new(width, height)?; + init(Config::default())?; + + let surface = glfw_ctx.create_surface(width, height, 1.0)?; + let graphics = graphics_create(surface, width, height)?; + let sphere = geometry_sphere(30.0, 32, 18)?; + + graphics_mode_3d(graphics)?; + transform_set_position(graphics, 0.0, 0.0, 600.0)?; + transform_look_at(graphics, 0.0, 0.0, 0.0)?; + + let dir_light = + light_create_directional(graphics, bevy::color::Color::srgb(1.0, 0.98, 0.95), 1_500.0)?; + transform_set_position(dir_light, 300.0, 400.0, 300.0)?; + transform_look_at(dir_light, 0.0, 0.0, 0.0)?; + + let point_light = + light_create_point(graphics, bevy::color::Color::WHITE, 100_000.0, 800.0, 0.0)?; + transform_set_position(point_light, 200.0, 200.0, 400.0)?; + + // Grid of materials varying roughness (x) and metallic (y) + let cols = 11; + let rows = 5; + let mut materials = Vec::new(); + + for row in 0..rows { + for col in 0..cols { + let mat = material_create_pbr()?; + let roughness = col as f32 / (cols - 1) as f32; + let metallic = row as f32 / (rows - 1) as f32; + + material_set(mat, "roughness", material::MaterialValue::Float(roughness))?; + material_set(mat, "metallic", material::MaterialValue::Float(metallic))?; + materials.push(mat); + } + } + + let base_color = bevy::color::Color::srgb(1.0, 0.85, 0.57); + let spacing = 70.0; + let offset_x = (cols - 1) as f32 * spacing / 2.0; + let offset_y = (rows - 1) as f32 * spacing / 2.0; + + while glfw_ctx.poll_events() { + graphics_begin_draw(graphics)?; + + graphics_record_command( + graphics, + DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.05, 0.05, 0.07)), + )?; + + graphics_record_command(graphics, DrawCommand::Fill(base_color))?; + + for row in 0..rows { + for col in 0..cols { + let mat = materials[row * cols + col]; + + graphics_record_command(graphics, DrawCommand::PushMatrix)?; + graphics_record_command( + graphics, + DrawCommand::Translate { + x: col as f32 * spacing - offset_x, + y: row as f32 * spacing - offset_y, + }, + )?; + graphics_record_command(graphics, DrawCommand::Material(mat))?; + graphics_record_command(graphics, DrawCommand::Geometry(sphere))?; + graphics_record_command(graphics, DrawCommand::PopMatrix)?; + } + } + + graphics_end_draw(graphics)?; + } + + for mat in materials { + material_destroy(mat)?; + } + + Ok(()) +} diff --git a/examples/pbr.rs b/examples/pbr.rs new file mode 100644 index 0000000..a2edcf3 --- /dev/null +++ b/examples/pbr.rs @@ -0,0 +1,91 @@ +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 width = 800; + let height = 600; + let mut glfw_ctx = GlfwContext::new(width, height)?; + init(Config::default())?; + + let surface = glfw_ctx.create_surface(width, height, 1.0)?; + let graphics = graphics_create(surface, width, height)?; + + graphics_mode_3d(graphics)?; + transform_set_position(graphics, 0.0, 0.0, 600.0)?; + transform_look_at(graphics, 0.0, 0.0, 0.0)?; + + let dir_light = + light_create_directional(graphics, bevy::color::Color::srgb(1.0, 0.98, 0.95), 1_500.0)?; + transform_set_position(dir_light, 300.0, 400.0, 300.0)?; + transform_look_at(dir_light, 0.0, 0.0, 0.0)?; + + let point_light = + light_create_point(graphics, bevy::color::Color::WHITE, 100_000.0, 800.0, 0.0)?; + transform_set_position(point_light, 200.0, 200.0, 400.0)?; + + let cols = 11; + let rows = 5; + let base_color = bevy::color::Color::srgb(1.0, 0.85, 0.57); + let spacing = 70.0; + let offset_x = (cols - 1) as f32 * spacing / 2.0; + let offset_y = (rows - 1) as f32 * spacing / 2.0; + + while glfw_ctx.poll_events() { + graphics_begin_draw(graphics)?; + + graphics_record_command( + graphics, + DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.05, 0.05, 0.07)), + )?; + + graphics_record_command(graphics, DrawCommand::Fill(base_color))?; + + for row in 0..rows { + for col in 0..cols { + let roughness = col as f32 / (cols - 1) as f32; + let metallic = row as f32 / (rows - 1) as f32; + + graphics_record_command(graphics, DrawCommand::Roughness(roughness))?; + graphics_record_command(graphics, DrawCommand::Metallic(metallic))?; + + graphics_record_command(graphics, DrawCommand::PushMatrix)?; + graphics_record_command( + graphics, + DrawCommand::Translate { + x: col as f32 * spacing - offset_x, + y: row as f32 * spacing - offset_y, + }, + )?; + graphics_record_command( + graphics, + DrawCommand::Sphere { + radius: 30.0, + sectors: 32, + stacks: 18, + }, + )?; + graphics_record_command(graphics, DrawCommand::PopMatrix)?; + } + } + + graphics_end_draw(graphics)?; + } + + Ok(()) +}